diff --git a/openbas-api/src/main/java/io/openbas/execution/ExecutionExecutorService.java b/openbas-api/src/main/java/io/openbas/execution/ExecutionExecutorService.java index 0d3aacf1e3..eaead67f2f 100644 --- a/openbas-api/src/main/java/io/openbas/execution/ExecutionExecutorService.java +++ b/openbas-api/src/main/java/io/openbas/execution/ExecutionExecutorService.java @@ -1,73 +1,54 @@ package io.openbas.execution; import io.openbas.database.model.*; -import io.openbas.database.model.Executor; -import io.openbas.database.model.InjectStatus; 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; +import io.openbas.executors.ExecutorContextService; import io.openbas.rest.exception.AgentException; -import io.openbas.service.AssetGroupService; +import io.openbas.rest.inject.service.InjectService; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; import org.hibernate.Hibernate; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; @RequiredArgsConstructor @Service @Log public class ExecutionExecutorService { - private final AssetGroupService assetGroupService; - private final CalderaExecutorConfig calderaExecutorConfig; - 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 ApplicationContext context; + private final InjectStatusRepository injectStatusRepository; + private final InjectService injectService; public void launchExecutorContext(Inject inject) { - // First, get the assets of this injects - List assets = - Stream.concat( - inject.getAssets().stream(), - inject.getAssetGroups().stream() - .flatMap( - assetGroup -> - this.assetGroupService - .assetsFromAssetGroup(assetGroup.getId()) - .stream())) - .toList(); + // First, get the agents of this injects + List agents = this.injectService.getAgentsByInject(inject); + InjectStatus injectStatus = - inject.getStatus().orElseThrow(() -> new IllegalArgumentException("Status should exists")); + inject.getStatus().orElseThrow(() -> new IllegalArgumentException("Status should exist")); AtomicBoolean atLeastOneExecution = new AtomicBoolean(false); AtomicBoolean atOneTraceAdded = new AtomicBoolean(false); - assets.forEach( - asset -> { + + agents.forEach( + agent -> { try { - launchExecutorContextForAsset(inject, asset); + launchExecutorContextForAgent(inject, agent); atLeastOneExecution.set(true); } catch (AgentException e) { ExecutionTraceStatus traceStatus = - e.getMessage().startsWith("Asset error") - ? ExecutionTraceStatus.ASSET_INACTIVE + e.getMessage().startsWith("Agent error") + ? ExecutionTraceStatus.AGENT_INACTIVE : ExecutionTraceStatus.ERROR; injectStatus.addTrace( traceStatus, e.getMessage(), ExecutionTraceAction.COMPLETE, e.getAgent()); atOneTraceAdded.set(true); } }); - // if launchExecutorContextForAsset fail for every assets we throw to manually set injectStatus + // if launchExecutorContextForAgent fail for every agent we throw to manually set injectStatus // to error if (atOneTraceAdded.get()) { this.injectStatusRepository.save(injectStatus); @@ -77,47 +58,30 @@ public void launchExecutorContext(Inject inject) { } } - private void launchExecutorContextForAsset(Inject inject, Asset asset) { - Endpoint assetEndpoint = (Endpoint) Hibernate.unproxy(asset); - Executor executor = assetEndpoint.getExecutor(); + private void launchExecutorContextForAgent(Inject inject, Agent agent) throws AgentException { + Endpoint assetEndpoint = (Endpoint) Hibernate.unproxy(agent.getAsset()); + Executor executor = agent.getExecutor(); if (executor == null) { - log.log(Level.SEVERE, "Cannot find the executor for the asset " + assetEndpoint.getName()); - } else if (!assetEndpoint.getActive()) { throw new AgentException( - "Asset error: " + assetEndpoint.getName() + " is inactive", - assetEndpoint.getAgents().getFirst()); + "Cannot find the executor for the agent " + + agent.getExecutedByUser() + + " from the asset " + + assetEndpoint.getName(), + agent); + } else if (!agent.isActive()) { + throw new AgentException( + "Agent error: agent " + + agent.getExecutedByUser() + + " is inactive for the asset " + + assetEndpoint.getName(), + agent); } else { - switch (executor.getType()) { - case "openbas_caldera" -> { - if (!this.calderaExecutorConfig.isEnable()) { - throw new AgentException( - "Fatal error: Caldera executor is not enabled", - assetEndpoint.getAgents().getFirst()); - } - this.calderaExecutorContextService.launchExecutorSubprocess(inject, assetEndpoint); - } - case "openbas_tanium" -> { - if (!this.taniumExecutorConfig.isEnable()) { - throw new AgentException( - "Fatal error: Tanium executor is not enabled", - assetEndpoint.getAgents().getFirst()); - } - this.taniumExecutorContextService.launchExecutorSubprocess(inject, assetEndpoint); - } - case "openbas_crowdstrike" -> { - if (!this.crowdStrikeExecutorConfig.isEnable()) { - throw new AgentException( - "Fatal error: CrowdStrike executor is not enabled", - assetEndpoint.getAgents().getFirst()); - } - this.crowdStrikeExecutorContextService.launchExecutorSubprocess(inject, assetEndpoint); - } - case "openbas_agent" -> - this.openBASExecutorContextService.launchExecutorSubprocess(inject, assetEndpoint); - default -> - throw new AgentException( - "Fatal error: Unsupported executor " + executor.getType(), - assetEndpoint.getAgents().getFirst()); + try { + ExecutorContextService executorContextService = + context.getBean(agent.getExecutor().getName(), ExecutorContextService.class); + executorContextService.launchExecutorSubprocess(inject, assetEndpoint, agent); + } catch (NoSuchBeanDefinitionException e) { + throw new AgentException("Fatal error: Unsupported executor " + executor.getType(), agent); } } } diff --git a/openbas-api/src/main/java/io/openbas/executors/ExecutorContextService.java b/openbas-api/src/main/java/io/openbas/executors/ExecutorContextService.java new file mode 100644 index 0000000000..0864c5af47 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/executors/ExecutorContextService.java @@ -0,0 +1,12 @@ +package io.openbas.executors; + +import io.openbas.database.model.Agent; +import io.openbas.database.model.Endpoint; +import io.openbas.database.model.Inject; +import io.openbas.rest.exception.AgentException; + +public abstract class ExecutorContextService { + + public abstract void launchExecutorSubprocess(Inject inject, Endpoint assetEndpoint, Agent agent) + throws AgentException; +} diff --git a/openbas-framework/src/main/java/io/openbas/integrations/ExecutorService.java b/openbas-api/src/main/java/io/openbas/executors/ExecutorService.java similarity index 98% rename from openbas-framework/src/main/java/io/openbas/integrations/ExecutorService.java rename to openbas-api/src/main/java/io/openbas/executors/ExecutorService.java index 48805e0a33..d8dbdbd50e 100644 --- a/openbas-framework/src/main/java/io/openbas/integrations/ExecutorService.java +++ b/openbas-api/src/main/java/io/openbas/executors/ExecutorService.java @@ -1,4 +1,4 @@ -package io.openbas.integrations; +package io.openbas.executors; import static io.openbas.service.FileService.EXECUTORS_IMAGES_BASE_PATH; diff --git a/openbas-api/src/main/java/io/openbas/executors/caldera/CalderaExecutor.java b/openbas-api/src/main/java/io/openbas/executors/caldera/CalderaExecutor.java index 272d9cc93f..18f339f460 100644 --- a/openbas-api/src/main/java/io/openbas/executors/caldera/CalderaExecutor.java +++ b/openbas-api/src/main/java/io/openbas/executors/caldera/CalderaExecutor.java @@ -1,11 +1,12 @@ package io.openbas.executors.caldera; +import io.openbas.executors.ExecutorService; import io.openbas.executors.caldera.client.CalderaExecutorClient; import io.openbas.executors.caldera.config.CalderaExecutorConfig; import io.openbas.executors.caldera.service.CalderaExecutorContextService; import io.openbas.executors.caldera.service.CalderaExecutorService; -import io.openbas.integrations.ExecutorService; import io.openbas.integrations.InjectorService; +import io.openbas.service.AgentService; import io.openbas.service.EndpointService; import io.openbas.service.PlatformSettingsService; import jakarta.annotation.PostConstruct; @@ -26,6 +27,7 @@ public class CalderaExecutor { private final ExecutorService executorService; private final InjectorService injectorService; private final PlatformSettingsService platformSettingsService; + private final AgentService agentService; @PostConstruct public void init() { @@ -37,7 +39,8 @@ public void init() { this.calderaExecutorContextService, this.endpointService, this.injectorService, - this.platformSettingsService); + this.platformSettingsService, + this.agentService); if (this.config.isEnable()) { this.taskScheduler.scheduleAtFixedRate(service, Duration.ofSeconds(60)); } diff --git a/openbas-api/src/main/java/io/openbas/executors/caldera/client/CalderaExecutorClient.java b/openbas-api/src/main/java/io/openbas/executors/caldera/client/CalderaExecutorClient.java index 8b16eea751..b2867c4021 100644 --- a/openbas-api/src/main/java/io/openbas/executors/caldera/client/CalderaExecutorClient.java +++ b/openbas-api/src/main/java/io/openbas/executors/caldera/client/CalderaExecutorClient.java @@ -51,13 +51,9 @@ public List agents() { } } - public void deleteAgent(Endpoint endpoint) { + public void deleteAgent(final String externalReference) { try { - this.delete( - this.config.getRestApiV2Url() - + AGENT_URI - + "/" - + endpoint.getAgents().getFirst().getExternalReference()); + this.delete(this.config.getRestApiV2Url() + AGENT_URI + "/" + externalReference); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorContextService.java b/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorContextService.java index 4422a87dd0..24fa883275 100644 --- a/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorContextService.java +++ b/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorContextService.java @@ -1,34 +1,30 @@ package io.openbas.executors.caldera.service; +import static io.openbas.executors.caldera.service.CalderaExecutorService.CALDERA_EXECUTOR_NAME; + import io.openbas.database.model.*; +import io.openbas.executors.ExecutorContextService; import io.openbas.executors.caldera.client.CalderaExecutorClient; import io.openbas.executors.caldera.client.model.Ability; +import io.openbas.executors.caldera.config.CalderaExecutorConfig; import io.openbas.integrations.InjectorService; +import io.openbas.rest.exception.AgentException; import jakarta.validation.constraints.NotNull; import java.util.HashMap; import java.util.List; import java.util.Map; +import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Log -@Service -public class CalderaExecutorContextService { - - private InjectorService injectorService; +@Service(CALDERA_EXECUTOR_NAME) +@RequiredArgsConstructor +public class CalderaExecutorContextService extends ExecutorContextService { - private CalderaExecutorClient calderaExecutorClient; - - @Autowired - public void setInjectorService(InjectorService injectorService) { - this.injectorService = injectorService; - } - - @Autowired - public void setCalderaExecutorClient(CalderaExecutorClient calderaExecutorClient) { - this.calderaExecutorClient = calderaExecutorClient; - } + private final CalderaExecutorConfig calderaExecutorConfig; + private final InjectorService injectorService; + private final CalderaExecutorClient calderaExecutorClient; public final Map injectorExecutorAbilities = new HashMap<>(); public final Map injectorExecutorClearAbilities = new HashMap<>(); @@ -71,7 +67,15 @@ public void registerAbilities() { } public void launchExecutorSubprocess( - @NotNull final Inject inject, @NotNull final Endpoint assetEndpoint) { + @NotNull final Inject inject, + @NotNull final Endpoint assetEndpoint, + @NotNull final Agent agent) + throws AgentException { + + if (!this.calderaExecutorConfig.isEnable()) { + throw new AgentException("Fatal error: Caldera executor is not enabled", agent); + } + inject .getInjectorContract() .map(InjectorContract::getInjector) @@ -81,26 +85,21 @@ public void launchExecutorSubprocess( List> additionalFields = List.of( Map.of("trait", "inject", "value", inject.getId()), - Map.of( - "trait", - "agent", - "value", - assetEndpoint.getAgents().getFirst().getId())); + Map.of("trait", "agent", "value", agent.getId())); calderaExecutorClient.exploit( "base64", - assetEndpoint.getAgents().getFirst().getExternalReference(), + agent.getExternalReference(), this.injectorExecutorAbilities.get(injector.getId()).getAbility_id(), additionalFields); } }); } - public void launchExecutorClear( - @NotNull final Injector injector, @NotNull final Endpoint assetEndpoint) { + public void launchExecutorClear(@NotNull final Injector injector, @NotNull final Agent agent) { if (this.injectorExecutorAbilities.containsKey(injector.getId())) { calderaExecutorClient.exploit( "base64", - assetEndpoint.getAgents().getFirst().getExternalReference(), + agent.getExternalReference(), this.injectorExecutorClearAbilities.get(injector.getId()).getAbility_id(), List.of()); } diff --git a/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorService.java b/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorService.java index aac3584f3e..d08f6c1146 100644 --- a/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorService.java +++ b/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorService.java @@ -1,29 +1,26 @@ package io.openbas.executors.caldera.service; +import static io.openbas.service.EndpointService.DELETE_TTL; +import static io.openbas.utils.Time.toInstant; import static java.time.Instant.now; -import static java.time.ZoneOffset.UTC; import com.cronutils.utils.VisibleForTesting; import io.openbas.database.model.*; +import io.openbas.executors.ExecutorService; import io.openbas.executors.caldera.client.CalderaExecutorClient; import io.openbas.executors.caldera.config.CalderaExecutorConfig; import io.openbas.executors.caldera.model.Agent; -import io.openbas.integrations.ExecutorService; import io.openbas.integrations.InjectorService; +import io.openbas.service.AgentService; import io.openbas.service.EndpointService; import io.openbas.service.PlatformSettingsService; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Optional; +import java.util.*; import java.util.logging.Level; +import java.util.stream.Collectors; import lombok.extern.java.Log; +import org.hibernate.Hibernate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -32,9 +29,8 @@ public class CalderaExecutorService implements Runnable { private static final int CLEAR_TTL = 1800000; // 30 minutes - private static final int DELETE_TTL = 86400000; // 24 hours private static final String CALDERA_EXECUTOR_TYPE = "openbas_caldera"; - private static final String CALDERA_EXECUTOR_NAME = "Caldera"; + public static final String CALDERA_EXECUTOR_NAME = "Caldera"; private final CalderaExecutorClient client; @@ -44,6 +40,7 @@ public class CalderaExecutorService implements Runnable { private final InjectorService injectorService; private final PlatformSettingsService platformSettingsService; + private final AgentService agentService; private Executor executor = null; @@ -72,12 +69,14 @@ public CalderaExecutorService( CalderaExecutorContextService calderaExecutorContextService, EndpointService endpointService, InjectorService injectorService, - PlatformSettingsService platformSettingsService) { + PlatformSettingsService platformSettingsService, + AgentService agentService) { this.client = client; this.endpointService = endpointService; this.calderaExecutorContextService = calderaExecutorContextService; this.injectorService = injectorService; this.platformSettingsService = platformSettingsService; + this.agentService = agentService; try { if (config.isEnable()) { this.executor = @@ -110,126 +109,158 @@ public void run() { // This is NOT a standard behaviour, this is because we are using Caldera as an executor and // we should not // Will be replaced by the XTM agent - List agents = - this.client.agents().stream() - .filter(agent -> !agent.getExe_name().contains("implant")) - .toList(); - log.info("Caldera executor provisioning based on " + agents.size() + " assets"); - agents.forEach( - agent -> { - Optional existingEndpoint = findExistingEndpointForAnAgent(agent); - - if (existingEndpoint.isEmpty()) { - Optional endpointByExternalReference = - endpointService.findByExternalReference(agent.getPaw()); - if (endpointByExternalReference.isPresent()) { - this.updateEndpoint(agent, endpointByExternalReference.get()); - } else { - this.endpointService.createEndpoint(toEndpoint(agent)); - } - } else { - this.updateEndpoint(agent, existingEndpoint.get()); - } - }); - List inactiveEndpoints = - toEndpoint(agents).stream().filter(endpoint -> !endpoint.getActive()).toList(); - inactiveEndpoints.forEach( - endpoint -> { - Optional optionalExistingEndpoint = - this.endpointService.findByExternalReference( - endpoint.getAgents().getFirst().getExternalReference()); - if (optionalExistingEndpoint.isPresent()) { - Endpoint existingEndpoint = optionalExistingEndpoint.get(); - if ((now().toEpochMilli() - existingEndpoint.getClearedAt().toEpochMilli()) - > DELETE_TTL) { - log.info("Found stale agent " + existingEndpoint.getName() + ", deleting it..."); - this.client.deleteAgent(existingEndpoint); - this.endpointService.deleteEndpoint(existingEndpoint.getId()); - } - } - }); + List endpointAgentList = + toAgentEndpoint( + this.client.agents().stream() + .filter(agent -> !agent.getExe_name().contains("implant")) + .toList()); + log.info("Caldera executor provisioning based on " + endpointAgentList.size() + " assets"); + + for (io.openbas.database.model.Agent agent : endpointAgentList) { + registerAgentEndpoint(agent); + } this.platformSettingsService.cleanMessage(BannerMessage.BANNER_KEYS.CALDERA_UNAVAILABLE); } catch (Exception e) { this.platformSettingsService.errorMessage(BannerMessage.BANNER_KEYS.CALDERA_UNAVAILABLE); } } - // -- PRIVATE -- + private void registerAgentEndpoint(io.openbas.database.model.Agent agent) { + Endpoint endpoint = (Endpoint) Hibernate.unproxy(agent.getAsset()); + Optional optionalEndpoint = + this.endpointService.findEndpointByAgentDetails( + endpoint.getHostname(), endpoint.getPlatform(), endpoint.getArch()); - @VisibleForTesting - protected Optional findExistingEndpointForAnAgent(@NotNull final Agent agent) { - return this.endpointService.findAssetsForInjectionByHostname(agent.getHost()).stream() - .filter( - endpoint -> - Arrays.stream(endpoint.getIps()) - .anyMatch(Arrays.asList(agent.getHost_ip_addrs())::contains) - && endpoint.getExecutor() != null - && CALDERA_EXECUTOR_TYPE.equals(endpoint.getExecutor().getType())) - .findFirst(); + if (agent.isActive()) { + // Endpoint already created -> attributes to update + handleActiveAgent(agent, optionalEndpoint, endpoint); + } else { + handleInactiveAgent(agent, optionalEndpoint, endpoint); + } + } + + private void handleInactiveAgent( + io.openbas.database.model.Agent agent, + Optional optionalEndpoint, + Endpoint endpoint) { + if (optionalEndpoint.isPresent()) { + Optional optionalAgent = + this.agentService.getAgentForAnAsset( + optionalEndpoint.get().getId(), + agent.getExecutedByUser(), + agent.getDeploymentMode(), + agent.getPrivilege(), + CALDERA_EXECUTOR_TYPE); + if (optionalAgent.isPresent()) { + io.openbas.database.model.Agent existingAgent = optionalAgent.get(); + if ((now().toEpochMilli() - agent.getLastSeen().toEpochMilli()) > DELETE_TTL) { + log.info( + "Found stale endpoint " + + endpoint.getName() + + ", deleting the agent " + + existingAgent.getExecutedByUser() + + " in it..."); + this.client.deleteAgent(existingAgent.getExternalReference()); + this.agentService.deleteAgent(existingAgent.getId()); + } + } + } + } + + private void handleActiveAgent( + io.openbas.database.model.Agent agent, + Optional optionalEndpoint, + Endpoint endpoint) { + if (optionalEndpoint.isPresent()) { + + Endpoint endpointToUpdate = optionalEndpoint.get(); + endpointToUpdate.setIps(endpoint.getIps()); + endpointToUpdate.setMacAddresses(endpoint.getMacAddresses()); + this.endpointService.updateEndpoint(endpointToUpdate); + + Optional optionalAgent = + this.agentService.getAgentForAnAsset( + endpointToUpdate.getId(), + agent.getExecutedByUser(), + agent.getDeploymentMode(), + agent.getPrivilege(), + CALDERA_EXECUTOR_TYPE); + + // Agent already created -> attributes to update + if (optionalAgent.isPresent()) { + updateExistingAgent(agent, optionalAgent, endpointToUpdate); + } else { + // New agent to create for the endpoint + this.endpointService.createNewAgent(agent, endpointToUpdate); + } + } else { + // New endpoint and new agent to create + this.endpointService.createNewEndpointAndAgent(agent, endpoint); + } } - private Endpoint toEndpoint(@NotNull final Agent agent) { - Endpoint endpoint = new Endpoint(); - endpoint.setName(agent.getHost()); - endpoint.setDescription("Asset collected by Caldera executor context."); - endpoint.setIps(agent.getHost_ip_addrs()); - endpoint.setHostname(agent.getHost()); - endpoint.setPlatform(toPlatform(agent.getPlatform())); - endpoint.setArch(toArch(agent.getArchitecture())); - io.openbas.database.model.Agent agentEndpoint = new io.openbas.database.model.Agent(); - agentEndpoint.setExecutor(this.executor); - agentEndpoint.setExternalReference(agent.getPaw()); - agentEndpoint.setPrivilege(io.openbas.database.model.Agent.PRIVILEGE.admin); - agentEndpoint.setDeploymentMode(io.openbas.database.model.Agent.DEPLOYMENT_MODE.session); - agentEndpoint.setExecutedByUser(agent.getUsername()); - agentEndpoint.setLastSeen(toInstant(agent.getLast_seen())); - agentEndpoint.setProcessName(agent.getExe_name()); - agentEndpoint.setAsset(endpoint); - endpoint.setAgents(List.of(agentEndpoint)); - return endpoint; + private void updateExistingAgent( + io.openbas.database.model.Agent agent, + Optional optionalAgent, + Endpoint endpointToUpdate) { + io.openbas.database.model.Agent agentToUpdate = optionalAgent.get(); + agentToUpdate.setAsset(endpointToUpdate); + agentToUpdate.setLastSeen(agent.getLastSeen()); + agentToUpdate.setExternalReference(agent.getExternalReference()); + agentToUpdate.setProcessName(agent.getProcessName()); + clearAbilityForAgent(agentToUpdate); + this.agentService.createOrUpdateAgent(agentToUpdate); } - private List toEndpoint(@NotNull final List agents) { - return agents.stream().map(this::toEndpoint).toList(); + // -- PRIVATE -- + + private List toAgentEndpoint( + @NotNull final List agentsCaldera) { + return agentsCaldera.stream() + .map( + agent -> { + Endpoint endpoint = new Endpoint(); + endpoint.setName(agent.getHost()); + endpoint.setDescription("Asset collected by Caldera executor context."); + endpoint.setIps(agent.getHost_ip_addrs()); + endpoint.setHostname(agent.getHost()); + endpoint.setPlatform(toPlatform(agent.getPlatform())); + endpoint.setArch(toArch(agent.getArchitecture())); + io.openbas.database.model.Agent agentEndpoint = new io.openbas.database.model.Agent(); + agentEndpoint.setExecutor(this.executor); + agentEndpoint.setExternalReference(agent.getPaw()); + agentEndpoint.setPrivilege(io.openbas.database.model.Agent.PRIVILEGE.admin); + agentEndpoint.setDeploymentMode( + io.openbas.database.model.Agent.DEPLOYMENT_MODE.session); + agentEndpoint.setExecutedByUser(agent.getUsername()); + agentEndpoint.setLastSeen(toInstant(agent.getLast_seen())); + agentEndpoint.setProcessName(agent.getExe_name()); + agentEndpoint.setAsset(endpoint); + return agentEndpoint; + }) + .collect(Collectors.toList()); } - private void updateEndpoint( - @NotNull final Agent agent, @NotNull final Endpoint existingEndpoint) { - existingEndpoint.getAgents().getFirst().setLastSeen(toInstant(agent.getLast_seen())); - existingEndpoint.getAgents().getFirst().setExternalReference(agent.getPaw()); - existingEndpoint.getAgents().getFirst().setExecutedByUser(agent.getUsername()); - existingEndpoint.getAgents().getFirst().setExecutor(this.executor); - existingEndpoint.setName(agent.getHost()); - existingEndpoint.setIps(agent.getHost_ip_addrs()); - existingEndpoint.setHostname(agent.getHost()); - existingEndpoint.getAgents().getFirst().setProcessName(agent.getExe_name()); - existingEndpoint.setPlatform(toPlatform(agent.getPlatform())); - existingEndpoint.setArch(toArch(agent.getArchitecture())); - if ((now().toEpochMilli() - existingEndpoint.getClearedAt().toEpochMilli()) > CLEAR_TTL) { + /** + * Used to delete existing agent in Caldera application if the clear ttl is reached (that means if + * agent Caldera is inactive in the Caldera app) + */ + private void clearAbilityForAgent(@NotNull final io.openbas.database.model.Agent existingAgent) { + if ((now().toEpochMilli() - existingAgent.getClearedAt().toEpochMilli()) > CLEAR_TTL) { try { - log.info("Clearing endpoint " + existingEndpoint.getHostname()); + log.info("Clearing agent caldera " + existingAgent.getExecutedByUser()); Iterable injectors = injectorService.injectors(); injectors.forEach( injector -> { if (injector.getExecutorClearCommands() != null) { - this.calderaExecutorContextService.launchExecutorClear(injector, existingEndpoint); + this.calderaExecutorContextService.launchExecutorClear(injector, existingAgent); } }); - existingEndpoint.setClearedAt(now()); + existingAgent.setClearedAt(now()); } catch (RuntimeException e) { log.info("Failed clear agents"); } } - this.endpointService.updateEndpoint(existingEndpoint); - } - - @VisibleForTesting - protected Instant toInstant(@NotNull final String lastSeen) { - String pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault()); - LocalDateTime localDateTime = LocalDateTime.parse(lastSeen, dateTimeFormatter); - ZonedDateTime zonedDateTime = localDateTime.atZone(UTC); - return zonedDateTime.toInstant(); } @VisibleForTesting diff --git a/openbas-api/src/main/java/io/openbas/executors/crowdstrike/CrowdStrikeExecutor.java b/openbas-api/src/main/java/io/openbas/executors/crowdstrike/CrowdStrikeExecutor.java index d337ed302e..ae2556d52c 100644 --- a/openbas-api/src/main/java/io/openbas/executors/crowdstrike/CrowdStrikeExecutor.java +++ b/openbas-api/src/main/java/io/openbas/executors/crowdstrike/CrowdStrikeExecutor.java @@ -1,11 +1,9 @@ package io.openbas.executors.crowdstrike; +import io.openbas.executors.ExecutorService; 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 io.openbas.service.EndpointService; import jakarta.annotation.PostConstruct; import java.time.Duration; @@ -21,20 +19,13 @@ public class CrowdStrikeExecutor { 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); + this.executorService, this.client, this.config, this.endpointService); if (this.config.isEnable()) { this.taskScheduler.scheduleAtFixedRate(service, Duration.ofSeconds(60)); } diff --git a/openbas-api/src/main/java/io/openbas/executors/crowdstrike/service/CrowdStrikeExecutorContextService.java b/openbas-api/src/main/java/io/openbas/executors/crowdstrike/service/CrowdStrikeExecutorContextService.java index ca747cfa0e..9078750a99 100644 --- a/openbas-api/src/main/java/io/openbas/executors/crowdstrike/service/CrowdStrikeExecutorContextService.java +++ b/openbas-api/src/main/java/io/openbas/executors/crowdstrike/service/CrowdStrikeExecutorContextService.java @@ -1,42 +1,37 @@ package io.openbas.executors.crowdstrike.service; import static io.openbas.executors.ExecutorHelper.replaceArgs; +import static io.openbas.executors.crowdstrike.service.CrowdStrikeExecutorService.CROWDSTRIKE_EXECUTOR_NAME; import io.openbas.database.model.*; +import io.openbas.executors.ExecutorContextService; import io.openbas.executors.crowdstrike.client.CrowdStrikeExecutorClient; import io.openbas.executors.crowdstrike.config.CrowdStrikeExecutorConfig; +import io.openbas.rest.exception.AgentException; import jakarta.validation.constraints.NotNull; import java.util.Base64; import java.util.Objects; +import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Log -@Service -public class CrowdStrikeExecutorContextService { - private CrowdStrikeExecutorConfig crowdStrikeExecutorConfig; +@Service(CROWDSTRIKE_EXECUTOR_NAME) +@RequiredArgsConstructor +public class CrowdStrikeExecutorContextService extends ExecutorContextService { - private CrowdStrikeExecutorClient crowdStrikeExecutorClient; - - @Autowired - public void setCrowdStrikeExecutorConfig(CrowdStrikeExecutorConfig crowdStrikeExecutorConfig) { - this.crowdStrikeExecutorConfig = crowdStrikeExecutorConfig; - } - - @Autowired - public void setCrowdStrikeExecutorClient(CrowdStrikeExecutorClient crowdStrikeExecutorClient) { - this.crowdStrikeExecutorClient = crowdStrikeExecutorClient; - } + private final CrowdStrikeExecutorConfig crowdStrikeExecutorConfig; + private final CrowdStrikeExecutorClient crowdStrikeExecutorClient; public void launchExecutorSubprocess( - @NotNull final Inject inject, @NotNull final Endpoint assetEndpoint) { - Injector injector = - inject - .getInjectorContract() - .map(InjectorContract::getInjector) - .orElseThrow( - () -> new UnsupportedOperationException("Inject does not have a contract")); + @NotNull final Inject inject, + @NotNull final Endpoint assetEndpoint, + @NotNull final Agent agent) + throws AgentException { + + if (!this.crowdStrikeExecutorConfig.isEnable()) { + throw new AgentException("Fatal error: CrowdStrike executor is not enabled", agent); + } Endpoint.PLATFORM_TYPE platform = Objects.equals(assetEndpoint.getType(), "Endpoint") ? assetEndpoint.getPlatform() : null; @@ -46,6 +41,13 @@ public void launchExecutorSubprocess( throw new RuntimeException("Unsupported platform: " + platform + " (arch:" + arch + ")"); } + Injector injector = + inject + .getInjectorContract() + .map(InjectorContract::getInjector) + .orElseThrow( + () -> new UnsupportedOperationException("Inject does not have a contract")); + String scriptName = switch (platform) { case Windows -> this.crowdStrikeExecutorConfig.getWindowsScriptName(); @@ -56,15 +58,11 @@ public void launchExecutorSubprocess( String executorCommandKey = platform.name() + "." + arch.name(); String command = injector.getExecutorCommands().get(executorCommandKey); - command = - replaceArgs( - platform, command, inject.getId(), assetEndpoint.getAgents().getFirst().getId()); + command = replaceArgs(platform, command, inject.getId(), agent.getId()); this.crowdStrikeExecutorClient.executeAction( - assetEndpoint.getAgents().getFirst().getExternalReference(), + agent.getExternalReference(), scriptName, Base64.getEncoder().encodeToString(command.getBytes())); } - - public void launchExecutorClear(@NotNull final Injector injector, @NotNull final Asset asset) {} } diff --git a/openbas-api/src/main/java/io/openbas/executors/crowdstrike/service/CrowdStrikeExecutorService.java b/openbas-api/src/main/java/io/openbas/executors/crowdstrike/service/CrowdStrikeExecutorService.java index 180816be64..ec82289563 100644 --- a/openbas-api/src/main/java/io/openbas/executors/crowdstrike/service/CrowdStrikeExecutorService.java +++ b/openbas-api/src/main/java/io/openbas/executors/crowdstrike/service/CrowdStrikeExecutorService.java @@ -1,29 +1,20 @@ package io.openbas.executors.crowdstrike.service; -import static java.time.Instant.now; -import static java.time.ZoneOffset.UTC; +import static io.openbas.utils.Time.toInstant; import io.openbas.database.model.Agent; import io.openbas.database.model.Endpoint; import io.openbas.database.model.Executor; -import io.openbas.database.model.Injector; +import io.openbas.executors.ExecutorService; import io.openbas.executors.crowdstrike.client.CrowdStrikeExecutorClient; import io.openbas.executors.crowdstrike.config.CrowdStrikeExecutorConfig; import io.openbas.executors.crowdstrike.model.CrowdStrikeDevice; -import io.openbas.integrations.ExecutorService; -import io.openbas.integrations.InjectorService; import io.openbas.service.EndpointService; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Optional; +import java.util.*; import java.util.logging.Level; +import java.util.stream.Collectors; import lombok.extern.java.Log; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -33,10 +24,9 @@ @Log @Service public class CrowdStrikeExecutorService implements Runnable { - private static final int CLEAR_TTL = 1800000; // 30 minutes - private static final int DELETE_TTL = 86400000; // 24 hours + private static final String CROWDSTRIKE_EXECUTOR_TYPE = "openbas_crowdstrike"; - private static final String CROWDSTRIKE_EXECUTOR_NAME = "CrowdStrike"; + public static final String CROWDSTRIKE_EXECUTOR_NAME = "CrowdStrike"; private static final String CROWDSTRIKE_EXECUTOR_DOCUMENTATION_LINK = "https://docs.openbas.io/latest/deployment/ecosystem/executors/#crowdstrike-falcon-agent"; @@ -44,10 +34,6 @@ public class CrowdStrikeExecutorService implements Runnable { private final EndpointService endpointService; - private final CrowdStrikeExecutorContextService crowdStrikeExecutorContextService; - - private final InjectorService injectorService; - private Executor executor = null; public static Endpoint.PLATFORM_TYPE toPlatform(@NotBlank final String platform) { @@ -72,13 +58,9 @@ public CrowdStrikeExecutorService( ExecutorService executorService, CrowdStrikeExecutorClient client, CrowdStrikeExecutorConfig config, - CrowdStrikeExecutorContextService crowdStrikeExecutorContextService, - EndpointService endpointService, - InjectorService injectorService) { + EndpointService endpointService) { this.client = client; this.endpointService = endpointService; - this.crowdStrikeExecutorContextService = crowdStrikeExecutorContextService; - this.injectorService = injectorService; try { if (config.isEnable()) { this.executor = @@ -105,123 +87,45 @@ public CrowdStrikeExecutorService( public void run() { log.info("Running CrowdStrike executor endpoints gathering..."); List devices = this.client.devices().getResources().stream().toList(); - List endpoints = - toEndpoint(devices).stream().filter(endpoint -> endpoint.getActive()).toList(); - log.info("CrowdStrike executor provisioning based on " + endpoints.size() + " assets"); - endpoints.forEach( - endpoint -> { - List existingEndpoints = - this.endpointService.findAssetsForInjectionByHostname(endpoint.getHostname()).stream() - .filter( - endpoint1 -> - Arrays.stream(endpoint1.getIps()) - .anyMatch(s -> Arrays.stream(endpoint.getIps()).toList().contains(s))) - .toList(); - if (existingEndpoints.isEmpty()) { - Optional endpointByExternalReference = - endpointService.findByExternalReference( - endpoint.getAgents().getFirst().getExternalReference()); - if (endpointByExternalReference.isPresent()) { - this.updateEndpoint(endpoint, List.of(endpointByExternalReference.get())); - } else { - this.endpointService.createEndpoint(endpoint); - } - } else { - this.updateEndpoint(endpoint, existingEndpoints); - } - }); - List inactiveEndpoints = - toEndpoint(devices).stream().filter(endpoint -> !endpoint.getActive()).toList(); - inactiveEndpoints.forEach( - endpoint -> { - Optional optionalExistingEndpoint = - this.endpointService.findByExternalReference( - endpoint.getAgents().getFirst().getExternalReference()); - if (optionalExistingEndpoint.isPresent()) { - Endpoint existingEndpoint = optionalExistingEndpoint.get(); - if ((now().toEpochMilli() - existingEndpoint.getClearedAt().toEpochMilli()) - > DELETE_TTL) { - log.info("Found stale endpoint " + existingEndpoint.getName() + ", deleting it..."); - this.endpointService.deleteEndpoint(existingEndpoint.getId()); - } - } - }); + List endpointAgentList = toAgentEndpoint(devices); + log.info("CrowdStrike executor provisioning based on " + endpointAgentList.size() + " assets"); + + for (Agent agent : endpointAgentList) { + endpointService.registerAgentEndpoint(agent, CROWDSTRIKE_EXECUTOR_TYPE); + } } // -- PRIVATE -- - private List toEndpoint(@NotNull final List devices) { + private List toAgentEndpoint(@NotNull final List devices) { return devices.stream() .map( - (crowdStrikeDevice) -> { + crowdStrikeDevice -> { Endpoint endpoint = new Endpoint(); Agent agent = new Agent(); + agent.setExecutor(this.executor); agent.setExternalReference(crowdStrikeDevice.getDevice_id()); agent.setPrivilege(Agent.PRIVILEGE.admin); agent.setDeploymentMode(Agent.DEPLOYMENT_MODE.service); + endpoint.setName(crowdStrikeDevice.getHostname()); endpoint.setDescription("Asset collected by CrowdStrike executor context."); endpoint.setIps(new String[] {crowdStrikeDevice.getConnection_ip()}); endpoint.setMacAddresses(new String[] {crowdStrikeDevice.getMac_address()}); endpoint.setHostname(crowdStrikeDevice.getHostname()); endpoint.setPlatform(toPlatform(crowdStrikeDevice.getPlatform_name())); + endpoint.setArch(toArch("x64")); + agent.setExecutedByUser( Endpoint.PLATFORM_TYPE.Windows.equals(endpoint.getPlatform()) ? Agent.ADMIN_SYSTEM_WINDOWS : Agent.ADMIN_SYSTEM_UNIX); - // Cannot find arch in CrowdStrike for the moment - endpoint.setArch(toArch("x64")); agent.setLastSeen(toInstant(crowdStrikeDevice.getLast_seen())); agent.setAsset(endpoint); - endpoint.setAgents(List.of(agent)); - return endpoint; - }) - .toList(); - } - - private void updateEndpoint( - @NotNull final Endpoint external, @NotNull final List existingList) { - Endpoint matchingExistingEndpoint = existingList.getFirst(); - matchingExistingEndpoint - .getAgents() - .getFirst() - .setLastSeen(external.getAgents().getFirst().getLastSeen()); - matchingExistingEndpoint.setName(external.getName()); - matchingExistingEndpoint.setIps(external.getIps()); - matchingExistingEndpoint.setHostname(external.getHostname()); - matchingExistingEndpoint - .getAgents() - .getFirst() - .setExternalReference(external.getAgents().getFirst().getExternalReference()); - matchingExistingEndpoint.setPlatform(external.getPlatform()); - matchingExistingEndpoint.setArch(external.getArch()); - matchingExistingEndpoint.getAgents().getFirst().setExecutor(this.executor); - if ((now().toEpochMilli() - matchingExistingEndpoint.getClearedAt().toEpochMilli()) - > CLEAR_TTL) { - try { - log.info("Clearing endpoint " + matchingExistingEndpoint.getHostname()); - Iterable injectors = injectorService.injectors(); - injectors.forEach( - injector -> { - if (injector.getExecutorClearCommands() != null) { - this.crowdStrikeExecutorContextService.launchExecutorClear( - injector, matchingExistingEndpoint); - } - }); - matchingExistingEndpoint.setClearedAt(now()); - } catch (RuntimeException e) { - log.info("Failed clear agents"); - } - } - this.endpointService.updateEndpoint(matchingExistingEndpoint); - } - private Instant toInstant(@NotNull final String lastSeen) { - String pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault()); - LocalDateTime localDateTime = LocalDateTime.parse(lastSeen, dateTimeFormatter); - ZonedDateTime zonedDateTime = localDateTime.atZone(UTC); - return zonedDateTime.toInstant(); + return agent; + }) + .collect(Collectors.toList()); } } diff --git a/openbas-api/src/main/java/io/openbas/executors/openbas/OpenBASExecutor.java b/openbas-api/src/main/java/io/openbas/executors/openbas/OpenBASExecutor.java index 424e94260c..8253d8d02a 100644 --- a/openbas-api/src/main/java/io/openbas/executors/openbas/OpenBASExecutor.java +++ b/openbas-api/src/main/java/io/openbas/executors/openbas/OpenBASExecutor.java @@ -1,7 +1,7 @@ package io.openbas.executors.openbas; import io.openbas.database.model.Endpoint; -import io.openbas.integrations.ExecutorService; +import io.openbas.executors.ExecutorService; import jakarta.annotation.PostConstruct; import java.util.logging.Level; import lombok.RequiredArgsConstructor; @@ -14,9 +14,9 @@ public class OpenBASExecutor { private final ExecutorService executorService; - public static String OPENBAS_EXECUTOR_ID = "2f9a0936-c327-4e95-b406-d161d32a2501"; - public static String OPENBAS_EXECUTOR_TYPE = "openbas_agent"; - public static String OPENBAS_EXECUTOR_NAME = "OpenBAS Agent"; + public static final String OPENBAS_EXECUTOR_ID = "2f9a0936-c327-4e95-b406-d161d32a2501"; + public static final String OPENBAS_EXECUTOR_TYPE = "openbas_agent"; + public static final String OPENBAS_EXECUTOR_NAME = "OpenBAS Agent"; @PostConstruct public void init() { diff --git a/openbas-api/src/main/java/io/openbas/executors/openbas/service/OpenBASExecutorContextService.java b/openbas-api/src/main/java/io/openbas/executors/openbas/service/OpenBASExecutorContextService.java index 51a5e856d7..744e61189c 100644 --- a/openbas-api/src/main/java/io/openbas/executors/openbas/service/OpenBASExecutorContextService.java +++ b/openbas-api/src/main/java/io/openbas/executors/openbas/service/OpenBASExecutorContextService.java @@ -1,25 +1,23 @@ package io.openbas.executors.openbas.service; import static io.openbas.executors.ExecutorHelper.replaceArgs; +import static io.openbas.executors.openbas.OpenBASExecutor.OPENBAS_EXECUTOR_NAME; import io.openbas.database.model.*; import io.openbas.database.repository.AssetAgentJobRepository; +import io.openbas.executors.ExecutorContextService; import jakarta.validation.constraints.NotNull; import java.util.Objects; +import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Log -@Service -public class OpenBASExecutorContextService { +@Service(OPENBAS_EXECUTOR_NAME) +@RequiredArgsConstructor +public class OpenBASExecutorContextService extends ExecutorContextService { - private AssetAgentJobRepository assetAgentJobRepository; - - @Autowired - public void setAssetAgentJobRepository(AssetAgentJobRepository assetAgentJobRepository) { - this.assetAgentJobRepository = assetAgentJobRepository; - } + private final AssetAgentJobRepository assetAgentJobRepository; private String computeCommand( @NotNull final Inject inject, @@ -44,7 +42,9 @@ private String computeCommand( } public void launchExecutorSubprocess( - @NotNull final Inject inject, @NotNull final Endpoint assetEndpoint) { + @NotNull final Inject inject, + @NotNull final Endpoint assetEndpoint, + @NotNull final Agent agent) { Endpoint.PLATFORM_TYPE platform = Objects.equals(assetEndpoint.getType(), "Endpoint") ? assetEndpoint.getPlatform() : null; Endpoint.PLATFORM_ARCH arch = @@ -53,14 +53,9 @@ public void launchExecutorSubprocess( throw new RuntimeException("Unsupported null platform"); } AssetAgentJob assetAgentJob = new AssetAgentJob(); - assetAgentJob.setCommand( - computeCommand(inject, assetEndpoint.getAgents().getFirst().getId(), platform, arch)); - assetAgentJob.setAgent(assetEndpoint.getAgents().getFirst()); + assetAgentJob.setCommand(computeCommand(inject, agent.getId(), platform, arch)); + assetAgentJob.setAgent(agent); assetAgentJob.setInject(inject); assetAgentJobRepository.save(assetAgentJob); } - - public void launchExecutorClear(@NotNull final Injector injector, @NotNull final Asset asset) { - // TODO - } } diff --git a/openbas-api/src/main/java/io/openbas/executors/tanium/TaniumExecutor.java b/openbas-api/src/main/java/io/openbas/executors/tanium/TaniumExecutor.java index da25b0ec73..889b94cb0e 100644 --- a/openbas-api/src/main/java/io/openbas/executors/tanium/TaniumExecutor.java +++ b/openbas-api/src/main/java/io/openbas/executors/tanium/TaniumExecutor.java @@ -1,11 +1,9 @@ package io.openbas.executors.tanium; +import io.openbas.executors.ExecutorService; import io.openbas.executors.tanium.client.TaniumExecutorClient; import io.openbas.executors.tanium.config.TaniumExecutorConfig; -import io.openbas.executors.tanium.service.TaniumExecutorContextService; import io.openbas.executors.tanium.service.TaniumExecutorService; -import io.openbas.integrations.ExecutorService; -import io.openbas.integrations.InjectorService; import io.openbas.service.EndpointService; import jakarta.annotation.PostConstruct; import java.time.Duration; @@ -21,20 +19,13 @@ public class TaniumExecutor { private final ThreadPoolTaskScheduler taskScheduler; private final TaniumExecutorClient client; private final EndpointService endpointService; - private final TaniumExecutorContextService taniumExecutorContextService; private final ExecutorService executorService; - private final InjectorService injectorService; @PostConstruct public void init() { TaniumExecutorService service = new TaniumExecutorService( - this.executorService, - this.client, - this.config, - this.taniumExecutorContextService, - this.endpointService, - this.injectorService); + this.executorService, this.client, this.config, this.endpointService); if (this.config.isEnable()) { this.taskScheduler.scheduleAtFixedRate(service, Duration.ofSeconds(60)); } diff --git a/openbas-api/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorContextService.java b/openbas-api/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorContextService.java index 3d5c8ae66b..c6994969dc 100644 --- a/openbas-api/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorContextService.java +++ b/openbas-api/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorContextService.java @@ -1,42 +1,37 @@ package io.openbas.executors.tanium.service; import static io.openbas.executors.ExecutorHelper.replaceArgs; +import static io.openbas.executors.tanium.service.TaniumExecutorService.TANIUM_EXECUTOR_NAME; import io.openbas.database.model.*; +import io.openbas.executors.ExecutorContextService; import io.openbas.executors.tanium.client.TaniumExecutorClient; import io.openbas.executors.tanium.config.TaniumExecutorConfig; +import io.openbas.rest.exception.AgentException; import jakarta.validation.constraints.NotNull; import java.util.Base64; import java.util.Objects; +import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Log -@Service -public class TaniumExecutorContextService { - private TaniumExecutorConfig taniumExecutorConfig; +@Service(TANIUM_EXECUTOR_NAME) +@RequiredArgsConstructor +public class TaniumExecutorContextService extends ExecutorContextService { - private TaniumExecutorClient taniumExecutorClient; - - @Autowired - public void setTaniumExecutorConfig(TaniumExecutorConfig taniumExecutorConfig) { - this.taniumExecutorConfig = taniumExecutorConfig; - } - - @Autowired - public void setTaniumExecutorClient(TaniumExecutorClient taniumExecutorClient) { - this.taniumExecutorClient = taniumExecutorClient; - } + private final TaniumExecutorConfig taniumExecutorConfig; + private final TaniumExecutorClient taniumExecutorClient; public void launchExecutorSubprocess( - @NotNull final Inject inject, @NotNull final Endpoint assetEndpoint) { - Injector injector = - inject - .getInjectorContract() - .map(InjectorContract::getInjector) - .orElseThrow( - () -> new UnsupportedOperationException("Inject does not have a contract")); + @NotNull final Inject inject, + @NotNull final Endpoint assetEndpoint, + @NotNull final Agent agent) + throws AgentException { + + if (!this.taniumExecutorConfig.isEnable()) { + throw new AgentException("Fatal error: Tanium executor is not enabled", agent); + } Endpoint.PLATFORM_TYPE platform = Objects.equals(assetEndpoint.getType(), "Endpoint") ? assetEndpoint.getPlatform() : null; @@ -46,6 +41,13 @@ public void launchExecutorSubprocess( throw new RuntimeException("Unsupported platform: " + platform + " (arch:" + arch + ")"); } + Injector injector = + inject + .getInjectorContract() + .map(InjectorContract::getInjector) + .orElseThrow( + () -> new UnsupportedOperationException("Inject does not have a contract")); + Integer packageId = switch (platform) { case Windows -> this.taniumExecutorConfig.getWindowsPackageId(); @@ -55,15 +57,11 @@ public void launchExecutorSubprocess( String executorCommandKey = platform.name() + "." + arch.name(); String command = injector.getExecutorCommands().get(executorCommandKey); - command = - replaceArgs( - platform, command, inject.getId(), assetEndpoint.getAgents().getFirst().getId()); + command = replaceArgs(platform, command, inject.getId(), agent.getId()); this.taniumExecutorClient.executeAction( - assetEndpoint.getAgents().getFirst().getExternalReference(), + agent.getExternalReference(), packageId, Base64.getEncoder().encodeToString(command.getBytes())); } - - public void launchExecutorClear(@NotNull final Injector injector, @NotNull final Asset asset) {} } 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 fce36b4c37..13a376fe35 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 @@ -1,24 +1,19 @@ package io.openbas.executors.tanium.service; -import static java.time.Instant.now; -import static java.time.ZoneOffset.UTC; +import static io.openbas.utils.Time.toInstant; import io.openbas.database.model.*; +import io.openbas.executors.ExecutorService; import io.openbas.executors.tanium.client.TaniumExecutorClient; import io.openbas.executors.tanium.config.TaniumExecutorConfig; import io.openbas.executors.tanium.model.NodeEndpoint; import io.openbas.executors.tanium.model.TaniumEndpoint; -import io.openbas.integrations.ExecutorService; -import io.openbas.integrations.InjectorService; import io.openbas.service.EndpointService; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; import java.util.logging.Level; +import java.util.stream.Collectors; import lombok.extern.java.Log; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -28,10 +23,9 @@ @Log @Service public class TaniumExecutorService implements Runnable { - private static final int CLEAR_TTL = 1800000; // 30 minutes - private static final int DELETE_TTL = 86400000; // 24 hours + private static final String TANIUM_EXECUTOR_TYPE = "openbas_tanium"; - private static final String TANIUM_EXECUTOR_NAME = "Tanium"; + public static final String TANIUM_EXECUTOR_NAME = "Tanium"; private static final String TANIUM_EXECUTOR_DOCUMENTATION_LINK = "https://docs.openbas.io/latest/deployment/ecosystem/executors/#tanium-agent"; @@ -39,10 +33,6 @@ public class TaniumExecutorService implements Runnable { private final EndpointService endpointService; - private final TaniumExecutorContextService taniumExecutorContextService; - - private final InjectorService injectorService; - private Executor executor = null; public static Endpoint.PLATFORM_TYPE toPlatform(@NotBlank final String platform) { @@ -67,13 +57,9 @@ public TaniumExecutorService( ExecutorService executorService, TaniumExecutorClient client, TaniumExecutorConfig config, - TaniumExecutorContextService taniumExecutorContextService, - EndpointService endpointService, - InjectorService injectorService) { + EndpointService endpointService) { this.client = client; this.endpointService = endpointService; - this.taniumExecutorContextService = taniumExecutorContextService; - this.injectorService = injectorService; try { if (config.isEnable()) { this.executor = @@ -101,55 +87,20 @@ public void run() { log.info("Running Tanium executor endpoints gathering..."); List nodeEndpoints = this.client.endpoints().getData().getEndpoints().getEdges().stream().toList(); - List endpoints = - toEndpoint(nodeEndpoints).stream().filter(endpoint -> endpoint.getActive()).toList(); - log.info("Tanium executor provisioning based on " + endpoints.size() + " assets"); - endpoints.forEach( - endpoint -> { - List existingEndpoints = - this.endpointService.findAssetsForInjectionByHostname(endpoint.getHostname()).stream() - .filter( - endpoint1 -> - Arrays.stream(endpoint1.getIps()) - .anyMatch(s -> Arrays.stream(endpoint.getIps()).toList().contains(s))) - .toList(); - if (existingEndpoints.isEmpty()) { - Optional endpointByExternalReference = - endpointService.findByExternalReference( - endpoint.getAgents().getFirst().getExternalReference()); - if (endpointByExternalReference.isPresent()) { - this.updateEndpoint(endpoint, List.of(endpointByExternalReference.get())); - } else { - this.endpointService.createEndpoint(endpoint); - } - } else { - this.updateEndpoint(endpoint, existingEndpoints); - } - }); - List inactiveEndpoints = - toEndpoint(nodeEndpoints).stream().filter(endpoint -> !endpoint.getActive()).toList(); - inactiveEndpoints.forEach( - endpoint -> { - Optional optionalExistingEndpoint = - this.endpointService.findByExternalReference( - endpoint.getAgents().getFirst().getExternalReference()); - if (optionalExistingEndpoint.isPresent()) { - Endpoint existingEndpoint = optionalExistingEndpoint.get(); - if ((now().toEpochMilli() - existingEndpoint.getClearedAt().toEpochMilli()) - > DELETE_TTL) { - log.info("Found stale endpoint " + existingEndpoint.getName() + ", deleting it..."); - this.endpointService.deleteEndpoint(existingEndpoint.getId()); - } - } - }); + List endpointAgentList = toAgentEndpoint(nodeEndpoints); + log.info("Tanium executor provisioning based on " + endpointAgentList.size() + " assets"); + + for (Agent agent : endpointAgentList) { + endpointService.registerAgentEndpoint(agent, TANIUM_EXECUTOR_TYPE); + } } // -- PRIVATE -- - private List toEndpoint(@NotNull final List nodeEndpoints) { + private List toAgentEndpoint(@NotNull final List nodeEndpoints) { return nodeEndpoints.stream() .map( - (nodeEndpoint) -> { + nodeEndpoint -> { TaniumEndpoint taniumEndpoint = nodeEndpoint.getNode(); Endpoint endpoint = new Endpoint(); Agent agent = new Agent(); @@ -170,54 +121,8 @@ private List toEndpoint(@NotNull final List nodeEndpoint endpoint.setArch(toArch(taniumEndpoint.getProcessor().getArchitecture())); agent.setLastSeen(toInstant(taniumEndpoint.getEidLastSeen())); agent.setAsset(endpoint); - endpoint.setAgents(List.of(agent)); - return endpoint; + return agent; }) - .toList(); - } - - private void updateEndpoint( - @NotNull final Endpoint external, @NotNull final List existingList) { - Endpoint matchingExistingEndpoint = existingList.getFirst(); - matchingExistingEndpoint - .getAgents() - .getFirst() - .setLastSeen(external.getAgents().getFirst().getLastSeen()); - matchingExistingEndpoint.setName(external.getName()); - matchingExistingEndpoint.setIps(external.getIps()); - matchingExistingEndpoint.setHostname(external.getHostname()); - matchingExistingEndpoint - .getAgents() - .getFirst() - .setExternalReference(external.getAgents().getFirst().getExternalReference()); - matchingExistingEndpoint.setPlatform(external.getPlatform()); - matchingExistingEndpoint.setArch(external.getArch()); - matchingExistingEndpoint.getAgents().getFirst().setExecutor(this.executor); - if ((now().toEpochMilli() - matchingExistingEndpoint.getClearedAt().toEpochMilli()) - > CLEAR_TTL) { - try { - log.info("Clearing endpoint " + matchingExistingEndpoint.getHostname()); - Iterable injectors = injectorService.injectors(); - injectors.forEach( - injector -> { - if (injector.getExecutorClearCommands() != null) { - this.taniumExecutorContextService.launchExecutorClear( - injector, matchingExistingEndpoint); - } - }); - matchingExistingEndpoint.setClearedAt(now()); - } catch (RuntimeException e) { - log.info("Failed clear agents"); - } - } - this.endpointService.updateEndpoint(matchingExistingEndpoint); - } - - private Instant toInstant(@NotNull final String lastSeen) { - String pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault()); - LocalDateTime localDateTime = LocalDateTime.parse(lastSeen, dateTimeFormatter); - ZonedDateTime zonedDateTime = localDateTime.atZone(UTC); - return zonedDateTime.toInstant(); + .collect(Collectors.toList()); } } diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaExecutor.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaExecutor.java index 355c04c54c..d83874dd14 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaExecutor.java +++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaExecutor.java @@ -7,6 +7,7 @@ import static io.openbas.model.expectation.DetectionExpectation.*; import static io.openbas.model.expectation.ManualExpectation.*; import static io.openbas.model.expectation.PreventionExpectation.*; +import static io.openbas.utils.AgentUtils.isValidAgent; import static java.time.Instant.now; import com.fasterxml.jackson.databind.JsonNode; @@ -27,9 +28,10 @@ import io.openbas.model.expectation.ManualExpectation; import io.openbas.model.expectation.PreventionExpectation; import io.openbas.rest.inject.service.InjectService; +import io.openbas.service.AgentService; import io.openbas.service.AssetGroupService; -import io.openbas.service.EndpointService; import io.openbas.service.InjectExpectationService; +import io.openbas.utils.ExpectationUtils; import io.openbas.utils.Time; import jakarta.validation.constraints.NotNull; import java.util.*; @@ -38,7 +40,6 @@ import java.util.stream.StreamSupport; import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; -import org.hibernate.Hibernate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -48,11 +49,11 @@ public class CalderaExecutor extends Injector { private static final String CALDERA_FAILED_TO_EXECUTE_THE_ABILITY_ON_AGENT = - "Caldera failed to execute the ability on agent"; + "Caldera failed to execute the ability on agent "; private static final int RETRY_NUMBER = 20; private final CalderaInjectorService calderaService; - private final EndpointService endpointService; + private final AgentService agentService; private final AssetGroupService assetGroupService; private final InjectExpectationService injectExpectationService; private final InjectService injectService; @@ -63,13 +64,16 @@ public ExecutionProcess process( @NotNull final Execution execution, @NotNull final ExecutableInject injection) throws Exception { CalderaInjectContent content = contentConvert(injection, CalderaInjectContent.class); + String obfuscator = content.getObfuscator() != null ? content.getObfuscator() : CalderaInjectContent.getDefaultObfuscator(); + Inject inject = this.injectService.inject(injection.getInjection().getInject().getId()); Map assets = this.injectService.resolveAllAssetsToExecute(inject); + // Execute inject for all assets if (assets.isEmpty()) { execution.addTrace( @@ -125,128 +129,138 @@ public ExecutionProcess process( } else { contract = injectorContract.getId(); } - assets.forEach( - (asset, aBoolean) -> { - if (!(asset instanceof Endpoint) || !((Endpoint) asset).getActive()) { - return; - } - try { - Endpoint executionEndpoint = - this.findAndRegisterAssetForExecution( - injection.getInjection().getInject(), asset); - if (executionEndpoint != null) { - if (Arrays.stream(injectorContract.getPlatforms()) - .anyMatch(s -> s.equals(executionEndpoint.getPlatform()))) { - String result = - this.calderaService.exploit( - obfuscator, - executionEndpoint.getAgents().getFirst().getExternalReference(), - contract, - additionalFields); - if (result.contains("complete")) { - execution.addTrace( - getNewInfoTrace( - "Request to execute the ability sent to Caldera", - ExecutionTraceAction.START, - ((Endpoint) asset).getAgents().getFirst(), - List.of())); - ExploitResult exploitResult = - this.calderaService.exploitResult( - executionEndpoint.getAgents().getFirst().getExternalReference(), - contract); - asyncIds.add(exploitResult.getLinkId()); - execution.addTrace( - getNewInfoTrace( - exploitResult.getCommand(), - ExecutionTraceAction.EXECUTION, - ((Endpoint) asset).getAgents().getFirst(), - List.of(exploitResult.getLinkId()))); - // Compute expectations - boolean isInGroup = - assets.get( - executionEndpoint - .getAgents() - .getFirst() - .getParent() - .getAsset()); - - computeExpectationsForAssetAndAgents( - expectations, - content, - executionEndpoint.getAgents().getFirst(), - isInGroup, - injectorContract.getPayload()); - - execution.addTrace( - getNewInfoTrace( - "Caldera executed the ability on agent" - + ((Endpoint) asset) - .getAgents() - .getFirst() - .getExecutedByUser() - + " using " - + executionEndpoint.getAgents().getFirst().getProcessName() - + " (paw: " - + executionEndpoint - .getAgents() - .getFirst() - .getExternalReference() - + ", linkID: " - + exploitResult.getLinkId() - + ")", - ExecutionTraceAction.EXECUTION, - ((Endpoint) asset).getAgents().getFirst(), - List.of(exploitResult.getLinkId()))); - } else { - execution.addTrace( - getNewErrorTrace( - CALDERA_FAILED_TO_EXECUTE_THE_ABILITY_ON_AGENT - + ((Endpoint) asset) - .getAgents() - .getFirst() - .getExecutedByUser() - + " (" - + result - + ")", - ExecutionTraceAction.COMPLETE, - ((Endpoint) asset).getAgents().getFirst())); - } - } else { - execution.addTrace( - getNewErrorTrace( - "Caldera failed to execute ability on agent " - + ((Endpoint) asset) - .getAgents() - .getFirst() - .getExecutedByUser() - + "(platform is not compatible:" - + executionEndpoint.getPlatform().name() - + ")", - ExecutionTraceAction.COMPLETE, - ((Endpoint) asset).getAgents().getFirst())); + + Map> executedAgentByEndpoint = + new HashMap<>(); + + // Loop for every asset in this inject + assets + .entrySet() + .forEach( + entry -> { + Asset asset = entry.getKey(); + boolean isInGroup = entry.getValue(); + + if (!(asset instanceof Endpoint)) { + return; } - } else { - execution.addTrace( - getNewErrorTrace( - CALDERA_FAILED_TO_EXECUTE_THE_ABILITY_ON_AGENT - + asset.getName() - + " (temporary injector not spawned correctly)", - ExecutionTraceAction.COMPLETE, - ((Endpoint) asset).getAgents().getFirst())); - } - } catch (Exception e) { - execution.addTrace( - getNewErrorTrace( - CALDERA_FAILED_TO_EXECUTE_THE_ABILITY_ON_AGENT - + ((Endpoint) asset).getAgents().getFirst().getExecutedByUser() - + " (" - + e.getMessage() - + ")", - ExecutionTraceAction.COMPLETE, - ((Endpoint) asset).getAgents().getFirst())); - log.severe(Arrays.toString(e.getStackTrace())); - } - }); + + // We execute just one time the inject in every agent + if (!executedAgentByEndpoint.containsKey(asset.getId())) { + Endpoint endpointAgent = (Endpoint) asset; + + List executedAgents = new ArrayList<>(); + + // Loop for every validated agent in this endpoint + endpointAgent.getAgents().stream() + .filter(agent -> isValidAgent(inject, agent)) + .forEach( + agent -> { + try { + io.openbas.database.model.Agent executionAgent = + this.findAndRegisterAgentForExecution( + injection.getInjection().getInject(), + endpointAgent, + agent); + if (executionAgent != null) { + if (Arrays.stream(injectorContract.getPlatforms()) + .anyMatch(s -> s.equals(endpointAgent.getPlatform()))) { + String result = + this.calderaService.exploit( + obfuscator, + executionAgent.getExternalReference(), + contract, + additionalFields); + if (result.contains("complete")) { + execution.addTrace( + getNewInfoTrace( + "Request to execute the ability sent to Caldera", + ExecutionTraceAction.START, + agent, + List.of())); + ExploitResult exploitResult = + this.calderaService.exploitResult( + executionAgent.getExternalReference(), + contract); + asyncIds.add(exploitResult.getLinkId()); + executedAgents.add(executionAgent); + execution.addTrace( + getNewInfoTrace( + exploitResult.getCommand(), + ExecutionTraceAction.EXECUTION, + agent, + List.of(exploitResult.getLinkId()))); + execution.addTrace( + getNewInfoTrace( + "Caldera executed the ability on agent" + + executionAgent.getExecutedByUser() + + " using " + + executionAgent.getProcessName() + + " (paw: " + + executionAgent.getExternalReference() + + ", linkID: " + + exploitResult.getLinkId() + + ")", + ExecutionTraceAction.EXECUTION, + agent, + List.of(exploitResult.getLinkId()))); + } else { + execution.addTrace( + getNewErrorTrace( + CALDERA_FAILED_TO_EXECUTE_THE_ABILITY_ON_AGENT + + agent.getExecutedByUser() + + " (" + + result + + ")", + ExecutionTraceAction.COMPLETE, + agent)); + } + } else { + execution.addTrace( + getNewErrorTrace( + CALDERA_FAILED_TO_EXECUTE_THE_ABILITY_ON_AGENT + + agent.getExecutedByUser() + + "(platform is not compatible:" + + endpointAgent.getPlatform().name() + + ")", + ExecutionTraceAction.COMPLETE, + agent)); + } + } else { + execution.addTrace( + getNewErrorTrace( + CALDERA_FAILED_TO_EXECUTE_THE_ABILITY_ON_AGENT + + agent.getExecutedByUser() + + " (temporary injector not spawned correctly)", + ExecutionTraceAction.COMPLETE, + agent)); + } + } catch (Exception e) { + execution.addTrace( + getNewErrorTrace( + CALDERA_FAILED_TO_EXECUTE_THE_ABILITY_ON_AGENT + + agent.getExecutedByUser() + + " (" + + e.getMessage() + + ")", + ExecutionTraceAction.COMPLETE, + agent)); + log.severe(Arrays.toString(e.getStackTrace())); + } + }); + + executedAgentByEndpoint.put(asset.getId(), executedAgents); + } + + // Creation of Expectations + computeExpectationsForAssetAndAgents( + expectations, + content, + asset, + isInGroup, + executedAgentByEndpoint.get(asset.getId()), + injectorContract.getPayload()); + }); }, () -> execution.addTrace( @@ -300,163 +314,161 @@ public StatusPayload getPayloadOutput(String externalId) { // -- PRIVATE -- - private Endpoint findAndRegisterAssetForExecution( - @NotNull final Inject inject, @NotNull final Asset asset) throws InterruptedException { - Endpoint endpointForExecution = null; - if (!asset.getType().equals("Endpoint")) { + private io.openbas.database.model.Agent findAndRegisterAgentForExecution( + @NotNull final Inject inject, + @NotNull final Endpoint assetEndpoint, + @NotNull final io.openbas.database.model.Agent agent) + throws InterruptedException { + io.openbas.database.model.Agent agentForExecution = null; + if (!assetEndpoint.getType().equals("Endpoint")) { log.log( Level.SEVERE, "Caldera failed to execute ability on the asset because type is not supported: " - + asset.getType()); + + assetEndpoint.getType()); return null; } - log.log(Level.INFO, "Trying to find an available executor for " + asset.getName()); - Endpoint assetEndpoint = (Endpoint) Hibernate.unproxy(asset); + log.log(Level.INFO, "Trying to find an available executor for " + assetEndpoint.getName()); for (int i = 0; i < RETRY_NUMBER; i++) { - // Find an executor agent matching the asset - log.log(Level.INFO, "Listing agents..."); - List agents = + // Find an executor agent matching the assetEndpoint + log.log(Level.INFO, "Listing agentsCaldera..."); + List agentsCaldera = this.calderaService.agents().stream() .filter( - agent -> - agent.getExe_name().contains("implant") + agentCaldera -> + agentCaldera.getExe_name().contains("implant") && (now().toEpochMilli() - - Time.toInstant(agent.getCreated()).toEpochMilli()) + - Time.toInstant(agentCaldera.getCreated()).toEpochMilli()) < io.openbas.database.model.Agent.ACTIVE_THRESHOLD - && (agent.getHost().equals(assetEndpoint.getHostname()) - || agent + && (agentCaldera.getHost().equalsIgnoreCase(assetEndpoint.getHostname()) + || agentCaldera .getHost() .split("\\.")[0] - .equals(assetEndpoint.getHostname().split("\\.")[0])) + .equalsIgnoreCase(assetEndpoint.getHostname().split("\\.")[0])) && Arrays.stream(assetEndpoint.getIps()) .anyMatch( s -> - Arrays.stream(agent.getHost_ip_addrs()).toList().contains(s))) + Arrays.stream(agentCaldera.getHost_ip_addrs()) + .toList() + .contains(s))) .toList(); - log.log(Level.INFO, "List return with " + agents.size() + " agents"); - if (!agents.isEmpty()) { - for (Agent agent : agents) { + log.log(Level.INFO, "List return with " + agentsCaldera.size() + " agents"); + + if (!agentsCaldera.isEmpty()) { + for (Agent agentCaldera : agentsCaldera) { // Check in the database if not exist - Optional resolvedExistingEndpoint = - this.endpointService.findByExternalReference(agent.getPaw()); - if (resolvedExistingEndpoint.isEmpty()) { + Optional resolvedExistingAgent = + this.agentService.findByExternalReference(agentCaldera.getPaw()); + + if (resolvedExistingAgent.isEmpty()) { log.log(Level.INFO, "Agent found and not present in the database, creating it..."); - Endpoint newEndpoint = new Endpoint(); io.openbas.database.model.Agent newAgent = new io.openbas.database.model.Agent(); newAgent.setInject(inject); - newAgent.setParent(assetEndpoint.getAgents().getFirst()); - newEndpoint.setName(assetEndpoint.getName()); - newEndpoint.setIps(assetEndpoint.getIps()); - newEndpoint.setHostname(assetEndpoint.getHostname()); - newEndpoint.setPlatform(assetEndpoint.getPlatform()); - newEndpoint.setArch(assetEndpoint.getArch()); - newAgent.setProcessName(agent.getExe_name()); - newAgent.setExecutor(assetEndpoint.getExecutor()); - newAgent.setExternalReference(agent.getPaw()); + newAgent.setParent(agent); + newAgent.setProcessName(agentCaldera.getExe_name()); + newAgent.setExecutor(agent.getExecutor()); + newAgent.setExternalReference(agentCaldera.getPaw()); newAgent.setPrivilege(io.openbas.database.model.Agent.PRIVILEGE.admin); newAgent.setDeploymentMode(io.openbas.database.model.Agent.DEPLOYMENT_MODE.session); - newAgent.setExecutedByUser(agent.getUsername()); - newAgent.setAsset(newEndpoint); - newEndpoint.setAgents(List.of(newAgent)); - endpointForExecution = this.endpointService.createEndpoint(newEndpoint); + newAgent.setExecutedByUser(agent.getExecutedByUser()); + newAgent.setAsset(assetEndpoint); + agentForExecution = this.agentService.createOrUpdateAgent(newAgent); break; } } } - if (endpointForExecution != null) { + if (agentForExecution != null) { break; } Thread.sleep(5000); } - return endpointForExecution; + return agentForExecution; } /** In case of direct asset, we have an individual expectation for the asset */ private void computeExpectationsForAssetAndAgents( - @NotNull final List expectations, + final List expectations, @NotNull final CalderaInjectContent content, - @NotNull final io.openbas.database.model.Agent executionAgent, + @NotNull final Asset asset, final boolean expectationGroup, + final List executedAgents, final Payload payload) { + if (!content.getExpectations().isEmpty()) { expectations.addAll( content.getExpectations().stream() .flatMap( - (expectation) -> { - final io.openbas.database.model.Agent agentParent = executionAgent.getParent(); - - return switch (expectation.getType()) { - case PREVENTION -> { - PreventionExpectation preventionExpectation = - preventionExpectationForAsset( - expectation.getScore(), - expectation.getName(), - expectation.getDescription(), - agentParent.getAsset(), - expectationGroup, // expectationGroup usefully in front-end - expectation.getExpirationTime()); - - // We propagate the asset expectation to agents - PreventionExpectation preventionExpectationAgent = - preventionExpectationForAgent( - expectation.getScore(), - expectation.getName(), - expectation.getDescription(), - agentParent, - agentParent.getAsset(), - expectation.getExpirationTime(), - computeSignatures(payload, executionAgent.getProcessName())); - - yield Stream.of(preventionExpectation, preventionExpectationAgent); - } - case DETECTION -> { - DetectionExpectation detectionExpectation = - detectionExpectationForAsset( - expectation.getScore(), - expectation.getName(), - expectation.getDescription(), - agentParent.getAsset(), - expectationGroup, - expectation.getExpirationTime()); - - // We propagate the asset expectation to agents - DetectionExpectation detectionExpectationAgent = - detectionExpectationForAgent( - expectation.getScore(), - expectation.getName(), - expectation.getDescription(), - agentParent, - agentParent.getAsset(), - expectation.getExpirationTime(), - computeSignatures(payload, executionAgent.getProcessName())); - - yield Stream.of(detectionExpectation, detectionExpectationAgent); - } - case MANUAL -> { - ManualExpectation manualExpectation = - manualExpectationForAsset( - expectation.getScore(), - expectation.getName(), - expectation.getDescription(), - agentParent.getAsset(), - expectation.getExpirationTime(), - expectationGroup); - - // We propagate the asset expectation to agents - ManualExpectation manualExpectationAgent = - manualExpectationForAgent( - expectation.getScore(), - expectation.getName(), - expectation.getDescription(), - agentParent, - agentParent.getAsset(), - expectation.getExpirationTime()); - - yield Stream.of(manualExpectation, manualExpectationAgent); - } - default -> Stream.of(); - }; - }) + (expectation) -> + switch (expectation.getType()) { + case PREVENTION -> { + PreventionExpectation preventionExpectation = + preventionExpectationForAsset( + expectation.getScore(), + expectation.getName(), + expectation.getDescription(), + asset, + expectationGroup, + expectation.getExpirationTime()); + + // We propagate the asset expectation to agents + List preventionExpectationList = + ExpectationUtils.getPreventionExpectationList( + asset, executedAgents, payload, preventionExpectation); + + // If any expectation for agent is created then we create also expectation + // for asset + if (!preventionExpectationList.isEmpty()) { + yield Stream.concat( + Stream.of(preventionExpectation), + preventionExpectationList.stream()); + } + yield Stream.empty(); + } + case DETECTION -> { + DetectionExpectation detectionExpectation = + detectionExpectationForAsset( + expectation.getScore(), + expectation.getName(), + expectation.getDescription(), + asset, + expectationGroup, + expectation.getExpirationTime()); + // We propagate the asset expectation to agents + List detectionExpectationList = + ExpectationUtils.getDetectionExpectationList( + asset, executedAgents, payload, detectionExpectation); + + // If any expectation for agent is created then we create also expectation + // for asset + if (!detectionExpectationList.isEmpty()) { + yield Stream.concat( + Stream.of(detectionExpectation), detectionExpectationList.stream()); + } + yield Stream.empty(); + } + case MANUAL -> { + ManualExpectation manualExpectation = + manualExpectationForAsset( + expectation.getScore(), + expectation.getName(), + expectation.getDescription(), + asset, + expectation.getExpirationTime(), + expectationGroup); + // We propagate the asset expectation to agents + List manualExpectationList = + ExpectationUtils.getManualExpectationList( + asset, executedAgents, manualExpectation); + + // If any expectation for agent is created then we create also expectation + // for asset + if (!manualExpectationList.isEmpty()) { + yield Stream.concat( + Stream.of(manualExpectation), manualExpectationList.stream()); + } + yield Stream.empty(); + } + default -> Stream.of(); + }) .toList()); } } @@ -561,56 +573,4 @@ private void computeExpectationsForAssetGroup( .toList()); } } - - private List computeSignatures(Payload payload, String processName) { - List injectExpectationSignatures = new ArrayList<>(); - if (payload != null) { - switch (payload.getTypeEnum()) { - case PayloadType.COMMAND: - injectExpectationSignatures.add( - InjectExpectationSignature.builder() - .type(EXPECTATION_SIGNATURE_TYPE_PROCESS_NAME) - .value(processName) - .build()); - break; - case PayloadType.EXECUTABLE: - Executable payloadExecutable = (Executable) Hibernate.unproxy(payload); - injectExpectationSignatures.add( - InjectExpectationSignature.builder() - .type(EXPECTATION_SIGNATURE_TYPE_FILE_NAME) - .value(payloadExecutable.getExecutableFile().getName()) - .build()); - // TODO File hash - break; - case PayloadType.FILE_DROP: - FileDrop payloadFileDrop = (FileDrop) Hibernate.unproxy(payload); - injectExpectationSignatures.add( - InjectExpectationSignature.builder() - .type(EXPECTATION_SIGNATURE_TYPE_FILE_NAME) - .value(payloadFileDrop.getFileDropFile().getName()) - .build()); - // TODO File hash - break; - case PayloadType.DNS_RESOLUTION: - DnsResolution payloadDnsResolution = (DnsResolution) Hibernate.unproxy(payload); - injectExpectationSignatures.add( - InjectExpectationSignature.builder() - .type(EXPECTATION_SIGNATURE_TYPE_HOSTNAME) - .value(payloadDnsResolution.getHostname().split("\\r?\\n")[0]) - .build()); - break; - default: - throw new UnsupportedOperationException( - "Payload type " + payload.getType() + " is not supported"); - } - } else { - injectExpectationSignatures.add( - InjectExpectationSignature.builder() - .type(EXPECTATION_SIGNATURE_TYPE_PROCESS_NAME) - .value(processName) - .build()); - } - - return injectExpectationSignatures; - } } diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaGarbageCollector.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaGarbageCollector.java index 8ee1648309..8f42dd94ca 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaGarbageCollector.java +++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaGarbageCollector.java @@ -3,7 +3,7 @@ import io.openbas.injectors.caldera.client.CalderaInjectorClient; import io.openbas.injectors.caldera.config.CalderaInjectorConfig; import io.openbas.injectors.caldera.service.CalderaGarbageCollectorService; -import io.openbas.service.EndpointService; +import io.openbas.service.AgentService; import jakarta.annotation.PostConstruct; import java.time.Duration; import lombok.RequiredArgsConstructor; @@ -19,14 +19,14 @@ public class CalderaGarbageCollector { private final CalderaInjectorConfig config; private final ThreadPoolTaskScheduler taskScheduler; private final CalderaInjectorClient client; - private final EndpointService endpointService; + private final AgentService agentService; @PostConstruct public void init() { // If enabled, scheduled every X seconds if (this.config.isEnable()) { CalderaGarbageCollectorService service = - new CalderaGarbageCollectorService(this.client, this.endpointService); + new CalderaGarbageCollectorService(this.client, this.agentService); this.taskScheduler.scheduleAtFixedRate(service, Duration.ofSeconds(120)); } } diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/client/CalderaInjectorClient.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/client/CalderaInjectorClient.java index 3f909d2bea..e436b244e9 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/caldera/client/CalderaInjectorClient.java +++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/client/CalderaInjectorClient.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import io.openbas.database.model.Endpoint; import io.openbas.injectors.caldera.client.model.Ability; import io.openbas.injectors.caldera.client.model.Agent; import io.openbas.injectors.caldera.client.model.Result; @@ -105,23 +104,6 @@ public Agent agent(@NotBlank final String paw, final String include) { } } - public void killAgent(Endpoint endpoint) { - try { - Map body = new HashMap<>(); - body.put("watchdog", 1); - body.put("sleep_min", 3); - body.put("sleep_max", 3); - this.patch( - this.config.getRestApiV2Url() - + AGENT_URI - + "/" - + endpoint.getAgents().getFirst().getExternalReference(), - body); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - public void killAgent(Agent agent) { try { Map body = new HashMap<>(); @@ -134,18 +116,6 @@ public void killAgent(Agent agent) { } } - public void deleteAgent(Endpoint endpoint) { - try { - this.delete( - this.config.getRestApiV2Url() - + AGENT_URI - + "/" - + endpoint.getAgents().getFirst().getExternalReference()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - public void deleteAgent(Agent agent) { try { this.delete(this.config.getRestApiV2Url() + AGENT_URI + "/" + agent.getPaw()); diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/service/CalderaGarbageCollectorService.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/service/CalderaGarbageCollectorService.java index 9323538e96..78a5f85d30 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/caldera/service/CalderaGarbageCollectorService.java +++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/service/CalderaGarbageCollectorService.java @@ -2,13 +2,10 @@ import static java.time.Instant.now; -import io.openbas.database.model.Endpoint; -import io.openbas.database.specification.EndpointSpecification; import io.openbas.injectors.caldera.client.CalderaInjectorClient; import io.openbas.injectors.caldera.client.model.Agent; -import io.openbas.service.EndpointService; +import io.openbas.service.AgentService; import io.openbas.utils.Time; -import jakarta.validation.constraints.NotBlank; import java.util.ArrayList; import java.util.List; import lombok.extern.java.Log; @@ -22,66 +19,53 @@ public class CalderaGarbageCollectorService implements Runnable { private final int DELETE_TTL = 1200000; // 20 min private final CalderaInjectorClient client; - private final EndpointService endpointService; - - public static Endpoint.PLATFORM_TYPE toPlatform(@NotBlank final String platform) { - return switch (platform) { - case "linux" -> Endpoint.PLATFORM_TYPE.Linux; - case "windows" -> Endpoint.PLATFORM_TYPE.Windows; - case "darwin" -> Endpoint.PLATFORM_TYPE.MacOS; - default -> throw new IllegalArgumentException("This platform is not supported : " + platform); - }; - } + private final AgentService agentService; @Autowired - public CalderaGarbageCollectorService( - CalderaInjectorClient client, EndpointService endpointService) { + public CalderaGarbageCollectorService(CalderaInjectorClient client, AgentService agentService) { this.client = client; - this.endpointService = endpointService; + this.agentService = agentService; } @Override public void run() { log.info("Running Caldera injector garbage collector..."); - List endpoints = - this.endpointService.endpoints(EndpointSpecification.findEndpointsForExecution()); - log.info("Running Caldera injector garbage collector on " + endpoints.size() + " endpoints"); - endpoints.forEach( - endpoint -> { - if ((now().toEpochMilli() - endpoint.getCreatedAt().toEpochMilli()) > DELETE_TTL) { - this.endpointService.deleteEndpoint(endpoint.getId()); - } - }); - List agents = this.client.agents(); + List agents = this.agentService.getAgentsForExecution(); log.info("Running Caldera injector garbage collector on " + agents.size() + " agents"); - List killedAgents = new ArrayList<>(); agents.forEach( agent -> { - if (agent.getExe_name().contains("implant") - && (now().toEpochMilli() - Time.toInstant(agent.getCreated()).toEpochMilli()) - > KILL_TTL - && (now().toEpochMilli() - Time.toInstant(agent.getLast_seen()).toEpochMilli()) - < KILL_TTL) { - try { - log.info("Killing agent " + agent.getHost()); - client.killAgent(agent); - killedAgents.add(agent.getPaw()); - } catch (RuntimeException e) { - log.info("Failed to kill agent, probably already killed"); - } + if ((now().toEpochMilli() - agent.getCreatedAt().toEpochMilli()) > DELETE_TTL) { + this.agentService.deleteAgent(agent.getId()); } }); - agents.forEach( - agent -> { - if (agent.getExe_name().contains("implant") - && (now().toEpochMilli() - Time.toInstant(agent.getCreated()).toEpochMilli()) - > DELETE_TTL - && !killedAgents.contains(agent.getPaw())) { - try { - log.info("Deleting agent " + agent.getHost()); - client.deleteAgent(agent); - } catch (RuntimeException e) { - log.severe("Failed to delete agent"); + List agentsCaldera = this.client.agents(); + log.info("Running Caldera injector garbage collector on " + agentsCaldera.size() + " agents"); + List killedAgents = new ArrayList<>(); + agentsCaldera.forEach( + agentCaldera -> { + if (agentCaldera.getExe_name().contains("implant")) { + if ((now().toEpochMilli() - Time.toInstant(agentCaldera.getCreated()).toEpochMilli()) + > KILL_TTL + && (now().toEpochMilli() + - Time.toInstant(agentCaldera.getLast_seen()).toEpochMilli()) + < KILL_TTL) { + try { + log.info("Killing agent " + agentCaldera.getHost()); + client.killAgent(agentCaldera); + killedAgents.add(agentCaldera.getPaw()); + } catch (RuntimeException e) { + log.info("Failed to kill agent, probably already killed"); + } + } + if ((now().toEpochMilli() - Time.toInstant(agentCaldera.getCreated()).toEpochMilli()) + > DELETE_TTL + && !killedAgents.contains(agentCaldera.getPaw())) { + try { + log.info("Deleting agent " + agentCaldera.getHost()); + client.deleteAgent(agentCaldera); + } catch (RuntimeException e) { + log.severe("Failed to delete agent"); + } } } }); diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/service/CalderaResultCollectorService.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/service/CalderaResultCollectorService.java index 97b7cbd09e..4c08d2c1cc 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/caldera/service/CalderaResultCollectorService.java +++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/service/CalderaResultCollectorService.java @@ -130,7 +130,7 @@ public void run() { } Inject relatedInject = injectStatus.getInject(); - if (injectStatusService.isAllInjectAssetsExecuted(relatedInject)) { + if (injectStatusService.isAllInjectAgentsExecuted(relatedInject)) { injectStatusService.updateFinalInjectStatus(injectStatus); } diff --git a/openbas-api/src/main/java/io/openbas/injectors/openbas/OpenBASImplantExecutor.java b/openbas-api/src/main/java/io/openbas/injectors/openbas/OpenBASImplantExecutor.java index c66fc3caca..b560a38850 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/openbas/OpenBASImplantExecutor.java +++ b/openbas-api/src/main/java/io/openbas/injectors/openbas/OpenBASImplantExecutor.java @@ -1,10 +1,10 @@ package io.openbas.injectors.openbas; import static io.openbas.database.model.ExecutionTraces.getNewErrorTrace; -import static io.openbas.database.model.InjectExpectationSignature.*; import static io.openbas.model.expectation.DetectionExpectation.*; import static io.openbas.model.expectation.ManualExpectation.*; import static io.openbas.model.expectation.PreventionExpectation.*; +import static io.openbas.utils.ExpectationUtils.*; import io.openbas.database.model.*; import io.openbas.execution.ExecutableInject; @@ -171,79 +171,6 @@ private void computeExpectationsForAssetAndAgents( } } - private List getManualExpectationList( - Asset asset, Inject inject, ManualExpectation manualExpectation) { - return getActiveAgents(asset, inject).stream() - .map( - agent -> - manualExpectationForAgent( - manualExpectation.getScore(), - manualExpectation.getName(), - manualExpectation.getDescription(), - agent, - asset, - manualExpectation.getExpirationTime())) - .toList(); - } - - private List getDetectionExpectationList( - Asset asset, Inject inject, String payloadType, DetectionExpectation detectionExpectation) { - return getActiveAgents(asset, inject).stream() - .map( - agent -> - detectionExpectationForAgent( - detectionExpectation.getScore(), - detectionExpectation.getName(), - detectionExpectation.getDescription(), - agent, - asset, - detectionExpectation.getExpirationTime(), - computeSignatures(inject.getId(), agent.getId(), payloadType))) - .toList(); - } - - private List getPreventionExpectationList( - Asset asset, Inject inject, String payloadType, PreventionExpectation preventionExpectation) { - return getActiveAgents(asset, inject).stream() - .map( - agent -> - preventionExpectationForAgent( - preventionExpectation.getScore(), - preventionExpectation.getName(), - preventionExpectation.getDescription(), - agent, - asset, - preventionExpectation.getExpirationTime(), - computeSignatures(inject.getId(), agent.getId(), payloadType))) - .toList(); - } - - private List getActiveAgents(Asset asset, Inject inject) { - return ((Endpoint) asset) - .getAgents().stream() - .filter(agent -> agent.getParent() == null && agent.getInject() == null) - .filter(agent -> hasOnlyValidTraces(inject, agent)) - .filter(Agent::isActive) - .toList(); - } - - private static boolean hasOnlyValidTraces(Inject inject, Agent agent) { - return inject - .getStatus() - .map(InjectStatus::getTraces) - .map( - traces -> - traces.stream() - .noneMatch( - trace -> - trace.getAgent() != null - && trace.getAgent().getId().equals(agent.getId()) - && (ExecutionTraceStatus.ERROR.equals(trace.getStatus()) - || ExecutionTraceStatus.ASSET_INACTIVE.equals( - trace.getStatus())))) - .orElse(true); // If there are no traces, return true by default - } - /** * In case of asset group if expectation group -> we have an expectation for the group and one for * each asset if not expectation group -> we have an individual expectation for each asset @@ -357,29 +284,4 @@ private void computeExpectationsForAssetGroup( .toList()); } } - - private static List computeSignatures( - String injectId, String agentId, String payloadType) { - List signatures = new ArrayList<>(); - List knownPayloadTypes = - Arrays.asList("Command", "Executable", "FileDrop", "DnsResolution"); - - /* - * Always add the "Parent process" signature type for the OpenBAS Implant Executor - */ - signatures.add( - createSignature( - EXPECTATION_SIGNATURE_TYPE_PARENT_PROCESS_NAME, - "obas-implant-" + injectId + "-agent-" + agentId)); - - if (!knownPayloadTypes.contains(payloadType)) { - throw new UnsupportedOperationException("Payload type " + payloadType + " is not supported"); - } - return signatures; - } - - private static InjectExpectationSignature createSignature( - String signatureType, String signatureValue) { - return builder().type(signatureType).value(signatureValue).build(); - } } diff --git a/openbas-api/src/main/java/io/openbas/migration/V3_66__Migrate_agents_to_same_endpoint.java b/openbas-api/src/main/java/io/openbas/migration/V3_66__Migrate_agents_to_same_endpoint.java new file mode 100644 index 0000000000..35c2160c82 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/migration/V3_66__Migrate_agents_to_same_endpoint.java @@ -0,0 +1,50 @@ +package io.openbas.migration; + +import java.sql.Statement; +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; +import org.springframework.stereotype.Component; + +@Component +public class V3_66__Migrate_agents_to_same_endpoint extends BaseJavaMigration { + @Override + public void migrate(Context context) throws Exception { + Statement select = context.getConnection().createStatement(); + select.execute( + """ + -- update hostnames to have lowercase for every executors + UPDATE assets SET endpoint_hostname = lower(endpoint_hostname) WHERE asset_type='Endpoint'; + -- update agent table to add cleared at column from asset table + ALTER TABLE agents ADD column agent_cleared_at timestamp default now(); + UPDATE agents ag set agent_cleared_at = a.asset_cleared_at FROM (SELECT asset_id, asset_cleared_at FROM assets) AS a WHERE ag.agent_asset=a.asset_id; + ALTER TABLE assets DROP column asset_cleared_at; + -- create temp asset table to group the identical endpoints + CREATE TABLE temp_assets + AS SELECT count(asset_id), + cast(array_agg(asset_id) AS VARCHAR) AS array_asset_id, + min(asset_id) AS uniq_asset_id, + endpoint_hostname, + endpoint_platform, + endpoint_arch, + string_agg(asset_description, '; ') AS agg_description + FROM assets WHERE asset_type='Endpoint' + GROUP BY endpoint_hostname, endpoint_platform, endpoint_arch HAVING count(asset_id) > 1; + -- update the assets table with the aggregated description for the corresponding unique endpoint + UPDATE assets asset + SET asset_description = ta.agg_description + FROM temp_assets ta WHERE asset.asset_id = ta.uniq_asset_id; + -- update the agents to match with an identical endpoint if it is possible + UPDATE agents a SET agent_asset=ta.uniq_asset_id + FROM (SELECT array_asset_id, uniq_asset_id FROM temp_assets) AS ta + WHERE ta.array_asset_id LIKE concat('%',a.agent_asset,'%'); + -- update the relation assets_tags + UPDATE assets_tags assets_tag SET asset_id=ta.uniq_asset_id + FROM (SELECT array_asset_id, uniq_asset_id FROM temp_assets) AS ta + WHERE ta.array_asset_id LIKE concat('%',assets_tag.asset_id,'%'); + -- drop temp asset table + DROP TABLE temp_assets; + -- delete old endpoints which are unused now + DELETE FROM assets WHERE asset_type='Endpoint' AND asset_id NOT IN (SELECT DISTINCT agent_asset FROM agents); + """); + } +} diff --git a/openbas-api/src/main/java/io/openbas/rest/asset/endpoint/form/EndpointOverviewOutput.java b/openbas-api/src/main/java/io/openbas/rest/asset/endpoint/form/EndpointOverviewOutput.java index df5562ce41..dc6592c28e 100644 --- a/openbas-api/src/main/java/io/openbas/rest/asset/endpoint/form/EndpointOverviewOutput.java +++ b/openbas-api/src/main/java/io/openbas/rest/asset/endpoint/form/EndpointOverviewOutput.java @@ -53,7 +53,7 @@ public class EndpointOverviewOutput { @JsonProperty("endpoint_mac_addresses") private Set macAddresses; - @Schema(description = "List of agents") + @Schema(description = "List of primary agents") @JsonProperty("asset_agents") @NotNull private Set agents; diff --git a/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java b/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java index 9d7d1b9e07..cb837bfddb 100644 --- a/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java @@ -316,19 +316,24 @@ public Page searchDocuments( @GetMapping("/api/documents/{documentId}") public Document document(@PathVariable String documentId) { - return resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + return resolveDocument(documentId) + .orElseThrow(() -> new ElementNotFoundException("Document not found")); } @GetMapping("/api/documents/{documentId}/tags") public Set documentTags(@PathVariable String documentId) { - Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + Document document = + resolveDocument(documentId) + .orElseThrow(() -> new ElementNotFoundException("Document not found")); return document.getTags(); } @PutMapping("/api/documents/{documentId}/tags") public Document documentTags( @PathVariable String documentId, @RequestBody DocumentTagUpdateInput input) { - Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + Document document = + resolveDocument(documentId) + .orElseThrow(() -> new ElementNotFoundException("Document not found")); document.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); return documentRepository.save(document); } @@ -337,7 +342,9 @@ public Document documentTags( @PutMapping("/api/documents/{documentId}") public Document updateDocumentInformation( @PathVariable String documentId, @Valid @RequestBody DocumentUpdateInput input) { - Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + Document document = + resolveDocument(documentId) + .orElseThrow(() -> new ElementNotFoundException("Document not found")); document.setUpdateAttributes(input); document.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); @@ -392,13 +399,17 @@ public Document updateDocumentInformation( @GetMapping("/api/documents/{documentId}/file") public void downloadDocument(@PathVariable String documentId, HttpServletResponse response) throws IOException { - Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + Document document = + resolveDocument(documentId) + .orElseThrow(() -> new ElementNotFoundException("Document not found")); response.addHeader( HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + document.getName()); response.addHeader(HttpHeaders.CONTENT_TYPE, document.getType()); response.setStatus(HttpServletResponse.SC_OK); try (InputStream fileStream = - fileService.getFile(document).orElseThrow(ElementNotFoundException::new)) { + fileService + .getFile(document) + .orElseThrow(() -> new ElementNotFoundException("File not found"))) { fileStream.transferTo(response.getOutputStream()); } } @@ -419,7 +430,9 @@ public void downloadDocument(@PathVariable String documentId, HttpServletRespons public @ResponseBody ResponseEntity getInjectorImageFromId( @PathVariable String injectorId) throws IOException { Injector injector = - this.injectorRepository.findById(injectorId).orElseThrow(ElementNotFoundException::new); + this.injectorRepository + .findById(injectorId) + .orElseThrow(() -> new ElementNotFoundException("Injector not found")); Optional fileStream = fileService.getInjectorImage(injector.getType()); if (fileStream.isPresent()) { return ResponseEntity.ok() @@ -450,7 +463,9 @@ public void downloadCollectorImage( response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE); response.setStatus(HttpServletResponse.SC_OK); try (InputStream fileStream = - fileService.getCollectorImage(collectorType).orElseThrow(ElementNotFoundException::new)) { + fileService + .getCollectorImage(collectorType) + .orElseThrow(() -> new ElementNotFoundException("File not found"))) { fileStream.transferTo(response.getOutputStream()); } } @@ -461,7 +476,9 @@ public void downloadCollectorImage( public @ResponseBody ResponseEntity getCollectorImageFromId( @PathVariable String collectorId) throws IOException { Collector collector = - this.collectorRepository.findById(collectorId).orElseThrow(ElementNotFoundException::new); + this.collectorRepository + .findById(collectorId) + .orElseThrow(() -> new ElementNotFoundException("Collector not found")); Optional fileStream = fileService.getCollectorImage(collector.getType()); if (fileStream.isPresent()) { return ResponseEntity.ok() @@ -478,7 +495,7 @@ public void getSecurityPlatformImageFromId( SecurityPlatform securityPlatform = this.securityPlatformRepository .findById(assetId) - .orElseThrow(ElementNotFoundException::new); + .orElseThrow(() -> new ElementNotFoundException("Security platform not found")); if (theme.equals("dark") && securityPlatform.getLogoDark() != null) { downloadDocument(securityPlatform.getLogoDark().getId(), response); } else if (securityPlatform.getLogoLight() != null) { @@ -616,7 +633,7 @@ public void downloadPlayerDocument( getExercisePlayerDocuments(exerciseOpt.get()).stream() .filter(doc -> doc.getId().equals(documentId)) .findFirst() - .orElseThrow(ElementNotFoundException::new); + .orElseThrow(() -> new ElementNotFoundException("Document not found")); } else if (scenarioOpt.isPresent()) { if (!scenarioOpt.get().isUserHasAccess(user) && !scenarioOpt.get().getUsers().contains(user)) { @@ -626,7 +643,7 @@ public void downloadPlayerDocument( getScenarioPlayerDocuments(scenarioOpt.get()).stream() .filter(doc -> doc.getId().equals(documentId)) .findFirst() - .orElseThrow(ElementNotFoundException::new); + .orElseThrow(() -> new ElementNotFoundException("Document not found")); } if (document != null) { @@ -635,7 +652,9 @@ public void downloadPlayerDocument( response.addHeader(HttpHeaders.CONTENT_TYPE, document.getType()); response.setStatus(HttpServletResponse.SC_OK); try (InputStream fileStream = - fileService.getFile(document).orElseThrow(ElementNotFoundException::new)) { + fileService + .getFile(document) + .orElseThrow(() -> new ElementNotFoundException("File not found"))) { fileStream.transferTo(response.getOutputStream()); } } diff --git a/openbas-api/src/main/java/io/openbas/rest/exception/AgentException.java b/openbas-api/src/main/java/io/openbas/rest/exception/AgentException.java index 9e6fe2df7d..99b2234e2d 100644 --- a/openbas-api/src/main/java/io/openbas/rest/exception/AgentException.java +++ b/openbas-api/src/main/java/io/openbas/rest/exception/AgentException.java @@ -4,7 +4,7 @@ import lombok.Getter; @Getter -public class AgentException extends RuntimeException { +public class AgentException extends Exception { private final Agent agent; public AgentException(String message, Agent agent) { diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java b/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java index c0a3016b2b..9b2cef3f49 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java @@ -2,6 +2,7 @@ import static io.openbas.helper.StreamHelper.fromIterable; import static io.openbas.helper.StreamHelper.iterableToSet; +import static io.openbas.utils.AgentUtils.isPrimaryAgent; import static io.openbas.utils.FilterUtilsJpa.computeFilterGroupJpa; import static io.openbas.utils.StringUtils.duplicateString; import static io.openbas.utils.pagination.SearchUtilsJpa.computeSearchJpa; @@ -45,6 +46,7 @@ import java.util.stream.StreamSupport; import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; +import org.hibernate.Hibernate; import org.jetbrains.annotations.NotNull; import org.springframework.data.jpa.domain.Specification; import org.springframework.security.access.AccessDeniedException; @@ -575,4 +577,24 @@ private void updateInjectEntities( } }); } + + public final List getAgentsByInject(Inject inject) { + List agents = new ArrayList<>(); + Set agentIds = new HashSet<>(); + + resolveAllAssetsToExecute(inject).keySet().stream() + .map(asset -> (Endpoint) Hibernate.unproxy(asset)) + .flatMap( + endpoint -> Optional.ofNullable(endpoint.getAgents()).stream().flatMap(List::stream)) + .filter(agent -> isPrimaryAgent(agent)) + .forEach( + agent -> { + if (!agentIds.contains(agent.getId())) { + agents.add(agent); + agentIds.add(agent.getId()); + } + }); + + return agents; + } } diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectStatusService.java b/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectStatusService.java index ad55d96518..19aecae10a 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectStatusService.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectStatusService.java @@ -23,9 +23,9 @@ public class InjectStatusService { private final InjectRepository injectRepository; private final AgentRepository agentRepository; + private final InjectService injectService; private final InjectUtils injectUtils; private final InjectStatusRepository injectStatusRepository; - private final InjectService injectService; public List findPendingInjectStatusByType(String injectType) { return this.injectStatusRepository.pendingForInjectType(injectType); @@ -86,14 +86,17 @@ private ExecutionTraceAction convertExecutionAction(InjectExecutionAction status private int getCompleteTrace(Inject inject) { return inject.getStatus().map(InjectStatus::getTraces).orElse(Collections.emptyList()).stream() .filter(trace -> ExecutionTraceAction.COMPLETE.equals(trace.getAction())) + .filter(trace -> trace.getAgent() != null) + .map(trace -> trace.getAgent().getId()) + .distinct() .toList() .size(); } - public boolean isAllInjectAssetsExecuted(Inject inject) { + public boolean isAllInjectAgentsExecuted(Inject inject) { int totalCompleteTrace = getCompleteTrace(inject); - Map assets = this.injectService.resolveAllAssetsToExecute(inject); - return assets.size() == totalCompleteTrace; + List agents = this.injectService.getAgentsByInject(inject); + return agents.size() == totalCompleteTrace; } public void updateFinalInjectStatus(InjectStatus injectStatus) { @@ -123,6 +126,7 @@ private void computeExecutionTraceStatusIfNeeded( convertExecutionStatus( computeStatus( injectStatus.getTraces().stream() + .filter(t -> t.getAgent() != null) .filter(t -> t.getAgent().getId().equals(agentId)) .toList())); executionTraces.setStatus(traceStatus); @@ -143,7 +147,7 @@ public Inject handleInjectExecutionCallback( injectStatus.addTrace(executionTraces); if (executionTraces.getAction().equals(ExecutionTraceAction.COMPLETE) - && (agentId == null || isAllInjectAssetsExecuted(inject))) { + && (agentId == null || isAllInjectAgentsExecuted(inject))) { updateFinalInjectStatus(injectStatus); } return injectRepository.save(inject); @@ -157,7 +161,7 @@ public ExecutionStatus computeStatus(List traces) { switch (trace.getStatus()) { case SUCCESS, WARNING -> successCount++; case PARTIAL -> partialCount++; - case ERROR, COMMAND_NOT_FOUND, ASSET_INACTIVE -> errorCount++; + case ERROR, COMMAND_NOT_FOUND, AGENT_INACTIVE -> errorCount++; case MAYBE_PREVENTED, MAYBE_PARTIAL_PREVENTED, COMMAND_CANNOT_BE_EXECUTED -> maybePreventedCount++; } diff --git a/openbas-api/src/main/java/io/openbas/rest/injector/InjectorApi.java b/openbas-api/src/main/java/io/openbas/rest/injector/InjectorApi.java index 73780a4a61..735de3dbc8 100644 --- a/openbas-api/src/main/java/io/openbas/rest/injector/InjectorApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/injector/InjectorApi.java @@ -324,10 +324,32 @@ public InjectorRegistration registerInjector( produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public @ResponseBody byte[] getCalderaImplant( @PathVariable String platform, @PathVariable String arch) throws IOException { - InputStream in = - getClass() - .getResourceAsStream( - "/implants/caldera/" + platform + "/" + arch + "/obas-implant-caldera-" + platform); + return getCalderaFile(platform, arch, null); + } + + @GetMapping( + value = "/api/implant/caldera/{platform}/{arch}/{extension}", + produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public @ResponseBody byte[] getCalderaScript( + @PathVariable String platform, @PathVariable String arch, @PathVariable String extension) + throws IOException { + return getCalderaFile(platform, arch, extension); + } + + private byte[] getCalderaFile(String platform, String arch, String extension) throws IOException { + if (!AVAILABLE_PLATFORMS.contains(platform)) { + throw new IllegalArgumentException("Platform invalid : " + platform); + } + if (!AVAILABLE_ARCHITECTURES.contains(arch)) { + throw new IllegalArgumentException("Architecture invalid : " + arch); + } + + String resource = + "/implants/caldera/" + platform + "/" + arch + "/obas-implant-caldera-" + platform; + if (extension != null) { + resource += "." + extension; + } + InputStream in = getClass().getResourceAsStream(resource); if (in != null) { return IOUtils.toByteArray(in); } diff --git a/openbas-api/src/main/java/io/openbas/service/AgentService.java b/openbas-api/src/main/java/io/openbas/service/AgentService.java index 9fe44d7692..ae69dea616 100644 --- a/openbas-api/src/main/java/io/openbas/service/AgentService.java +++ b/openbas-api/src/main/java/io/openbas/service/AgentService.java @@ -1,10 +1,11 @@ package io.openbas.service; -import io.openbas.database.model.Agent; +import io.openbas.database.model.*; import io.openbas.database.repository.AgentRepository; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -14,11 +15,29 @@ public class AgentService { private final AgentRepository agentRepository; - public Map> getAgentsGroupedByAsset(List assetIds) { - List agents = agentRepository.findAgentsByAssetIds(assetIds); + public Optional getAgentForAnAsset( + String assetId, + String user, + Agent.DEPLOYMENT_MODE deploymentMode, + Agent.PRIVILEGE privilege, + String executor) { + return agentRepository.findByAssetExecutorUserDeploymentAndPrivilege( + assetId, user, deploymentMode.name(), privilege.name(), executor); + } + + public List getAgentsForExecution() { + return agentRepository.findForExecution(); + } + + public Agent createOrUpdateAgent(@NotNull final Agent agent) { + return this.agentRepository.save(agent); + } + + public void deleteAgent(@NotBlank final String agentId) { + this.agentRepository.deleteByAgentId(agentId); + } - return agents.stream() - .filter(Agent::isActive) - .collect(Collectors.groupingBy(agent -> agent.getAsset().getId())); + public Optional findByExternalReference(String externalReference) { + return agentRepository.findByExternalReference(externalReference); } } diff --git a/openbas-api/src/main/java/io/openbas/service/EndpointService.java b/openbas-api/src/main/java/io/openbas/service/EndpointService.java index a5a8df22b2..e835c83034 100644 --- a/openbas-api/src/main/java/io/openbas/service/EndpointService.java +++ b/openbas-api/src/main/java/io/openbas/service/EndpointService.java @@ -1,6 +1,7 @@ package io.openbas.service; import static io.openbas.executors.openbas.OpenBASExecutor.OPENBAS_EXECUTOR_ID; +import static io.openbas.executors.openbas.OpenBASExecutor.OPENBAS_EXECUTOR_TYPE; import static io.openbas.helper.StreamHelper.fromIterable; import static io.openbas.helper.StreamHelper.iterableToSet; import static io.openbas.utils.ArchitectureFilterUtils.handleEndpointFilter; @@ -32,7 +33,9 @@ import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; import org.apache.commons.io.IOUtils; +import org.hibernate.Hibernate; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -42,8 +45,11 @@ @RequiredArgsConstructor @Service +@Log public class EndpointService { + public static final int DELETE_TTL = 86400000; // 24 hours + public static String JFROG_BASE = "https://filigran.jfrog.io/artifactory"; @Resource private OpenBASConfig openBASConfig; @@ -64,6 +70,7 @@ public class EndpointService { private final ExecutorRepository executorRepository; private final AssetAgentJobRepository assetAgentJobRepository; private final TagRepository tagRepository; + private final AgentService agentService; // -- CRUD -- public Endpoint createEndpoint(@NotNull final Endpoint endpoint) { @@ -72,18 +79,17 @@ public Endpoint createEndpoint(@NotNull final Endpoint endpoint) { public Endpoint endpoint(@NotBlank final String endpointId) { return this.endpointRepository - .findByEndpointIdWithFirstLevelOfAgents(endpointId) + .findById(endpointId) .orElseThrow(() -> new ElementNotFoundException("Endpoint not found")); } @Transactional(readOnly = true) - public List findAssetsForInjectionByHostname(@NotBlank final String hostname) { - return endpoints(EndpointSpecification.findEndpointsForInjectionByHostname(hostname)); - } - - @Transactional(readOnly = true) - public Optional findByExternalReference(@NotBlank final String externalReference) { - return this.endpointRepository.findByExternalReference(externalReference); + public Optional findEndpointByAgentDetails( + @NotBlank final String hostname, + @NotNull final Endpoint.PLATFORM_TYPE platform, + @NotNull final Endpoint.PLATFORM_ARCH arch) { + return this.endpointRepository.findByHostnameArchAndPlatform( + hostname.toLowerCase(), platform.name(), arch.name()); } public List endpoints() { @@ -126,63 +132,155 @@ public Endpoint updateEndpoint( // -- INSTALLATION AGENT -- + public void registerAgentEndpoint(Agent agent, String executorType) { + Endpoint endpoint = (Endpoint) Hibernate.unproxy(agent.getAsset()); + Optional optionalEndpoint = + this.findEndpointByAgentDetails( + endpoint.getHostname(), endpoint.getPlatform(), endpoint.getArch()); + if (agent.isActive()) { + // Endpoint already created -> attributes to update + handleActiveAgent(agent, executorType, optionalEndpoint, endpoint); + } else { + handleInactiveAgent(agent, executorType, optionalEndpoint, endpoint); + } + } + + private void handleInactiveAgent( + Agent agent, String executorType, Optional optionalEndpoint, Endpoint endpoint) { + if (optionalEndpoint.isPresent()) { + Optional optionalAgent = + this.agentService.getAgentForAnAsset( + optionalEndpoint.get().getId(), + agent.getExecutedByUser(), + agent.getDeploymentMode(), + agent.getPrivilege(), + executorType); + if (optionalAgent.isPresent()) { + Agent existingAgent = optionalAgent.get(); + if ((now().toEpochMilli() - existingAgent.getLastSeen().toEpochMilli()) > DELETE_TTL) { + log.info( + "Found stale endpoint " + + endpoint.getName() + + ", deleting the agent " + + existingAgent.getExecutedByUser() + + " in it..."); + this.agentService.deleteAgent(existingAgent.getId()); + } + } + } + } + + private void handleActiveAgent( + Agent agent, String executorType, Optional optionalEndpoint, Endpoint endpoint) { + if (optionalEndpoint.isPresent()) { + + Endpoint endpointToUpdate = optionalEndpoint.get(); + endpointToUpdate.setIps(endpoint.getIps()); + endpointToUpdate.setMacAddresses(endpoint.getMacAddresses()); + this.updateEndpoint(endpointToUpdate); + + Optional optionalAgent = + this.agentService.getAgentForAnAsset( + endpointToUpdate.getId(), + agent.getExecutedByUser(), + agent.getDeploymentMode(), + agent.getPrivilege(), + executorType); + + // Agent already created -> attributes to update + if (optionalAgent.isPresent()) { + updateExistingAgent(agent, optionalAgent, endpointToUpdate); + } else { + // New agent to create for the endpoint + createNewAgent(agent, endpointToUpdate); + } + } else { + // New endpoint and new agent to create + createNewEndpointAndAgent(agent, endpoint); + } + } + + public void createNewEndpointAndAgent(Agent agent, Endpoint endpoint) { + this.createEndpoint(endpoint); + this.agentService.createOrUpdateAgent(agent); + } + + public void createNewAgent(Agent agent, Endpoint endpointToUpdate) { + agent.setAsset(endpointToUpdate); + this.agentService.createOrUpdateAgent(agent); + } + + private void updateExistingAgent( + Agent agent, Optional optionalAgent, Endpoint endpointToUpdate) { + Agent agentToUpdate = optionalAgent.get(); + agentToUpdate.setAsset(endpointToUpdate); + agentToUpdate.setLastSeen(agent.getLastSeen()); + agentToUpdate.setExternalReference(agent.getExternalReference()); + this.agentService.createOrUpdateAgent(agentToUpdate); + } + public Endpoint register(final EndpointRegisterInput input) throws IOException { - Optional optionalEndpoint = findByExternalReference(input.getExternalReference()); + Optional optionalEndpoint = + findEndpointByAgentDetails(input.getHostname(), input.getPlatform(), input.getArch()); Endpoint endpoint; + Agent agent; + // Endpoint already created -> attributes to update if (optionalEndpoint.isPresent()) { endpoint = optionalEndpoint.get(); endpoint.setIps(input.getIps()); endpoint.setMacAddresses(input.getMacAddresses()); - endpoint.setHostname(input.getHostname()); - endpoint.setPlatform(input.getPlatform()); - endpoint.setArch(input.getArch()); - endpoint.setName(input.getName()); - endpoint.getAgents().getFirst().setVersion(input.getAgentVersion()); - endpoint.setDescription(input.getDescription()); - endpoint.getAgents().getFirst().setLastSeen(Instant.now()); - endpoint - .getAgents() - .getFirst() - .setExecutor(executorRepository.findById(OPENBAS_EXECUTOR_ID).orElse(null)); - endpoint - .getAgents() - .getFirst() - .setPrivilege(input.isElevated() ? Agent.PRIVILEGE.admin : Agent.PRIVILEGE.standard); - endpoint - .getAgents() - .getFirst() - .setDeploymentMode( - input.isService() ? Agent.DEPLOYMENT_MODE.service : Agent.DEPLOYMENT_MODE.session); - endpoint.getAgents().getFirst().setExecutedByUser(input.getExecutedByUser()); + Agent.PRIVILEGE privilege = + input.isElevated() ? Agent.PRIVILEGE.admin : Agent.PRIVILEGE.standard; + Agent.DEPLOYMENT_MODE deploymentMode = + input.isService() ? Agent.DEPLOYMENT_MODE.service : Agent.DEPLOYMENT_MODE.session; + Optional optionalAgent = + agentService.getAgentForAnAsset( + endpoint.getId(), + input.getExecutedByUser(), + deploymentMode, + privilege, + OPENBAS_EXECUTOR_TYPE); + // Agent already created -> attributes to update + if (optionalAgent.isPresent()) { + agent = optionalAgent.get(); + } else { + // New agent to create for the endpoint + agent = new Agent(); + setAgentAttributes(input, agent); + } } else { + // New endpoint and new agent to create endpoint = new Endpoint(); - Agent agent = new Agent(); - agent.setVersion(input.getAgentVersion()); - agent.setExternalReference(input.getExternalReference()); + agent = new Agent(); endpoint.setUpdateAttributes(input); - agent.setLastSeen(Instant.now()); - agent.setPrivilege(input.isElevated() ? Agent.PRIVILEGE.admin : Agent.PRIVILEGE.standard); - agent.setDeploymentMode( - input.isService() ? Agent.DEPLOYMENT_MODE.service : Agent.DEPLOYMENT_MODE.session); - agent.setExecutedByUser(input.getExecutedByUser()); endpoint.setTags(iterableToSet(this.tagRepository.findAllById(input.getTagIds()))); - agent.setExecutor(executorRepository.findById(OPENBAS_EXECUTOR_ID).orElse(null)); - agent.setAsset(endpoint); - endpoint.setAgents(List.of(agent)); + setAgentAttributes(input, agent); } + agent.setVersion(input.getAgentVersion()); + agent.setLastSeen(Instant.now()); + agent.setAsset(endpoint); Endpoint updatedEndpoint = updateEndpoint(endpoint); + agentService.createOrUpdateAgent(agent); // If agent is not temporary and not the same version as the platform => Create an upgrade task // for the agent - if (updatedEndpoint.getAgents().getFirst().getParent() == null - && !updatedEndpoint.getAgents().getFirst().getVersion().equals(version)) { + if (agent.getParent() == null && !agent.getVersion().equals(version)) { AssetAgentJob assetAgentJob = new AssetAgentJob(); assetAgentJob.setCommand(generateUpgradeCommand(updatedEndpoint.getPlatform().name())); - assetAgentJob.setAgent(updatedEndpoint.getAgents().getFirst()); + assetAgentJob.setAgent(agent); assetAgentJobRepository.save(assetAgentJob); } return updatedEndpoint; } + private void setAgentAttributes(EndpointRegisterInput input, Agent agent) { + agent.setExternalReference(input.getExternalReference()); + agent.setPrivilege(input.isElevated() ? Agent.PRIVILEGE.admin : Agent.PRIVILEGE.standard); + agent.setDeploymentMode( + input.isService() ? Agent.DEPLOYMENT_MODE.service : Agent.DEPLOYMENT_MODE.session); + agent.setExecutedByUser(input.getExecutedByUser()); + agent.setExecutor(executorRepository.findById(OPENBAS_EXECUTOR_ID).orElse(null)); + } + public String getFileOrDownloadFromJfrog(String platform, String file, String adminToken) throws IOException { String extension = diff --git a/openbas-api/src/main/java/io/openbas/utils/AgentUtils.java b/openbas-api/src/main/java/io/openbas/utils/AgentUtils.java index 454e106a7f..1fb5e08619 100644 --- a/openbas-api/src/main/java/io/openbas/utils/AgentUtils.java +++ b/openbas-api/src/main/java/io/openbas/utils/AgentUtils.java @@ -1,6 +1,6 @@ package io.openbas.utils; -import io.openbas.database.model.Endpoint; +import io.openbas.database.model.*; import java.util.List; public class AgentUtils { @@ -17,4 +17,36 @@ private AgentUtils() {} List.of( Endpoint.PLATFORM_ARCH.x86_64.name().toLowerCase(), Endpoint.PLATFORM_ARCH.arm64.name().toLowerCase()); + + public static List getActiveAgents(Asset asset, Inject inject) { + return ((Endpoint) asset) + .getAgents().stream().filter(agent -> isValidAgent(inject, agent)).toList(); + } + + public static boolean isValidAgent(Inject inject, Agent agent) { + return isPrimaryAgent(agent) && hasOnlyValidTraces(inject, agent) && agent.isActive(); + } + + public static boolean hasOnlyValidTraces(Inject inject, Agent agent) { + return inject + .getStatus() + .map(InjectStatus::getTraces) + .map( + traces -> + Boolean.valueOf( + traces.stream() + .noneMatch( + trace -> + trace.getAgent() != null + && trace.getAgent().getId().equals(agent.getId()) + && (ExecutionTraceStatus.ERROR.equals(trace.getStatus()) + || ExecutionTraceStatus.AGENT_INACTIVE.equals( + trace.getStatus()))))) + .orElse(Boolean.TRUE) + .booleanValue(); // If there are no traces, return true by default + } + + public static boolean isPrimaryAgent(Agent agent) { + return agent.getParent() == null && agent.getInject() == null; + } } diff --git a/openbas-api/src/main/java/io/openbas/utils/EndpointMapper.java b/openbas-api/src/main/java/io/openbas/utils/EndpointMapper.java index 26603a2f49..025c530a92 100644 --- a/openbas-api/src/main/java/io/openbas/utils/EndpointMapper.java +++ b/openbas-api/src/main/java/io/openbas/utils/EndpointMapper.java @@ -1,5 +1,6 @@ package io.openbas.utils; +import static io.openbas.utils.AgentUtils.isPrimaryAgent; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; @@ -13,6 +14,7 @@ import java.util.*; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; @Component @@ -24,7 +26,7 @@ public EndpointOutput toEndpointOutput(Endpoint endpoint) { .id(endpoint.getId()) .name(endpoint.getName()) .type(endpoint.getType()) - .agents(toAgentOutputs(endpoint.getAgents())) + .agents(toAgentOutputs(getPrimaryAgents(endpoint))) .platform(endpoint.getPlatform()) .arch(endpoint.getArch()) .tags(endpoint.getTags().stream().map(Tag::getId).collect(Collectors.toSet())) @@ -47,7 +49,7 @@ public EndpointOverviewOutput toEndpointOverviewOutput(Endpoint endpoint) { endpoint.getMacAddresses() != null ? new HashSet<>(Arrays.asList(endpoint.getMacAddresses())) : emptySet()) - .agents(toAgentOutputs(endpoint.getAgents())) + .agents(toAgentOutputs(getPrimaryAgents(endpoint))) .tags(endpoint.getTags().stream().map(Tag::getId).collect(Collectors.toSet())) .build(); } @@ -78,4 +80,11 @@ private AgentOutput toAgentOutput(Agent agent) { } return builder.build(); } + + @NotNull + private static List getPrimaryAgents(Endpoint endpoint) { + return endpoint.getAgents().stream() + .filter(agent -> isPrimaryAgent(agent)) + .collect(Collectors.toList()); + } } diff --git a/openbas-api/src/main/java/io/openbas/utils/ExpectationUtils.java b/openbas-api/src/main/java/io/openbas/utils/ExpectationUtils.java index 35d2057b7c..5452d14604 100644 --- a/openbas-api/src/main/java/io/openbas/utils/ExpectationUtils.java +++ b/openbas-api/src/main/java/io/openbas/utils/ExpectationUtils.java @@ -1,14 +1,19 @@ package io.openbas.utils; -import io.openbas.database.model.InjectExpectation; -import io.openbas.database.model.InjectExpectationResult; -import io.openbas.database.model.Team; +import static io.openbas.database.model.InjectExpectationSignature.*; +import static io.openbas.model.expectation.DetectionExpectation.detectionExpectationForAgent; +import static io.openbas.model.expectation.ManualExpectation.manualExpectationForAgent; +import static io.openbas.model.expectation.PreventionExpectation.preventionExpectationForAgent; +import static io.openbas.utils.AgentUtils.getActiveAgents; + +import io.openbas.database.model.*; +import io.openbas.model.expectation.DetectionExpectation; +import io.openbas.model.expectation.ManualExpectation; +import io.openbas.model.expectation.PreventionExpectation; import io.openbas.rest.exception.ElementNotFoundException; import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.OptionalDouble; +import java.util.*; +import org.hibernate.Hibernate; public class ExpectationUtils { @@ -96,4 +101,187 @@ public static List processByValidationType( return updatedExpectations; } + + // -- CALDERA EXPECTATIONS -- + + public static List getPreventionExpectationList( + Asset asset, + List executedAgents, + Payload payload, + PreventionExpectation preventionExpectation) { + return executedAgents.stream() + .map( + executedAgent -> + preventionExpectationForAgent( + preventionExpectation.getScore(), + preventionExpectation.getName(), + preventionExpectation.getDescription(), + executedAgent.getParent(), + asset, + preventionExpectation.getExpirationTime(), + computeSignatures(payload, executedAgent.getProcessName()))) + .toList(); + } + + public static List getDetectionExpectationList( + Asset asset, + List executedAgents, + Payload payload, + DetectionExpectation detectionExpectation) { + return executedAgents.stream() + .map( + executedAgent -> + detectionExpectationForAgent( + detectionExpectation.getScore(), + detectionExpectation.getName(), + detectionExpectation.getDescription(), + executedAgent.getParent(), + asset, + detectionExpectation.getExpirationTime(), + computeSignatures(payload, executedAgent.getProcessName()))) + .toList(); + } + + public static List getManualExpectationList( + Asset asset, + List executedAgents, + ManualExpectation manualExpectation) { + return executedAgents.stream() + .map( + executedAgent -> + manualExpectationForAgent( + manualExpectation.getScore(), + manualExpectation.getName(), + manualExpectation.getDescription(), + executedAgent.getParent(), + asset, + manualExpectation.getExpirationTime())) + .toList(); + } + + private static List computeSignatures( + Payload payload, String processName) { + List injectExpectationSignatures = new ArrayList<>(); + + if (payload != null) { + PayloadType payloadType = payload.getTypeEnum(); + switch (payloadType) { + case COMMAND: + injectExpectationSignatures.add( + builder().type(EXPECTATION_SIGNATURE_TYPE_PROCESS_NAME).value(processName).build()); + break; + case EXECUTABLE: + Executable payloadExecutable = (Executable) Hibernate.unproxy(payload); + injectExpectationSignatures.add( + builder() + .type(EXPECTATION_SIGNATURE_TYPE_FILE_NAME) + .value( + Optional.ofNullable(payloadExecutable.getExecutableFile()) + .map(Document::getName) + .orElse("")) + .build()); + break; + case FILE_DROP: + FileDrop payloadFileDrop = (FileDrop) Hibernate.unproxy(payload); + injectExpectationSignatures.add( + builder() + .type(EXPECTATION_SIGNATURE_TYPE_FILE_NAME) + .value( + Optional.ofNullable(payloadFileDrop.getFileDropFile()) + .map(Document::getName) + .orElse("")) + .build()); + break; + case DNS_RESOLUTION: + DnsResolution payloadDnsResolution = (DnsResolution) Hibernate.unproxy(payload); + injectExpectationSignatures.add( + builder() + .type(EXPECTATION_SIGNATURE_TYPE_HOSTNAME) + .value(payloadDnsResolution.getHostname().split("\\r?\\n")[0]) + .build()); + break; + default: + throw new UnsupportedOperationException( + "Payload type " + payloadType + " is not supported"); + } + } else { + injectExpectationSignatures.add( + builder().type(EXPECTATION_SIGNATURE_TYPE_PROCESS_NAME).value(processName).build()); + } + return injectExpectationSignatures; + } + + // OBAS IMPLANT EXPECTATIONS + + public static List getManualExpectationList( + Asset asset, Inject inject, ManualExpectation manualExpectation) { + return getActiveAgents(asset, inject).stream() + .map( + agent -> + manualExpectationForAgent( + manualExpectation.getScore(), + manualExpectation.getName(), + manualExpectation.getDescription(), + agent, + asset, + manualExpectation.getExpirationTime())) + .toList(); + } + + public static List getDetectionExpectationList( + Asset asset, Inject inject, String payloadType, DetectionExpectation detectionExpectation) { + return getActiveAgents(asset, inject).stream() + .map( + agent -> + detectionExpectationForAgent( + detectionExpectation.getScore(), + detectionExpectation.getName(), + detectionExpectation.getDescription(), + agent, + asset, + detectionExpectation.getExpirationTime(), + computeSignatures(inject.getId(), agent.getId(), payloadType))) + .toList(); + } + + public static List getPreventionExpectationList( + Asset asset, Inject inject, String payloadType, PreventionExpectation preventionExpectation) { + return getActiveAgents(asset, inject).stream() + .map( + agent -> + preventionExpectationForAgent( + preventionExpectation.getScore(), + preventionExpectation.getName(), + preventionExpectation.getDescription(), + agent, + asset, + preventionExpectation.getExpirationTime(), + computeSignatures(inject.getId(), agent.getId(), payloadType))) + .toList(); + } + + private static List computeSignatures( + String injectId, String agentId, String payloadType) { + List signatures = new ArrayList<>(); + List knownPayloadTypes = + Arrays.asList("Command", "Executable", "FileDrop", "DnsResolution"); + + /* + * Always add the "Parent process" signature type for the OpenBAS Implant Executor + */ + signatures.add( + createSignature( + EXPECTATION_SIGNATURE_TYPE_PARENT_PROCESS_NAME, + "obas-implant-" + injectId + "-agent-" + agentId)); + + if (!knownPayloadTypes.contains(payloadType)) { + throw new UnsupportedOperationException("Payload type " + payloadType + " is not supported"); + } + return signatures; + } + + private static InjectExpectationSignature createSignature( + String signatureType, String signatureValue) { + return builder().type(signatureType).value(signatureValue).build(); + } } diff --git a/openbas-api/src/main/java/io/openbas/utils/Time.java b/openbas-api/src/main/java/io/openbas/utils/Time.java index 584665c62a..0b191a80bc 100644 --- a/openbas-api/src/main/java/io/openbas/utils/Time.java +++ b/openbas-api/src/main/java/io/openbas/utils/Time.java @@ -10,6 +10,7 @@ import java.util.Locale; public class Time { + public static Instant toInstant(@NotNull final String dateString) { String pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'"; DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault()); diff --git a/openbas-api/src/main/resources/implants/caldera/windows/x86_64/obas-implant-caldera-windows.ps1 b/openbas-api/src/main/resources/implants/caldera/windows/x86_64/obas-implant-caldera-windows.ps1 new file mode 100644 index 0000000000..e971f10988 --- /dev/null +++ b/openbas-api/src/main/resources/implants/caldera/windows/x86_64/obas-implant-caldera-windows.ps1 @@ -0,0 +1,2 @@ +$server=$args[0] +& "C:\Program Files (x86)\Filigran\OBAS Caldera\obas-agent-caldera.exe" -server $server -group red diff --git a/openbas-api/src/test/java/io/openbas/executors/caldera/service/CalderaExecutorServiceTest.java b/openbas-api/src/test/java/io/openbas/executors/caldera/service/CalderaExecutorServiceTest.java index dcc44e7ac8..aa4d23a497 100644 --- a/openbas-api/src/test/java/io/openbas/executors/caldera/service/CalderaExecutorServiceTest.java +++ b/openbas-api/src/test/java/io/openbas/executors/caldera/service/CalderaExecutorServiceTest.java @@ -1,31 +1,39 @@ package io.openbas.executors.caldera.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static io.openbas.executors.caldera.service.CalderaExecutorService.toArch; +import static io.openbas.executors.caldera.service.CalderaExecutorService.toPlatform; +import static io.openbas.utils.Time.toInstant; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import io.openbas.database.model.Endpoint; import io.openbas.database.model.Executor; +import io.openbas.executors.ExecutorService; import io.openbas.executors.caldera.client.CalderaExecutorClient; import io.openbas.executors.caldera.config.CalderaExecutorConfig; import io.openbas.executors.caldera.model.Agent; -import io.openbas.integrations.ExecutorService; import io.openbas.integrations.InjectorService; +import io.openbas.service.AgentService; import io.openbas.service.EndpointService; import io.openbas.service.PlatformSettingsService; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class CalderaExecutorServiceTest { - private static final String CALDERA_AGENT_HOSTNAME = "calderaHostname"; + + private static final String CALDERA_AGENT_HOSTNAME = "calderahostname"; private static final String CALDERA_AGENT_EXTERNAL_REF = "calderaExt"; private static final String CALDERA_AGENT_IP = "10.10.10.10"; private static final String CALDERA_AGENT_USERNAME = "openbas_user"; @@ -33,7 +41,7 @@ public class CalderaExecutorServiceTest { private static final String CALDERA_EXECUTOR_TYPE = "openbas_caldera"; private static final String CALDERA_EXECUTOR_NAME = "Caldera"; - private static final String DATE = "2024-12-11T12:45:30Z"; + private String DATE; @Mock private ExecutorService executorService; @@ -45,6 +53,8 @@ public class CalderaExecutorServiceTest { @Mock private EndpointService endpointService; + @Mock private AgentService agentService; + @Mock private InjectorService injectorService; @Mock private PlatformSettingsService platformSettingsService; @@ -55,6 +65,7 @@ public class CalderaExecutorServiceTest { private Endpoint calderaEndpoint; private Endpoint randomEndpoint; + private io.openbas.database.model.Agent agentEndpoint; private Agent calderaAgent; private Agent randomAgent; private Executor calderaExecutor; @@ -62,6 +73,11 @@ public class CalderaExecutorServiceTest { @BeforeEach void setUp() { + Instant now = Instant.now(); + DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.systemDefault()); + DATE = formatter.format(now); + calderaAgent = new Agent(); calderaAgent.setArchitecture("Arch"); calderaAgent.setPaw(CALDERA_AGENT_EXTERNAL_REF); @@ -80,31 +96,28 @@ void setUp() { CALDERA_AGENT_EXTERNAL_REF, CALDERA_AGENT_USERNAME); randomAgent = createAgent("hostname", "1.1.1.1", "ref", CALDERA_AGENT_USERNAME); - calderaEndpoint = createEndpoint(calderaAgent, calderaExecutor); - randomEndpoint = createEndpoint(randomAgent, randomExecutor); + calderaEndpoint = createEndpoint(calderaAgent); + randomEndpoint = createEndpoint(randomAgent); - when(endpointService.findAssetsForInjectionByHostname(CALDERA_AGENT_HOSTNAME)) - .thenReturn(List.of(calderaEndpoint, randomEndpoint)); + agentEndpoint = new io.openbas.database.model.Agent(); + agentEndpoint.setProcessName(calderaAgent.getExe_name()); + agentEndpoint.setExecutor(calderaExecutor); + agentEndpoint.setExternalReference(calderaAgent.getPaw()); + agentEndpoint.setPrivilege(io.openbas.database.model.Agent.PRIVILEGE.admin); + agentEndpoint.setDeploymentMode(io.openbas.database.model.Agent.DEPLOYMENT_MODE.session); + agentEndpoint.setExecutedByUser(calderaAgent.getUsername()); + agentEndpoint.setLastSeen(toInstant(DATE)); + agentEndpoint.setAsset(calderaEndpoint); } - private Endpoint createEndpoint(Agent agent, Executor executor) { + private Endpoint createEndpoint(Agent agent) { Endpoint endpoint = new Endpoint(); - io.openbas.database.model.Agent agentEndpoint = new io.openbas.database.model.Agent(); endpoint.setName(agent.getHost()); endpoint.setDescription("Asset collected by Caldera executor context."); endpoint.setIps(agent.getHost_ip_addrs()); endpoint.setHostname(agent.getHost()); - endpoint.setPlatform(CalderaExecutorService.toPlatform("windows")); - endpoint.setArch(CalderaExecutorService.toArch("amd64")); - agentEndpoint.setProcessName(agent.getExe_name()); - agentEndpoint.setExecutor(executor); - agentEndpoint.setExternalReference(agent.getPaw()); - agentEndpoint.setPrivilege(io.openbas.database.model.Agent.PRIVILEGE.admin); - agentEndpoint.setDeploymentMode(io.openbas.database.model.Agent.DEPLOYMENT_MODE.session); - agentEndpoint.setExecutedByUser(agent.getUsername()); - agentEndpoint.setLastSeen(calderaExecutorService.toInstant(DATE)); - agentEndpoint.setAsset(endpoint); - endpoint.setAgents(List.of(agentEndpoint)); + endpoint.setPlatform(toPlatform("windows")); + endpoint.setArch(toArch("amd64")); return endpoint; } @@ -122,41 +135,44 @@ private Agent createAgent(String hostname, String ip, String externalRef, String } @Test - void test_run_WITH_one_endpoint() throws Exception { + void test_run_WITH_one_endpoint_one_agent() { when(client.agents()).thenReturn(List.of(calderaAgent)); calderaExecutorService.run(); - verify(endpointService).updateEndpoint(calderaEndpoint); + ArgumentCaptor agentCaptor = + ArgumentCaptor.forClass(io.openbas.database.model.Agent.class); + ArgumentCaptor endpointCaptor = ArgumentCaptor.forClass(Endpoint.class); + verify(endpointService) + .createNewEndpointAndAgent(agentCaptor.capture(), endpointCaptor.capture()); + + Endpoint capturedEndpoint = endpointCaptor.getValue(); + assertEquals(CALDERA_AGENT_HOSTNAME, capturedEndpoint.getHostname()); + assertArrayEquals(new String[] {CALDERA_AGENT_IP}, capturedEndpoint.getIps()); + assertEquals(Endpoint.PLATFORM_TYPE.Windows, capturedEndpoint.getPlatform()); + assertEquals(Endpoint.PLATFORM_ARCH.x86_64, capturedEndpoint.getArch()); + + io.openbas.database.model.Agent capturedAgent = agentCaptor.getValue(); + assertEquals(CALDERA_AGENT_EXTERNAL_REF, capturedAgent.getExternalReference()); } @Test - void test_run_WITH_2_existing_endpoint_same_machine() throws Exception { + void test_run_WITH_2_existing_agents_same_machine() { when(client.agents()).thenReturn(List.of(calderaAgent)); - randomEndpoint.setHostname(CALDERA_AGENT_HOSTNAME); - randomEndpoint.setIps(new String[] {CALDERA_AGENT_IP}); - calderaExecutorService.run(); - verify(endpointService).updateEndpoint(calderaEndpoint); - } + when(this.endpointService.findEndpointByAgentDetails( + calderaEndpoint.getHostname(), + calderaEndpoint.getPlatform(), + calderaEndpoint.getArch())) + .thenReturn(Optional.of(calderaEndpoint)); - @Test - void test_findExistingEndpointForAnAgent_WITH_2_existing_endpoint_same_host() throws Exception { - Optional result = calderaExecutorService.findExistingEndpointForAnAgent(calderaAgent); - assertEquals(calderaEndpoint, result.get()); - } - - @Test - void test_findExistingEndpointForAnAgent_WITH_no_existing_endpoint() throws Exception { - when(endpointService.findAssetsForInjectionByHostname(CALDERA_AGENT_HOSTNAME)) - .thenReturn(List.of()); - Optional result = calderaExecutorService.findExistingEndpointForAnAgent(calderaAgent); - assertTrue(result.isEmpty()); - } - - @Test - void test_findExistingEndpointForAnAgent_WITH_1_enpdoint_with_null_executor() throws Exception { - randomEndpoint.getAgents().getFirst().setExecutor(null); randomEndpoint.setHostname(CALDERA_AGENT_HOSTNAME); randomEndpoint.setIps(new String[] {CALDERA_AGENT_IP}); - Optional result = calderaExecutorService.findExistingEndpointForAnAgent(calderaAgent); - assertEquals(calderaEndpoint, result.get()); + calderaExecutorService.run(); + ArgumentCaptor endpointCaptor = ArgumentCaptor.forClass(Endpoint.class); + verify(endpointService).updateEndpoint(endpointCaptor.capture()); + + Endpoint capturedEndpoint = endpointCaptor.getValue(); + assertEquals(CALDERA_AGENT_HOSTNAME, capturedEndpoint.getHostname()); + assertArrayEquals(new String[] {CALDERA_AGENT_IP}, capturedEndpoint.getIps()); + assertEquals(Endpoint.PLATFORM_TYPE.Windows, capturedEndpoint.getPlatform()); + assertEquals(Endpoint.PLATFORM_ARCH.x86_64, capturedEndpoint.getArch()); } } diff --git a/openbas-api/src/test/java/io/openbas/rest/EndpointApiTest.java b/openbas-api/src/test/java/io/openbas/rest/EndpointApiTest.java index c8a949c7f6..30a69fe3bf 100644 --- a/openbas-api/src/test/java/io/openbas/rest/EndpointApiTest.java +++ b/openbas-api/src/test/java/io/openbas/rest/EndpointApiTest.java @@ -96,7 +96,7 @@ void given_validEndpointInput_should_upsertEndpointSuccessfully() throws Excepti .getContentAsString(); // --ASSERT-- - assertEquals(newName, JsonPath.read(response, "$.endpoint_hostname")); + assertEquals(newName.toLowerCase(), JsonPath.read(response, "$.endpoint_hostname")); } @DisplayName( diff --git a/openbas-api/src/test/java/io/openbas/rest/inject/InjectApiTest.java b/openbas-api/src/test/java/io/openbas/rest/inject/InjectApiTest.java index 7d8b3274d1..e57140915f 100644 --- a/openbas-api/src/test/java/io/openbas/rest/inject/InjectApiTest.java +++ b/openbas-api/src/test/java/io/openbas/rest/inject/InjectApiTest.java @@ -758,11 +758,8 @@ private Inject getPendingInjectWithAssets() { .withEndpoint( endpointComposer .forEndpoint(EndpointFixture.createEndpoint()) - .withAgent(agentComposer.forAgent(AgentFixture.createDefaultAgent()))) - .withEndpoint( - endpointComposer - .forEndpoint(EndpointFixture.createEndpoint()) - .withAgent(agentComposer.forAgent(AgentFixture.createDefaultAgent()))) + .withAgent(agentComposer.forAgent(AgentFixture.createDefaultAgentService())) + .withAgent(agentComposer.forAgent(AgentFixture.createDefaultAgentSession()))) .withInjectStatus( injectStatusComposer.forInjectStatus(InjectStatusFixture.createDefaultInjectStatus())) .persist() diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/AgentFixture.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/AgentFixture.java index 2f6e5505db..dc988d434e 100644 --- a/openbas-api/src/test/java/io/openbas/utils/fixtures/AgentFixture.java +++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/AgentFixture.java @@ -6,7 +6,7 @@ public class AgentFixture { - public static Agent createDefaultAgent() { + public static Agent createDefaultAgentService() { Agent agent = new Agent(); agent.setExecutedByUser(Agent.ADMIN_SYSTEM_WINDOWS); agent.setPrivilege(Agent.PRIVILEGE.admin); @@ -15,8 +15,17 @@ public static Agent createDefaultAgent() { return agent; } + public static Agent createDefaultAgentSession() { + Agent agent = new Agent(); + agent.setExecutedByUser(Agent.ADMIN_SYSTEM_WINDOWS); + agent.setPrivilege(Agent.PRIVILEGE.admin); + agent.setDeploymentMode(Agent.DEPLOYMENT_MODE.session); + agent.setLastSeen(Instant.now()); + return agent; + } + public static Agent createAgent(Asset asset, String externalReference) { - Agent agent = createDefaultAgent(); + Agent agent = createDefaultAgentService(); agent.setAsset(asset); agent.setExternalReference(externalReference); return agent; diff --git a/openbas-front/src/admin/components/agents/Agents.tsx b/openbas-front/src/admin/components/agents/Agents.tsx index 949e089358..59ba9aa830 100644 --- a/openbas-front/src/admin/components/agents/Agents.tsx +++ b/openbas-front/src/admin/components/agents/Agents.tsx @@ -117,11 +117,14 @@ get-process | ? {$_.modules.filename -like '${agentFolder ?? 'C:\\Program Files rm -force '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -ea ignore; New-Item -ItemType Directory -Force -Path '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}' | Out-Null; [io.file]::WriteAllBytes('${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe',$data) | Out-Null; +$data=$wc.DownloadData($url + "/ps1"); +rm -force 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera\\obas-agent-caldera.ps1' -ea ignore; +[io.file]::WriteAllBytes('C:\\Program Files (x86)\\Filigran\\OBAS Caldera\\obas-agent-caldera.ps1',$data) | Out-Null; New-NetFirewallRule -DisplayName "Allow OpenBAS" -Direction Inbound -Program '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -Action Allow | Out-Null; New-NetFirewallRule -DisplayName "Allow OpenBAS" -Direction Outbound -Program '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -Action Allow | Out-Null; Start-Process -FilePath '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -ArgumentList "-server $server -group red" -WindowStyle hidden; -schtasks /create /tn OpenBAS /sc onstart /ru system /tr "Powershell -ExecutionPolicy Bypass -Command \\\`"Start-Process -FilePath \\\\\\\`"${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe\\\\\\\`" -ArgumentList \\\\\\\`"-server $server -group red\\\\\\\`" -WindowStyle hidden;\\\`"";`, - code: `$server="${settings.executor_caldera_public_url}";$url="${settings.platform_base_url}/api/implant/caldera/windows/${arch}";$wc=New-Object System.Net.WebClient;$data=$wc.DownloadData($url);get-process | ? {$_.modules.filename -like '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe'} | stop-process -f;rm -force '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -ea ignore;New-Item -ItemType Directory -Force -Path '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}' | Out-Null;[io.file]::WriteAllBytes('${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe',$data) | Out-Null;New-NetFirewallRule -DisplayName "Allow OpenBAS" -Direction Inbound -Program '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -Action Allow | Out-Null;New-NetFirewallRule -DisplayName "Allow OpenBAS" -Direction Outbound -Program '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -Action Allow | Out-Null;Start-Process -FilePath '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -ArgumentList "-server $server -group red" -WindowStyle hidden;schtasks /create /tn OpenBAS /sc onstart /ru system /tr "Powershell -NoProfile -ExecutionPolicy Bypass -Command \\\`"Start-Process -FilePath \\\\\\\`"${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe\\\\\\\`" -ArgumentList \\\\\\\`"-server $server -group red\\\\\\\`" -WindowStyle hidden; \\\`"";`, +schtasks /create /tn OpenBASCaldera /sc onlogon /rl highest /tr "Powershell -ExecutionPolicy Bypass -NoProfile -WindowStyle hidden -File 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera\\obas-agent-caldera.ps1' $server";`, + code: `$server="${settings.executor_caldera_public_url}";$url="${settings.platform_base_url}/api/implant/caldera/windows/${arch}";$wc=New-Object System.Net.WebClient;$data=$wc.DownloadData($url);get-process | ? {$_.modules.filename -like '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe'} | stop-process -f;rm -force '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -ea ignore;New-Item -ItemType Directory -Force -Path '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}' | Out-Null;[io.file]::WriteAllBytes('${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe',$data) | Out-Null;$data=$wc.DownloadData($url + "/ps1");rm -force 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera\\obas-agent-caldera.ps1' -ea ignore;[io.file]::WriteAllBytes('C:\\Program Files (x86)\\Filigran\\OBAS Caldera\\obas-agent-caldera.ps1',$data) | Out-Null;New-NetFirewallRule -DisplayName "Allow OpenBAS" -Direction Inbound -Program '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -Action Allow | Out-Null;New-NetFirewallRule -DisplayName "Allow OpenBAS" -Direction Outbound -Program '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -Action Allow | Out-Null;Start-Process -FilePath '${agentFolder ?? 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera'}\\obas-agent-caldera.exe' -ArgumentList "-server $server -group red" -WindowStyle hidden;schtasks /create /tn OpenBASCaldera /sc onlogon /rl highest /tr "Powershell -ExecutionPolicy Bypass -NoProfile -WindowStyle hidden -File 'C:\\Program Files (x86)\\Filigran\\OBAS Caldera\\obas-agent-caldera.ps1' $server";`, }; case 'linux': return { diff --git a/openbas-front/src/admin/components/common/injects/status/InjectStatus.tsx b/openbas-front/src/admin/components/common/injects/status/InjectStatus.tsx index 08b2e3089f..c2d0602997 100644 --- a/openbas-front/src/admin/components/common/injects/status/InjectStatus.tsx +++ b/openbas-front/src/admin/components/common/injects/status/InjectStatus.tsx @@ -54,9 +54,15 @@ const InjectStatus = ({ injectStatus = null, endpointsMap = new Map() }: Props) {t('Traces')} {(injectStatus.status_main_traces || []).length > 0 && } - {Array.from(orderedTracesByAsset.entries()).map(([assetId, tracesByAgent]) => ( - - ))} + {Array.from(orderedTracesByAsset.entries()) + .sort(([assetIdA], [assetIdB]) => assetIdA.localeCompare(assetIdB)) + .map(([assetId, tracesByAgent]) => ( + + ))} ) : ( diff --git a/openbas-front/src/admin/components/common/injects/status/traces/AgentTraces.tsx b/openbas-front/src/admin/components/common/injects/status/traces/AgentTraces.tsx index b1952d32bf..5cf336d70d 100644 --- a/openbas-front/src/admin/components/common/injects/status/traces/AgentTraces.tsx +++ b/openbas-front/src/admin/components/common/injects/status/traces/AgentTraces.tsx @@ -1,5 +1,6 @@ import { ArrowDropDownSharp, ArrowRightSharp } from '@mui/icons-material'; import { Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; import { useState } from 'react'; import { useFormatter } from '../../../../../../components/i18n'; @@ -11,6 +12,8 @@ import TraceMessage from './TraceMessage'; interface Props { agentStatus: AgentStatusOutput } const AgentTraces = ({ agentStatus }: Props) => { + // Standard hooks + const theme = useTheme(); const [isExpanded, setIsExpanded] = useState(false); const { t } = useFormatter(); @@ -40,6 +43,7 @@ const AgentTraces = ({ agentStatus }: Props) => {
{ const { t } = useFormatter(); + const sortedTraces = [...tracesByAgent].sort((a, b) => { + const nameA = a.agent_name ?? ''; + const nameB = b.agent_name ?? ''; + return nameA.localeCompare(nameB); + }); + return (
{ {t(endpoint.endpoint_platform)}
- {tracesByAgent.map(t => )} + {sortedTraces.map(t => )}
); diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 4c335919e6..5bb676095b 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -13,6 +13,8 @@ export interface Agent { agent_active?: boolean; agent_asset: string; /** @format date-time */ + agent_cleared_at?: string; + /** @format date-time */ agent_created_at: string; agent_deployment_mode: "service" | "session"; agent_executed_by_user: string; @@ -31,7 +33,7 @@ export interface Agent { listened?: boolean; } -/** List of agents */ +/** List of primary agents */ export interface AgentOutput { /** Indicates whether the endpoint is active. The endpoint is considered active if it was seen in the last 3 minutes. */ agent_active?: boolean; @@ -290,8 +292,6 @@ export interface AttackPatternUpsertInput { interface BasePayload { listened?: boolean; - /** @format int32 */ - numberOfActions?: number; payload_arguments?: PayloadArgument[]; payload_attack_patterns?: string[]; payload_cleanup_command?: string; @@ -540,8 +540,6 @@ export interface Command { command_content: string; command_executor: string; listened?: boolean; - /** @format int32 */ - numberOfActions?: number; payload_arguments?: PayloadArgument[]; payload_attack_patterns?: string[]; payload_cleanup_command?: string; @@ -624,8 +622,6 @@ export interface DirectInjectInput { export interface DnsResolution { dns_resolution_hostname: string; listened?: boolean; - /** @format int32 */ - numberOfActions?: number; payload_arguments?: PayloadArgument[]; payload_attack_patterns?: string[]; payload_cleanup_command?: string; @@ -684,8 +680,6 @@ export interface DocumentUpdateInput { export interface Endpoint { asset_agents?: Agent[]; /** @format date-time */ - asset_cleared_at?: string; - /** @format date-time */ asset_created_at: string; asset_description?: string; asset_id: string; @@ -727,7 +721,7 @@ export interface EndpointOutput { export interface EndpointOverviewOutput { /** - * List of agents + * List of primary agents * @uniqueItems true */ asset_agents: AgentOutput[]; @@ -809,8 +803,6 @@ export interface EvaluationInput { export interface Executable { executable_file?: string; listened?: boolean; - /** @format int32 */ - numberOfActions?: number; payload_arguments?: PayloadArgument[]; payload_attack_patterns?: string[]; payload_cleanup_command?: string; @@ -858,7 +850,7 @@ export interface ExecutionTraces { | "WARNING" | "PARTIAL" | "MAYBE_PARTIAL_PREVENTED" - | "ASSET_INACTIVE" + | "AGENT_INACTIVE" | "INFO"; /** @format date-time */ execution_time?: string; @@ -898,7 +890,7 @@ export interface ExecutionTracesOutput { | "WARNING" | "PARTIAL" | "MAYBE_PARTIAL_PREVENTED" - | "ASSET_INACTIVE" + | "AGENT_INACTIVE" | "INFO"; /** @format date-time */ execution_time: string; @@ -1147,8 +1139,6 @@ export interface ExportOptionsInput { export interface FileDrop { file_drop_file?: string; listened?: boolean; - /** @format int32 */ - numberOfActions?: number; payload_arguments?: PayloadArgument[]; payload_attack_patterns?: string[]; payload_cleanup_command?: string; @@ -2206,8 +2196,6 @@ export interface NetworkTraffic { /** @format int32 */ network_traffic_port_src: number; network_traffic_protocol: string; - /** @format int32 */ - numberOfActions?: number; payload_arguments?: PayloadArgument[]; payload_attack_patterns?: string[]; payload_cleanup_command?: string; @@ -3325,8 +3313,6 @@ export interface SearchTerm { } export interface SecurityPlatform { - /** @format date-time */ - asset_cleared_at?: string; /** @format date-time */ asset_created_at: string; asset_description?: string; diff --git a/openbas-model/src/main/java/io/openbas/database/model/Agent.java b/openbas-model/src/main/java/io/openbas/database/model/Agent.java index b6896a5dde..1276867b36 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Agent.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Agent.java @@ -138,6 +138,10 @@ public boolean isActive() { @NotNull private Instant updatedAt = now(); + @Column(name = "agent_cleared_at") + @JsonProperty("agent_cleared_at") + private Instant clearedAt = now(); + @Override public int hashCode() { return Objects.hash(id); diff --git a/openbas-model/src/main/java/io/openbas/database/model/Asset.java b/openbas-model/src/main/java/io/openbas/database/model/Asset.java index e8dc19a5ea..09a7afb43d 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Asset.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Asset.java @@ -53,10 +53,6 @@ public class Asset implements Base { @JsonProperty("asset_description") private String description; - @Column(name = "asset_cleared_at") - @JsonProperty("asset_cleared_at") - private Instant clearedAt = now(); - // -- TAG -- @ArraySchema(schema = @Schema(type = "string")) diff --git a/openbas-model/src/main/java/io/openbas/database/model/DnsResolution.java b/openbas-model/src/main/java/io/openbas/database/model/DnsResolution.java index 290117073a..a80bce9a67 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/DnsResolution.java +++ b/openbas-model/src/main/java/io/openbas/database/model/DnsResolution.java @@ -34,12 +34,4 @@ public DnsResolution() {} public DnsResolution(String id, String type, String name) { super(id, type, name); } - - /* - * the DNS resolution payload expects one action carried out per listed hostname - */ - @Override - public int getNumberOfActions() { - return this.getHostname().split("\\r?\\n").length; - } } diff --git a/openbas-model/src/main/java/io/openbas/database/model/Endpoint.java b/openbas-model/src/main/java/io/openbas/database/model/Endpoint.java index 10ef5128e2..209d61d19e 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Endpoint.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Endpoint.java @@ -1,6 +1,5 @@ package io.openbas.database.model; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.hypersistence.utils.hibernate.type.array.StringArrayType; @@ -90,26 +89,18 @@ public enum PLATFORM_TYPE { mappedBy = "asset", fetch = FetchType.EAGER, cascade = CascadeType.ALL, - orphanRemoval = true) // TODO lazy with transactions with agent repository for the "getAgents" + orphanRemoval = true) // method @JsonProperty("asset_agents") @JsonSerialize(using = MultiModelDeserializer.class) private List agents = new ArrayList<>(); - @JsonIgnore - public Executor getExecutor() { - if (this.agents.isEmpty()) { - return null; - } - return this.agents.getFirst().getExecutor(); + public String getHostname() { + return hostname.toLowerCase(); } - @JsonIgnore - public boolean getActive() { - if (this.agents.isEmpty()) { - return false; - } - return this.agents.getFirst().isActive(); + public void setHostname(String hostname) { + this.hostname = hostname.toLowerCase(); } public Endpoint() {} diff --git a/openbas-model/src/main/java/io/openbas/database/model/Executable.java b/openbas-model/src/main/java/io/openbas/database/model/Executable.java index ba73f21f47..5f66c00e10 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Executable.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Executable.java @@ -33,12 +33,4 @@ public Executable() {} public Executable(String id, String type, String name) { super(id, type, name); } - - /* - * return the number of actions an executable is expected to achieve - * by default this is 2 here, one file drop and one execution - */ - public int getNumberOfActions() { - return DEFAULT_NUMBER_OF_ACTIONS_FOR_EXECUTABLE; - } } diff --git a/openbas-model/src/main/java/io/openbas/database/model/ExecutionTraceStatus.java b/openbas-model/src/main/java/io/openbas/database/model/ExecutionTraceStatus.java index 0ccc3bc17a..43cec29de2 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/ExecutionTraceStatus.java +++ b/openbas-model/src/main/java/io/openbas/database/model/ExecutionTraceStatus.java @@ -15,6 +15,6 @@ public enum ExecutionTraceStatus { MAYBE_PARTIAL_PREVENTED, // Other informations - ASSET_INACTIVE, + AGENT_INACTIVE, INFO, } diff --git a/openbas-model/src/main/java/io/openbas/database/model/Inject.java b/openbas-model/src/main/java/io/openbas/database/model/Inject.java index 877fc54693..eb3ddf3c8d 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Inject.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Inject.java @@ -1,6 +1,5 @@ package io.openbas.database.model; -import static io.openbas.database.model.Endpoint.ENDPOINT_TYPE; import static io.openbas.database.specification.InjectSpecification.VALID_TESTABLE_TYPES; import static java.time.Instant.now; import static java.util.Optional.ofNullable; @@ -445,195 +444,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(id); } - - /** - * Creates an Inject from a Raw Inject - * - * @param rawInject the raw inject to convert - * @param rawTeams the map of the teams containing at least the ones linked to this inject - * @param rawInjectExpectationMap the map of the expectations containing at least the ones linked - * to this inject - * @param mapOfAssetGroups the map of the asset groups containing at least the ones linked to this - * inject - * @param mapOfAsset the map of the asset containing at least the ones linked to this inject and - * the asset groups linked to it - * @return an Inject - */ - public static Inject fromRawInject( - RawInject rawInject, - Map rawTeams, - Map rawInjectExpectationMap, - Map mapOfAssetGroups, - Map mapOfAsset) { - // Create the object - Inject inject = new Inject(); - inject.setId(rawInject.getInject_id()); - - // Set a list of expectations - inject.setExpectations(new ArrayList<>()); - for (String expectationId : rawInject.getInject_expectations()) { - RawInjectExpectation rawInjectExpectation = rawInjectExpectationMap.get(expectationId); - if (rawInjectExpectation != null) { - // Create a new expectation - InjectExpectation expectation = new InjectExpectation(); - expectation.setId(rawInjectExpectation.getInject_expectation_id()); - expectation.setType( - InjectExpectation.EXPECTATION_TYPE.valueOf( - rawInjectExpectation.getInject_expectation_type())); - expectation.setScore(rawInjectExpectation.getInject_expectation_score()); - expectation.setExpectedScore(rawInjectExpectation.getInject_expectation_expected_score()); - expectation.setExpectationGroup(rawInjectExpectation.getInject_expectation_group()); - - // Add the team of the expectation - if (rawInjectExpectation.getTeam_id() != null) { - RawTeam rawTeam = rawTeams.get(rawInjectExpectation.getTeam_id()); - if (rawTeam != null) { - Team team = new Team(); - team.setId(rawInjectExpectation.getTeam_id()); - team.setName(rawTeam.getTeam_name()); - expectation.setTeam(team); - } - } - - // Add the asset group of the expectation - if (rawInjectExpectation.getAsset_group_id() != null) { - RawAssetGroup rawAssetGroup = - mapOfAssetGroups.get(rawInjectExpectation.getAsset_group_id()); - if (rawAssetGroup != null) { - AssetGroup assetGroup = new AssetGroup(); - assetGroup.setId(rawAssetGroup.getAsset_group_id()); - assetGroup.setName(rawAssetGroup.getAsset_group_name()); - assetGroup.setAssets(new ArrayList<>()); - - // We add the assets to the asset group - if (rawAssetGroup.getAsset_ids() != null) { - for (String assetId : rawAssetGroup.getAsset_ids()) { - RawAsset rawAsset = mapOfAsset.get(assetId); - if (rawAsset != null) { - if (rawAsset.getAsset_type().equals(ENDPOINT_TYPE)) { - Endpoint endpoint = - new Endpoint( - rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name(), - Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); - assetGroup.getAssets().add(endpoint); - } else { - Asset asset = - new Asset( - rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name()); - assetGroup.getAssets().add(asset); - } - } - } - } - expectation.setAssetGroup(assetGroup); - } - } - - // We add the asset to the expectation - if (rawInjectExpectation.getAsset_id() != null) { - RawAsset rawAsset = mapOfAsset.get(rawInjectExpectation.getAsset_id()); - if (rawAsset != null) { - if (rawAsset.getAsset_type().equals(ENDPOINT_TYPE)) { - Endpoint endpoint = - new Endpoint( - rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name(), - Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); - expectation.setAsset(endpoint); - } else { - Asset asset = - new Asset( - rawAsset.getAsset_id(), rawAsset.getAsset_type(), rawAsset.getAsset_name()); - expectation.setAsset(asset); - } - } - } - inject.getExpectations().add(expectation); - } - } - - // We add the teams to the inject - ArrayList injectTeams = new ArrayList<>(); - for (String injectTeamId : rawInject.getInject_teams()) { - Team team = new Team(); - team.setId(rawTeams.get(injectTeamId).getTeam_id()); - team.setName(rawTeams.get(injectTeamId).getTeam_name()); - injectTeams.add(team); - } - inject.setTeams(injectTeams); - - // We add the assets to the inject - ArrayList injectAssets = new ArrayList<>(); - for (String injectAssetId : rawInject.getInject_assets()) { - RawAsset rawAsset = mapOfAsset.get(injectAssetId); - - if (rawAsset == null) { - continue; // Skip to the next iteration - } - - if ("Endpoint".equals(rawAsset.getAsset_type())) { - Endpoint endpoint = - new Endpoint( - rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name(), - Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); - injectAssets.add(endpoint); - } else { - Asset newAsset = - new Asset(rawAsset.getAsset_id(), rawAsset.getAsset_type(), rawAsset.getAsset_name()); - injectAssets.add(newAsset); - } - } - inject.setAssets(injectAssets); - - // Add the asset groups to the inject - ArrayList injectAssetGroups = new ArrayList<>(); - for (String injectAssetGroupId : rawInject.getInject_asset_groups()) { - Optional rawAssetGroup = - Optional.ofNullable(mapOfAssetGroups.get(injectAssetGroupId)); - rawAssetGroup.ifPresent( - rag -> { - AssetGroup assetGroup = new AssetGroup(); - assetGroup.setName(rag.getAsset_group_name()); - assetGroup.setId(rag.getAsset_group_id()); - - // We add the assets linked to the asset group - assetGroup.setAssets( - rag.getAsset_ids().stream() - .map( - assetId -> { - RawAsset rawAsset = mapOfAsset.get(assetId); - if (rawAsset == null) { - return null; - } - - if ("Endpoint".equals(rawAsset.getAsset_type())) { - return new Endpoint( - rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name(), - Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); - } else { - return new Asset( - rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name()); - } - }) - .filter(Objects::nonNull) - .toList()); - injectAssetGroups.add(assetGroup); - }); - } - - inject.setAssetGroups(injectAssetGroups); - - return inject; - } } diff --git a/openbas-model/src/main/java/io/openbas/database/model/Payload.java b/openbas-model/src/main/java/io/openbas/database/model/Payload.java index 3e7c216ef3..706f373797 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Payload.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Payload.java @@ -227,12 +227,4 @@ public Payload(String id, String type, String name) { this.id = id; this.type = type; } - - /* - * return the number of actions a given payload is expected to achieve - * by default this is 1, e.g. one command, one file drop etc... - */ - public int getNumberOfActions() { - return DEFAULT_NUMBER_OF_ACTIONS_FOR_PAYLOAD; - } } diff --git a/openbas-model/src/main/java/io/openbas/database/model/User.java b/openbas-model/src/main/java/io/openbas/database/model/User.java index 5e2ea94645..6ec9c2587d 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/User.java +++ b/openbas-model/src/main/java/io/openbas/database/model/User.java @@ -165,7 +165,6 @@ public class User implements Base { @ArraySchema(schema = @Schema(description = "Group IDs of the user", type = "string")) @Setter - // @ManyToMany(fetch = FetchType.LAZY) @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "users_groups", diff --git a/openbas-model/src/main/java/io/openbas/database/repository/AgentRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/AgentRepository.java index e9ed14a080..7f3be2c40f 100644 --- a/openbas-model/src/main/java/io/openbas/database/repository/AgentRepository.java +++ b/openbas-model/src/main/java/io/openbas/database/repository/AgentRepository.java @@ -2,16 +2,48 @@ import io.openbas.database.model.Agent; import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; @Repository public interface AgentRepository - extends JpaRepository, JpaSpecificationExecutor { + extends CrudRepository, JpaSpecificationExecutor { - @Query("SELECT a FROM Agent a WHERE a.asset.id IN :assetIds") - List findAgentsByAssetIds(@Param("assetIds") List assetIds); + @Query( + value = + "SELECT a.* FROM agents a left join executors ex on a.agent_executor = ex.executor_id " + + "where a.agent_asset = :assetId and a.agent_executed_by_user = :user and a.agent_deployment_mode = :deployment " + + "and a.agent_privilege = :privilege and a.agent_parent is null and a.agent_inject is null and ex.executor_type = :executor", + nativeQuery = true) + Optional findByAssetExecutorUserDeploymentAndPrivilege( + @Param("assetId") String assetId, + @Param("user") String user, + @Param("deployment") String deployment, + @Param("privilege") String privilege, + @Param("executor") String executor); + + Optional findByExternalReference(String externalReference); + + /** + * Returns the agents for Caldera execution + * + * @return the list of agents + */ + @Query( + value = + "SELECT a.* FROM agents a WHERE a.agent_parent is not null and a.agent_inject is not null;", + nativeQuery = true) + List findForExecution(); + + // TODO : understand why the generic deleteById from Hibernate doesn't work + @Modifying + @Query(value = "DELETE FROM agents agent where agent.agent_id = :agentId;", nativeQuery = true) + @Transactional + void deleteByAgentId(String agentId); } diff --git a/openbas-model/src/main/java/io/openbas/database/repository/EndpointRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/EndpointRepository.java index 1478d6c1bf..5f5811e5ac 100644 --- a/openbas-model/src/main/java/io/openbas/database/repository/EndpointRepository.java +++ b/openbas-model/src/main/java/io/openbas/database/repository/EndpointRepository.java @@ -17,16 +17,14 @@ public interface EndpointRepository StatisticRepository, JpaSpecificationExecutor { - @Query( - value = "select e.* from assets e where e.endpoint_hostname = :hostname", - nativeQuery = true) - List findByHostname(@NotBlank final @Param("hostname") String hostname); - @Query( value = - "select e.* from assets e left join agents a on e.asset_id = a.agent_asset where a.agent_external_reference = :externalReference", + "select e.* from assets e where e.endpoint_hostname = :hostname and e.endpoint_platform = :platform and e.endpoint_arch = :arch", nativeQuery = true) - Optional findByExternalReference(@Param("externalReference") String externalReference); + Optional findByHostnameArchAndPlatform( + @NotBlank final @Param("hostname") String hostname, + @NotBlank final @Param("platform") String platform, + @NotBlank final @Param("arch") String arch); @Override @Query( @@ -42,16 +40,6 @@ public interface EndpointRepository @Query("select count(distinct e) from Endpoint e where e.createdAt > :creationDate") long globalCount(@Param("creationDate") Instant creationDate); - /* For some agents (e.g. Caldera), we have the behavior that secondary agents are created to run the implants, so with this query we only get the first level of the agent and not the secondary ones*/ @Query( - value = - "select asset.* from assets asset " - + "left join agents agent on asset.asset_id = agent.agent_asset " - + "where agent.agent_parent is null " - + "AND agent.agent_inject is null " - + "AND asset.asset_id = :endpointId", - nativeQuery = true) - Optional findByEndpointIdWithFirstLevelOfAgents(@NotBlank String endpointId); - @Query( "SELECT a FROM Inject i" + " JOIN i.assets a"