Repository: OpenAEV-Platform/openaev (java)

Issue:
[backend] Create the Agents paginated tab
"Agents" tab in inject targets:

- Present a paginated list of all Agents either directly targeted by the inject or targeted indirectly by way of being a member of a targeted asset group and endpoint.



Discussion:

Patch:
diff --git a/openbas-api/src/main/java/io/openbas/service/targets/TargetService.java b/openbas-api/src/main/java/io/openbas/service/targets/TargetService.java
--- a/openbas-api/src/main/java/io/openbas/service/targets/TargetService.java
+++ b/openbas-api/src/main/java/io/openbas/service/targets/TargetService.java
@@ -1,6 +1,7 @@
 package io.openbas.service.targets;
 
 import io.openbas.database.model.*;
+import io.openbas.service.targets.search.AgentTargetSearchAdaptor;
 import io.openbas.service.targets.search.AssetGroupTargetSearchAdaptor;
 import io.openbas.service.targets.search.EndpointTargetSearchAdaptor;
 import io.openbas.service.targets.search.PlayerTargetSearchAdaptor;
@@ -19,6 +20,7 @@ public class TargetService {
   private final AssetGroupTargetSearchAdaptor assetGroupTargetSearchAdaptor;
   private final EndpointTargetSearchAdaptor endpointTargetSearchAdaptor;
   private final TeamTargetSearchAdaptor teamTargetSearchAdaptor;
+  private final AgentTargetSearchAdaptor agentTargetSearchAdaptor;
   private final PlayerTargetSearchAdaptor playerTargetSearchAdaptor;
 
   public Page<InjectTarget> searchTargets(
@@ -36,6 +38,7 @@ public Page<InjectTarget> searchTargets(
       case ASSETS -> endpointTargetSearchAdaptor.search(input, inject);
       case TEAMS -> teamTargetSearchAdaptor.search(input, inject);
       case PLAYERS -> playerTargetSearchAdaptor.search(input, inject);
+      case AGENT -> agentTargetSearchAdaptor.search(input, inject);
       default -> throw new IllegalArgumentException("Unsupported target type: " + injectTargetType);
     };
   }
@@ -47,6 +50,7 @@ public List<FilterUtilsJpa.Option> getTargetOptions(
       case ASSETS -> endpointTargetSearchAdaptor.getOptionsForInject(inject, textSearch);
       case TEAMS -> teamTargetSearchAdaptor.getOptionsForInject(inject, textSearch);
       case PLAYERS -> playerTargetSearchAdaptor.getOptionsForInject(inject, textSearch);
+      case AGENT -> agentTargetSearchAdaptor.getOptionsForInject(inject, textSearch);
       default -> throw new IllegalArgumentException("Unsupported target type: " + targetType);
     };
   }
@@ -57,6 +61,7 @@ public List<FilterUtilsJpa.Option> getTargetOptionsByIds(
       case ASSETS_GROUPS -> assetGroupTargetSearchAdaptor.getOptionsByIds(ids);
       case ASSETS -> endpointTargetSearchAdaptor.getOptionsByIds(ids);
       case TEAMS -> teamTargetSearchAdaptor.getOptionsByIds(ids);
+      case AGENT -> agentTargetSearchAdaptor.getOptionsByIds(ids);
       case PLAYERS -> playerTargetSearchAdaptor.getOptionsByIds(ids);
       default -> throw new IllegalArgumentException("Unsupported target type: " + targetType);
     };
diff --git a/openbas-api/src/main/java/io/openbas/service/targets/search/AgentTargetSearchAdaptor.java b/openbas-api/src/main/java/io/openbas/service/targets/search/AgentTargetSearchAdaptor.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/service/targets/search/AgentTargetSearchAdaptor.java
@@ -0,0 +1,125 @@
+package io.openbas.service.targets.search;
+
+import static io.openbas.utils.pagination.PaginationUtils.buildPaginationJPA;
+
+import io.openbas.database.model.*;
+import io.openbas.database.repository.AgentRepository;
+import io.openbas.service.InjectExpectationService;
+import io.openbas.service.targets.search.specifications.SearchSpecificationUtils;
+import io.openbas.utils.AtomicTestingUtils;
+import io.openbas.utils.FilterUtilsJpa;
+import io.openbas.utils.pagination.SearchPaginationInput;
+import java.util.List;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class AgentTargetSearchAdaptor extends SearchAdaptorBase {
+  private final AgentRepository agentRepository;
+  private final InjectExpectationService injectExpectationService;
+  private final SearchSpecificationUtils<Agent> specificationUtils;
+
+  private final List<String> joinPath = List.of("assets", "agents");
+
+  public AgentTargetSearchAdaptor(
+      AgentRepository agentRepository,
+      InjectExpectationService injectExpectationService,
+      SearchSpecificationUtils<Agent> specificationUtils) {
+    this.agentRepository = agentRepository;
+    this.injectExpectationService = injectExpectationService;
+    this.specificationUtils = specificationUtils;
+    // field name translations
+    this.fieldTranslations.put("target_name", "agent_executed_by_user");
+    this.fieldTranslations.put("target_endpoint", "agent_asset");
+  }
+
+  @Override
+  public Page<InjectTarget> search(SearchPaginationInput input, Inject scopedInject) {
+
+    Specification<Agent> memberOfAssetGroupSpec =
+        specificationUtils.compileSpecificationForAssetGroupMembership(
+            scopedInject, input, joinPath);
+
+    Specification<Agent> memberOfAnyTargetGroupSpec =
+        specificationUtils.compileSpecificationForAssetGroupMembership(
+            scopedInject,
+            SearchPaginationInput.builder().filterGroup(new Filters.FilterGroup()).build(),
+            joinPath);
+
+    Specification<Agent> tagsSpec = specificationUtils.compileSpecificationForTags(input, joinPath);
+
+    SearchPaginationInput translatedInput = this.translate(input, scopedInject);
+
+    Page<Agent> eps =
+        buildPaginationJPA(
+            (Specification<Agent> specification, Pageable pageable) -> {
+              if (Filters.FilterMode.and.equals(input.getFilterGroup().getMode())) {
+                Specification<Agent> finalSpec = memberOfAssetGroupSpec.and(specification);
+                if (tagsSpec != null) {
+                  finalSpec = tagsSpec.and(finalSpec);
+                }
+                return this.agentRepository.findAll(finalSpec, pageable);
+              }
+              Specification<Agent> finalSpec =
+                  memberOfAssetGroupSpec.or(specification.and(memberOfAnyTargetGroupSpec));
+              if (tagsSpec != null) {
+                finalSpec = tagsSpec.or(finalSpec);
+              }
+              return this.agentRepository.findAll(finalSpec, pageable);
+            },
+            translatedInput,
+            Agent.class);
+
+    return new PageImpl<>(
+        eps.getContent().stream()
+            .map(endpoint -> convertFromAgent(endpoint, scopedInject))
+            .toList(),
+        eps.getPageable(),
+        eps.getTotalElements());
+  }
+
+  @Override
+  public List<FilterUtilsJpa.Option> getOptionsForInject(Inject scopedInject, String textSearch) {
+    log.info(
+        "AgentTargetSearchAdaptor.getOptionsForInject: this method is stubbed, as there are no current filters on agent options.");
+    return List.of();
+  }
+
+  @Override
+  public List<FilterUtilsJpa.Option> getOptionsByIds(List<String> ids) {
+    log.info(
+        "AgentTargetSearchAdaptor.getOptionsByIds: this method is stubbed, as there are no current filters on agent options.");
+    return List.of();
+  }
+
+  private InjectTarget convertFromAgent(Agent agent, Inject inject) {
+    InjectTarget target =
+        new AgentTarget(
+            agent.getId(),
+            agent.getTargetDisplayName(),
+            Set.of(),
+            agent.getAsset().getId(),
+            agent.getExecutor().getType());
+
+    List<AtomicTestingUtils.ExpectationResultsByType> results =
+        AtomicTestingUtils.getExpectationResultByTypes(
+            injectExpectationService.findMergedExpectationsByInjectAndTargetAndTargetType(
+                inject.getId(), target.getId(), target.getTargetType()));
+
+    for (AtomicTestingUtils.ExpectationResultsByType result : results) {
+      switch (result.type()) {
+        case DETECTION -> target.setTargetDetectionStatus(result.avgResult());
+        case PREVENTION -> target.setTargetPreventionStatus(result.avgResult());
+        case HUMAN_RESPONSE -> target.setTargetHumanResponseStatus(result.avgResult());
+      }
+    }
+
+    return target;
+  }
+}
diff --git a/openbas-api/src/main/java/io/openbas/service/targets/search/EndpointTargetSearchAdaptor.java b/openbas-api/src/main/java/io/openbas/service/targets/search/EndpointTargetSearchAdaptor.java
--- a/openbas-api/src/main/java/io/openbas/service/targets/search/EndpointTargetSearchAdaptor.java
+++ b/openbas-api/src/main/java/io/openbas/service/targets/search/EndpointTargetSearchAdaptor.java
@@ -1,19 +1,19 @@
 package io.openbas.service.targets.search;
 
+import static io.openbas.helper.StreamHelper.fromIterable;
 import static io.openbas.utils.pagination.PaginationUtils.buildPaginationJPA;
 
 import io.openbas.database.model.*;
 import io.openbas.database.repository.EndpointRepository;
 import io.openbas.service.InjectExpectationService;
-import io.openbas.service.targets.search.specifications.ExcludeMembersOfAssetGroupsSpecification;
 import io.openbas.service.targets.search.specifications.IncludeDirectEndpointTargetsSpecification;
 import io.openbas.service.targets.search.specifications.IncludeMembersOfAssetGroupsSpecification;
+import io.openbas.service.targets.search.specifications.SearchSpecificationUtils;
 import io.openbas.utils.AtomicTestingUtils;
 import io.openbas.utils.FilterUtilsJpa;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import java.util.List;
 import java.util.stream.Collectors;
-import org.apache.commons.lang3.NotImplementedException;
 import org.jetbrains.annotations.NotNull;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
@@ -25,25 +25,25 @@
 public class EndpointTargetSearchAdaptor extends SearchAdaptorBase {
   private final EndpointRepository endpointRepository;
   private final InjectExpectationService injectExpectationService;
-  private final IncludeMembersOfAssetGroupsSpecification includeMembersOfAssetGroupsSpecification;
-  private final ExcludeMembersOfAssetGroupsSpecification excludeMembersOfAssetGroupsSpecification;
-  private final IncludeDirectEndpointTargetsSpecification includeDirectEndpointTargetsSpecification;
+  private final SearchSpecificationUtils<Endpoint> searchSpecificationUtils;
+  private final IncludeMembersOfAssetGroupsSpecification<Endpoint>
+      includeMembersOfAssetGroupsSpecification;
+  private final IncludeDirectEndpointTargetsSpecification<Endpoint>
+      includeDirectEndpointTargetsSpecification;
 
-  private record AssetGroupSplit(
-      List<AssetGroup> includedAssetGroups,
-      List<AssetGroup> excludedAssetGroups,
-      Filters.FilterOperator filterOperator) {}
+  private final List<String> joinPath = List.of("assets");
 
   public EndpointTargetSearchAdaptor(
       EndpointRepository endpointRepository,
       InjectExpectationService injectExpectationService,
-      IncludeMembersOfAssetGroupsSpecification includeMembersOfAssetGroupsSpecification,
-      ExcludeMembersOfAssetGroupsSpecification excludeMembersOfAssetGroupsSpecification,
-      IncludeDirectEndpointTargetsSpecification includeDirectEndpointTargetsSpecification) {
+      SearchSpecificationUtils<Endpoint> searchSpecificationUtils,
+      IncludeMembersOfAssetGroupsSpecification<Endpoint> includeMembersOfAssetGroupsSpecification,
+      IncludeDirectEndpointTargetsSpecification<Endpoint>
+          includeDirectEndpointTargetsSpecification) {
     this.endpointRepository = endpointRepository;
     this.injectExpectationService = injectExpectationService;
+    this.searchSpecificationUtils = searchSpecificationUtils;
     this.includeMembersOfAssetGroupsSpecification = includeMembersOfAssetGroupsSpecification;
-    this.excludeMembersOfAssetGroupsSpecification = excludeMembersOfAssetGroupsSpecification;
     this.includeDirectEndpointTargetsSpecification = includeDirectEndpointTargetsSpecification;
     // field name translations
     this.fieldTranslations.put("target_name", "asset_name");
@@ -52,35 +52,15 @@ public EndpointTargetSearchAdaptor(
 
   @Override
   public Page<InjectTarget> search(SearchPaginationInput input, @NotNull Inject scopedInject) {
-    AssetGroupSplit split = determineAssetGroupSplit(scopedInject, input);
-
     Specification<Endpoint> overallSpec =
-        switch (split.filterOperator) {
-          case null ->
-              includeMembersOfAssetGroupsSpecification
-                  .buildSpecification(
-                      split.includedAssetGroups.stream().map(AssetGroup::getId).toList())
-                  .or(includeDirectEndpointTargetsSpecification.buildSpecification(scopedInject));
-
-          case contains, not_empty ->
-              includeMembersOfAssetGroupsSpecification.buildSpecification(
-                  split.includedAssetGroups.stream().map(AssetGroup::getId).toList());
-
-          case empty ->
-              excludeMembersOfAssetGroupsSpecification
-                  .buildSpecification(
-                      split.excludedAssetGroups.stream().map(AssetGroup::getId).toList())
-                  .and(includeDirectEndpointTargetsSpecification.buildSpecification(scopedInject));
-          case not_contains ->
-              includeMembersOfAssetGroupsSpecification
-                  .buildSpecification(
-                      split.includedAssetGroups.stream().map(AssetGroup::getId).toList())
-                  .and(
-                      excludeMembersOfAssetGroupsSpecification.buildSpecification(
-                          split.excludedAssetGroups.stream().map(AssetGroup::getId).toList()))
-                  .or(includeDirectEndpointTargetsSpecification.buildSpecification(scopedInject));
-          default -> throw new IllegalArgumentException();
-        };
+        searchSpecificationUtils.compileSpecificationForAssetGroupMembership(
+            scopedInject, input, joinPath);
+
+    Specification<Endpoint> memberOfAnyTargetGroupSpec =
+        searchSpecificationUtils.compileSpecificationForAssetGroupMembership(
+            scopedInject,
+            SearchPaginationInput.builder().filterGroup(new Filters.FilterGroup()).build(),
+            joinPath);
 
     SearchPaginationInput translatedInput = this.translate(input, scopedInject);
 
@@ -90,7 +70,8 @@ public Page<InjectTarget> search(SearchPaginationInput input, @NotNull Inject sc
               if (Filters.FilterMode.and.equals(input.getFilterGroup().getMode())) {
                 return this.endpointRepository.findAll(overallSpec.and(specification), pageable);
               }
-              return this.endpointRepository.findAll(overallSpec.or(specification), pageable);
+              return this.endpointRepository.findAll(
+                  overallSpec.or(specification.and(memberOfAnyTargetGroupSpec)), pageable);
             },
             translatedInput,
             Endpoint.class);
@@ -105,57 +86,28 @@ public Page<InjectTarget> search(SearchPaginationInput input, @NotNull Inject sc
 
   @Override
   public List<FilterUtilsJpa.Option> getOptionsForInject(Inject scopedInject, String textSearch) {
-    throw new NotImplementedException("Implement when needed by the Agents paginated tab");
+    Specification<Endpoint> spec =
+        includeMembersOfAssetGroupsSpecification
+            .buildSpecification(
+                scopedInject.getAssetGroups().stream().map(AssetGroup::getId).toList(), joinPath)
+            .or(
+                includeDirectEndpointTargetsSpecification.buildSpecification(
+                    scopedInject, joinPath));
+
+    Specification<Endpoint> nameSpec =
+        (root, query, criteriaBuilder) ->
+            criteriaBuilder.like(root.get("name"), "%" + textSearch + "%");
+
+    return this.endpointRepository.findAll(spec.and(nameSpec)).stream()
+        .map(ep -> new FilterUtilsJpa.Option(ep.getId(), ep.getName()))
+        .toList();
   }
 
   @Override
   public List<FilterUtilsJpa.Option> getOptionsByIds(List<String> ids) {
-    throw new NotImplementedException("Implement when needed by the Agents paginated tab");
-  }
-
-  private AssetGroupSplit determineAssetGroupSplit(
-      Inject scopedInject, SearchPaginationInput input) {
-    List<AssetGroup> allTargetAssetGroups = scopedInject.getAssetGroups();
-
-    Filters.Filter assetGroupFilter = getAssetGroupFilter(input);
-    if (assetGroupFilter == null) {
-      return new AssetGroupSplit(allTargetAssetGroups, List.of(), null);
-    }
-
-    return switch (assetGroupFilter.getOperator()) {
-      case contains ->
-          new AssetGroupSplit(
-              allTargetAssetGroups.stream()
-                  .filter(ag -> assetGroupFilter.getValues().contains(ag.getId()))
-                  .toList(),
-              List.of(),
-              assetGroupFilter.getOperator());
-      case not_empty ->
-          new AssetGroupSplit(allTargetAssetGroups, List.of(), assetGroupFilter.getOperator());
-      case empty ->
-          new AssetGroupSplit(List.of(), allTargetAssetGroups, assetGroupFilter.getOperator());
-      case not_contains ->
-          new AssetGroupSplit(
-              allTargetAssetGroups.stream()
-                  .filter(ag -> !assetGroupFilter.getValues().contains(ag.getId()))
-                  .toList(),
-              allTargetAssetGroups.stream()
-                  .filter(ag -> assetGroupFilter.getValues().contains(ag.getId()))
-                  .toList(),
-              assetGroupFilter.getOperator());
-      default ->
-          throw new IllegalArgumentException("Unknown operator: " + assetGroupFilter.getOperator());
-    };
-  }
-
-  private Filters.Filter getAssetGroupFilter(SearchPaginationInput input) {
-    Filters.FilterGroup filterGroup = input.getFilterGroup();
-    String key = "target_asset_groups";
-
-    return filterGroup.getFilters().stream()
-        .filter(filter -> key.equals(filter.getKey()))
-        .findFirst()
-        .orElse(null);
+    return fromIterable(this.endpointRepository.findAllById(ids)).stream()
+        .map(ep -> new FilterUtilsJpa.Option(ep.getId(), ep.getName()))
+        .toList();
   }
 
   private InjectTarget convertFromEndpoint(Endpoint endpoint, Inject inject) {
diff --git a/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/ExcludeMembersOfAssetGroupsSpecification.java b/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/ExcludeMembersOfAssetGroupsSpecification.java
--- a/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/ExcludeMembersOfAssetGroupsSpecification.java
+++ b/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/ExcludeMembersOfAssetGroupsSpecification.java
@@ -1,33 +1,35 @@
 package io.openbas.service.targets.search.specifications;
 
+import static io.openbas.service.targets.search.specifications.SearchSpecificationUtils.compileFilterGroupsWithOR;
+import static io.openbas.service.targets.search.specifications.SearchSpecificationUtils.createJoinedFrom;
+
 import io.openbas.database.model.*;
 import io.openbas.database.repository.AssetGroupRepository;
-import io.openbas.utils.FilterUtilsJpa;
-import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.From;
 import jakarta.persistence.criteria.Root;
 import jakarta.persistence.criteria.Subquery;
 import java.util.List;
 import lombok.RequiredArgsConstructor;
-import org.jetbrains.annotations.NotNull;
 import org.springframework.data.jpa.domain.Specification;
 import org.springframework.stereotype.Component;
 
 @Component
 @RequiredArgsConstructor
-public class ExcludeMembersOfAssetGroupsSpecification {
+public class ExcludeMembersOfAssetGroupsSpecification<T> {
   private final AssetGroupRepository assetGroupRepository;
 
-  public Specification<Endpoint> buildSpecification(List<String> assetGroupIds) {
-    Specification<Endpoint> dynamicFiltersSpec = getDynamicFilterSpecification(assetGroupIds);
-    Specification<Endpoint> transitiveMembershipSpec =
-        getTransitiveTargetingSpecification(assetGroupIds);
+  public Specification<T> buildSpecification(List<String> assetGroupIds, List<String> joinPath) {
+    Specification<T> dynamicFiltersSpec = getDynamicFilterSpecification(assetGroupIds, joinPath);
+    Specification<T> transitiveMembershipSpec =
+        getTransitiveTargetingSpecification(assetGroupIds, joinPath);
 
     return dynamicFiltersSpec == null
         ? transitiveMembershipSpec
         : dynamicFiltersSpec.and(transitiveMembershipSpec);
   }
 
-  private Specification<Endpoint> getDynamicFilterSpecification(List<String> assetGroupIds) {
+  private Specification<T> getDynamicFilterSpecification(
+      List<String> assetGroupIds, List<String> joinPath) {
     Specification<Endpoint> positiveSpec =
         compileFilterGroupsWithOR(
             assetGroupRepository.rawDynamicFiltersByAssetGroupIds(assetGroupIds).stream()
@@ -35,48 +37,39 @@ private Specification<Endpoint> getDynamicFilterSpecification(List<String> asset
                 .filter(fg -> !fg.getFilters().isEmpty())
                 .toList());
 
+    if (positiveSpec == null) {
+      return null;
+    }
+
     return ((root, query, criteriaBuilder) -> {
-      if (positiveSpec == null) {
-        return null;
-      }
       Subquery<Integer> subQuery = query.subquery(Integer.class);
-      Root<Asset> assetTable = subQuery.from(Asset.class);
+      Root<Endpoint> assetTable = subQuery.from(Endpoint.class);
+      From<?, ?> finalFrom =
+          createJoinedFrom(
+              assetTable, joinPath.stream().filter(path -> !"assets".equals(path)).toList());
 
       subQuery
           .select(criteriaBuilder.literal(1))
           .where(
-              positiveSpec.toPredicate(root, query, criteriaBuilder),
+              positiveSpec.toPredicate(assetTable, query, criteriaBuilder),
               criteriaBuilder.equal(
-                  assetTable.get("id"), query.getRoots().stream().findFirst().get().get("id")));
+                  finalFrom.get("id"), query.getRoots().stream().findFirst().get().get("id")));
       return criteriaBuilder.exists(subQuery).not();
     });
   }
 
-  private Specification<Endpoint> compileFilterGroupsWithOR(
-      @NotNull List<Filters.FilterGroup> filterGroups) {
-    Specification<Endpoint> result = null;
-    for (Filters.FilterGroup filterGroup : filterGroups) {
-      Specification<Endpoint> converted = FilterUtilsJpa.computeFilterGroupJpa(filterGroup);
-      if (result == null) {
-        result = converted;
-        continue;
-      }
-      result = result.or(converted);
-    }
-    return result;
-  }
-
-  private Specification<Endpoint> getTransitiveTargetingSpecification(List<String> assetGroupIds) {
+  private Specification<T> getTransitiveTargetingSpecification(
+      List<String> assetGroupIds, List<String> joinPath) {
     return (root, query, criteriaBuilder) -> {
       Subquery<Integer> subQuery = query.subquery(Integer.class);
       Root<AssetGroup> assetGroupTable = subQuery.from(AssetGroup.class);
-      Join<AssetGroup, Asset> assetJoin = assetGroupTable.join("assets");
+      From<?, ?> finalFrom = createJoinedFrom(assetGroupTable, joinPath);
 
       subQuery
           .select(criteriaBuilder.literal(1))
           .where(
               criteriaBuilder.equal(
-                  assetJoin.get("id"), query.getRoots().stream().findFirst().get().get("id")),
+                  finalFrom.get("id"), query.getRoots().stream().findFirst().get().get("id")),
               assetGroupTable.get("id").in(assetGroupIds.stream().toList()));
       return criteriaBuilder.exists(subQuery).not();
     };
diff --git a/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/IncludeDirectEndpointTargetsSpecification.java b/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/IncludeDirectEndpointTargetsSpecification.java
--- a/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/IncludeDirectEndpointTargetsSpecification.java
+++ b/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/IncludeDirectEndpointTargetsSpecification.java
@@ -1,34 +1,37 @@
 package io.openbas.service.targets.search.specifications;
 
-import io.openbas.database.model.Asset;
-import io.openbas.database.model.Endpoint;
+import static io.openbas.service.targets.search.specifications.SearchSpecificationUtils.createJoinedFrom;
+
 import io.openbas.database.model.Inject;
-import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.From;
 import jakarta.persistence.criteria.Root;
 import jakarta.persistence.criteria.Subquery;
+import java.util.List;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.jpa.domain.Specification;
 import org.springframework.stereotype.Component;
 
 @Component
 @RequiredArgsConstructor
-public class IncludeDirectEndpointTargetsSpecification {
-  public Specification<Endpoint> buildSpecification(Inject scopedInject) {
-    return getDirectTargetingSpecification(scopedInject);
+public class IncludeDirectEndpointTargetsSpecification<T> {
+
+  public Specification<T> buildSpecification(Inject scopedInject, List<String> joinPath) {
+    return getDirectTargetingSpecification(scopedInject, joinPath);
   }
 
-  private Specification<Endpoint> getDirectTargetingSpecification(Inject scopedInject) {
+  private Specification<T> getDirectTargetingSpecification(
+      Inject scopedInject, List<String> joinPath) {
     return (root, query, criteriaBuilder) -> {
       Subquery<Integer> subQuery = query.subquery(Integer.class);
       Root<Inject> injectTable = subQuery.from(Inject.class);
-      Join<Inject, Asset> assetJoin = injectTable.join("assets");
+      From<?, ?> finalFrom = createJoinedFrom(injectTable, joinPath);
 
       subQuery
           .select(criteriaBuilder.literal(1))
           .where(
               criteriaBuilder.equal(injectTable.get("id"), scopedInject.getId()),
               criteriaBuilder.equal(
-                  assetJoin.get("id"), query.getRoots().stream().findFirst().get().get("id")));
+                  finalFrom.get("id"), query.getRoots().stream().findFirst().get().get("id")));
       return criteriaBuilder.exists(subQuery);
     };
   }
diff --git a/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/IncludeMembersOfAssetGroupsSpecification.java b/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/IncludeMembersOfAssetGroupsSpecification.java
--- a/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/IncludeMembersOfAssetGroupsSpecification.java
+++ b/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/IncludeMembersOfAssetGroupsSpecification.java
@@ -1,65 +1,75 @@
 package io.openbas.service.targets.search.specifications;
 
+import static io.openbas.service.targets.search.specifications.SearchSpecificationUtils.compileFilterGroupsWithOR;
+import static io.openbas.service.targets.search.specifications.SearchSpecificationUtils.createJoinedFrom;
+
 import io.openbas.database.model.*;
 import io.openbas.database.repository.AssetGroupRepository;
-import io.openbas.utils.FilterUtilsJpa;
-import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.From;
 import jakarta.persistence.criteria.Root;
 import jakarta.persistence.criteria.Subquery;
 import java.util.List;
 import lombok.RequiredArgsConstructor;
-import org.jetbrains.annotations.NotNull;
 import org.springframework.data.jpa.domain.Specification;
 import org.springframework.stereotype.Component;
 
 @Component
 @RequiredArgsConstructor
-public class IncludeMembersOfAssetGroupsSpecification {
+public class IncludeMembersOfAssetGroupsSpecification<T> {
   private final AssetGroupRepository assetGroupRepository;
 
-  public Specification<Endpoint> buildSpecification(List<String> assetGroupIds) {
-    Specification<Endpoint> dynamicFiltersSpec = getDynamicFilterSpecification(assetGroupIds);
-    Specification<Endpoint> transitiveMembershipSpec =
-        getTransitiveTargetingSpecification(assetGroupIds);
+  public Specification<T> buildSpecification(List<String> assetGroupIds, List<String> joinPath) {
+    Specification<T> dynamicFiltersSpec = getDynamicFilterSpecification(assetGroupIds, joinPath);
+    Specification<T> transitiveMembershipSpec =
+        getTransitiveTargetingSpecification(assetGroupIds, joinPath);
 
     return dynamicFiltersSpec == null
         ? transitiveMembershipSpec
         : dynamicFiltersSpec.or(transitiveMembershipSpec);
   }
 
-  private Specification<Endpoint> getDynamicFilterSpecification(List<String> assetGroupIds) {
-    return compileFilterGroupsWithOR(
-        assetGroupRepository.rawDynamicFiltersByAssetGroupIds(assetGroupIds).stream()
-            .map(df -> df.getAssetGroupDynamicFilter())
-            .filter(fg -> !fg.getFilters().isEmpty())
-            .toList());
-  }
+  private Specification<T> getDynamicFilterSpecification(
+      List<String> assetGroupIds, List<String> joinPath) {
+    Specification<Endpoint> positiveSpec =
+        compileFilterGroupsWithOR(
+            assetGroupRepository.rawDynamicFiltersByAssetGroupIds(assetGroupIds).stream()
+                .map(df -> df.getAssetGroupDynamicFilter())
+                .filter(fg -> !fg.getFilters().isEmpty())
+                .toList());
 
-  private Specification<Endpoint> compileFilterGroupsWithOR(
-      @NotNull List<Filters.FilterGroup> filterGroups) {
-    Specification<Endpoint> result = null;
-    for (Filters.FilterGroup filterGroup : filterGroups) {
-      Specification<Endpoint> converted = FilterUtilsJpa.computeFilterGroupJpa(filterGroup);
-      if (result == null) {
-        result = converted;
-        continue;
-      }
-      result = result.or(converted);
+    if (positiveSpec == null) {
+      return null;
     }
-    return result;
+
+    return ((root, query, criteriaBuilder) -> {
+      Subquery<Integer> subQuery = query.subquery(Integer.class);
+      Root<Endpoint> assetTable = subQuery.from(Endpoint.class);
+      From<?, ?> finalJoin =
+          createJoinedFrom(
+              assetTable, joinPath.stream().filter(path -> !"assets".equals(path)).toList());
+
+      subQuery
+          .select(criteriaBuilder.literal(1))
+          .where(
+              positiveSpec.toPredicate(assetTable, query, criteriaBuilder),
+              criteriaBuilder.equal(
+                  finalJoin.get("id"), query.getRoots().stream().findFirst().get().get("id")));
+      return criteriaBuilder.exists(subQuery);
+    });
   }
 
-  private Specification<Endpoint> getTransitiveTargetingSpecification(List<String> assetGroupIds) {
+  private Specification<T> getTransitiveTargetingSpecification(
+      List<String> assetGroupIds, List<String> joinPath) {
     return (root, query, criteriaBuilder) -> {
       Subquery<Integer> subQuery = query.subquery(Integer.class);
       Root<AssetGroup> assetGroupTable = subQuery.from(AssetGroup.class);
-      Join<AssetGroup, Asset> assetJoin = assetGroupTable.join("assets");
+      From<?, ?> finalJoin = createJoinedFrom(assetGroupTable, joinPath);
 
       subQuery
           .select(criteriaBuilder.literal(1))
           .where(
               criteriaBuilder.equal(
-                  assetJoin.get("id"), query.getRoots().stream().findFirst().get().get("id")),
+                  finalJoin.get("id"), query.getRoots().stream().findFirst().get().get("id")),
               assetGroupTable.get("id").in(assetGroupIds));
       return criteriaBuilder.exists(subQuery);
     };
diff --git a/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/SearchSpecificationUtils.java b/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/SearchSpecificationUtils.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/service/targets/search/specifications/SearchSpecificationUtils.java
@@ -0,0 +1,187 @@
+package io.openbas.service.targets.search.specifications;
+
+import io.openbas.database.model.*;
+import io.openbas.utils.FilterUtilsJpa;
+import io.openbas.utils.pagination.SearchPaginationInput;
+import jakarta.persistence.criteria.*;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class SearchSpecificationUtils<T> {
+  private final IncludeDirectEndpointTargetsSpecification<T>
+      includeDirectEndpointTargetsSpecification;
+  private final IncludeMembersOfAssetGroupsSpecification<T>
+      includeMembersOfAssetGroupsSpecification;
+  private final ExcludeMembersOfAssetGroupsSpecification<T>
+      excludeMembersOfAssetGroupsSpecification;
+
+  public record AssetGroupSplit(
+      List<AssetGroup> includedAssetGroups,
+      List<AssetGroup> excludedAssetGroups,
+      Filters.FilterOperator filterOperator) {}
+
+  public Specification<T> compileSpecificationForTags(
+      SearchPaginationInput input, List<String> joinPath) {
+
+    Filters.Filter tagsFilter = getTagsFilter(input);
+    if (tagsFilter == null) {
+      return null;
+    }
+
+    Filters.FilterGroup filterConfig = getFilterAsFilterGroup(input.getFilterGroup(), tagsFilter);
+    Specification<Endpoint> spec = FilterUtilsJpa.computeFilterGroupJpa(filterConfig);
+
+    return ((root, query, criteriaBuilder) -> {
+      Subquery<Integer> subQuery = query.subquery(Integer.class);
+      Root<Endpoint> assetTable = subQuery.from(Endpoint.class);
+      From<?, ?> finalJoin =
+          createJoinedFrom(
+              assetTable, joinPath.stream().filter(path -> !"assets".equals(path)).toList());
+
+      subQuery
+          .select(criteriaBuilder.literal(1))
+          .where(
+              spec.toPredicate(assetTable, query, criteriaBuilder),
+              criteriaBuilder.equal(
+                  finalJoin.get("id"), query.getRoots().stream().findFirst().get().get("id")));
+      return criteriaBuilder.exists(subQuery);
+    });
+  }
+
+  private Filters.FilterGroup getFilterAsFilterGroup(
+      Filters.FilterGroup filterGroup, Filters.Filter filter) {
+    Filters.FilterGroup fg = new Filters.FilterGroup();
+    fg.setMode(filterGroup.getMode());
+    if (filter != null) {
+      Filters.Filter newFilter = new Filters.Filter();
+      newFilter.setOperator(filter.getOperator());
+      newFilter.setKey("asset_tags");
+      newFilter.setOperator(filter.getOperator());
+      newFilter.setValues(filter.getValues());
+      fg.setFilters(List.of(newFilter));
+    } else {
+      fg.setFilters(List.of());
+    }
+    return fg;
+  }
+
+  private Filters.Filter getTagsFilter(SearchPaginationInput input) {
+    Filters.FilterGroup filterGroup = input.getFilterGroup();
+    String key = "target_tags";
+
+    return filterGroup.getFilters().stream()
+        .filter(filter -> key.equals(filter.getKey()))
+        .findFirst()
+        .orElse(null);
+  }
+
+  public Specification<T> compileSpecificationForAssetGroupMembership(
+      Inject scopedInject, SearchPaginationInput input, List<String> joinPath) {
+    AssetGroupSplit split = determineAssetGroupSplit(scopedInject, input);
+    return switch (split.filterOperator) {
+      case null ->
+          includeMembersOfAssetGroupsSpecification
+              .buildSpecification(
+                  split.includedAssetGroups.stream().map(AssetGroup::getId).toList(), joinPath)
+              .or(
+                  includeDirectEndpointTargetsSpecification.buildSpecification(
+                      scopedInject, joinPath));
+
+      case contains, not_empty ->
+          includeMembersOfAssetGroupsSpecification.buildSpecification(
+              split.includedAssetGroups.stream().map(AssetGroup::getId).toList(), joinPath);
+
+      case empty ->
+          excludeMembersOfAssetGroupsSpecification
+              .buildSpecification(
+                  split.excludedAssetGroups.stream().map(AssetGroup::getId).toList(), joinPath)
+              .and(
+                  includeDirectEndpointTargetsSpecification.buildSpecification(
+                      scopedInject, joinPath));
+      case not_contains ->
+          includeMembersOfAssetGroupsSpecification
+              .buildSpecification(
+                  split.includedAssetGroups.stream().map(AssetGroup::getId).toList(), joinPath)
+              .and(
+                  excludeMembersOfAssetGroupsSpecification.buildSpecification(
+                      split.excludedAssetGroups.stream().map(AssetGroup::getId).toList(), joinPath))
+              .or(
+                  includeDirectEndpointTargetsSpecification.buildSpecification(
+                      scopedInject, joinPath));
+      default -> throw new IllegalArgumentException();
+    };
+  }
+
+  private AssetGroupSplit determineAssetGroupSplit(
+      Inject scopedInject, SearchPaginationInput input) {
+    List<AssetGroup> allTargetAssetGroups = scopedInject.getAssetGroups();
+
+    Filters.Filter assetGroupFilter = getAssetGroupFilter(input);
+    if (assetGroupFilter == null) {
+      return new AssetGroupSplit(allTargetAssetGroups, List.of(), null);
+    }
+
+    return switch (assetGroupFilter.getOperator()) {
+      case contains ->
+          new AssetGroupSplit(
+              allTargetAssetGroups.stream()
+                  .filter(ag -> assetGroupFilter.getValues().contains(ag.getId()))
+                  .toList(),
+              List.of(),
+              assetGroupFilter.getOperator());
+      case not_empty ->
+          new AssetGroupSplit(allTargetAssetGroups, List.of(), assetGroupFilter.getOperator());
+      case empty ->
+          new AssetGroupSplit(List.of(), allTargetAssetGroups, assetGroupFilter.getOperator());
+      case not_contains ->
+          new AssetGroupSplit(
+              allTargetAssetGroups.stream()
+                  .filter(ag -> !assetGroupFilter.getValues().contains(ag.getId()))
+                  .toList(),
+              allTargetAssetGroups.stream()
+                  .filter(ag -> assetGroupFilter.getValues().contains(ag.getId()))
+                  .toList(),
+              assetGroupFilter.getOperator());
+      default ->
+          throw new IllegalArgumentException("Unknown operator: " + assetGroupFilter.getOperator());
+    };
+  }
+
+  private Filters.Filter getAssetGroupFilter(SearchPaginationInput input) {
+    Filters.FilterGroup filterGroup = input.getFilterGroup();
+    String key = "target_asset_groups";
+
+    return filterGroup.getFilters().stream()
+        .filter(filter -> key.equals(filter.getKey()))
+        .findFirst()
+        .orElse(null);
+  }
+
+  // static
+  public static Specification<Endpoint> compileFilterGroupsWithOR(
+      @NotNull List<Filters.FilterGroup> filterGroups) {
+    Specification<Endpoint> result = null;
+    for (Filters.FilterGroup filterGroup : filterGroups) {
+      Specification<Endpoint> converted = FilterUtilsJpa.computeFilterGroupJpa(filterGroup);
+      if (result == null) {
+        result = converted;
+        continue;
+      }
+      result = result.or(converted);
+    }
+    return result;
+  }
+
+  public static From<?, ?> createJoinedFrom(From<?, ?> root, List<String> joinPath) {
+    From<?, ?> finalJoin = root;
+    for (String path : joinPath) {
+      finalJoin = finalJoin.join(path);
+    }
+    return finalJoin;
+  }
+}
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
--- a/openbas-model/src/main/java/io/openbas/database/model/Agent.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/Agent.java
@@ -2,6 +2,7 @@
 
 import static java.time.Instant.now;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import io.openbas.annotation.Queryable;
@@ -50,7 +51,7 @@ public enum DEPLOYMENT_MODE {
   // ID is UUID by default and external reference for CrowdStrike agent
   private String id = UUID.randomUUID().toString();
 
-  @Queryable(sortable = true)
+  @Queryable(sortable = true, filterable = true, path = "asset.id")
   @ManyToOne
   @JoinColumn(name = "agent_asset")
   @JsonSerialize(using = MonoIdDeserializer.class)
@@ -73,7 +74,7 @@ public enum DEPLOYMENT_MODE {
   @NotNull
   private DEPLOYMENT_MODE deploymentMode;
 
-  @Queryable(sortable = true)
+  @Queryable(sortable = true, filterable = true)
   @Column(name = "agent_executed_by_user")
   @JsonProperty("agent_executed_by_user")
   @NotBlank
@@ -141,6 +142,12 @@ public boolean isActive() {
   @JsonProperty("agent_cleared_at")
   private Instant clearedAt = now();
 
+  @JsonIgnore
+  @Transient
+  public String getTargetDisplayName() {
+    return getExecutedByUser() + " (" + getAsset().getName() + ")";
+  }
+
   @Override
   public int hashCode() {
     return Objects.hash(id);
diff --git a/openbas-model/src/main/java/io/openbas/database/model/AgentTarget.java b/openbas-model/src/main/java/io/openbas/database/model/AgentTarget.java
new file mode 100644
--- /dev/null
+++ b/openbas-model/src/main/java/io/openbas/database/model/AgentTarget.java
@@ -0,0 +1,53 @@
+package io.openbas.database.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.openbas.annotation.Queryable;
+import java.util.Optional;
+import java.util.Set;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.NotImplementedException;
+
+public class AgentTarget extends InjectTarget {
+
+  public AgentTarget(String id, String name, Set<String> tags, String endpoint, String subType) {
+    this.setId(id);
+    this.setName(name);
+    this.setTags(tags);
+    this.setEndpoint(endpoint);
+    this.setTargetType("AGENT");
+    this.subType = subType;
+  }
+
+  @JsonProperty("target_name")
+  @Getter
+  @Setter
+  @Queryable(filterable = true, searchable = true, sortable = true)
+  private String name;
+
+  @JsonIgnore private final String subType;
+
+  @JsonIgnore
+  @JsonProperty("target_asset_groups")
+  @Queryable(searchable = true, filterable = true, dynamicValues = true)
+  public Set<String> getAssetGroups() {
+    // note that it's not possible at the entity level to fetch a complete set of
+    // containing asset groups since dynamic filters do not implement a relationship
+    // between the Asset and AssetGroup entities.
+    // This is currently only dealt with at the API Service level.
+    throw new NotImplementedException(
+        "NOT AVAILABLE; this property exists for exposing a filter only.");
+  }
+
+  @JsonIgnore
+  @Setter
+  @JsonProperty("target_endpoint")
+  @Queryable(searchable = true, filterable = true, dynamicValues = true)
+  private String endpoint;
+
+  @Override
+  protected String getTargetSubtype() {
+    return Optional.ofNullable(this.subType).orElse("Unknown");
+  }
+}
diff --git a/openbas-model/src/main/java/io/openbas/database/model/AssetGroupTarget.java b/openbas-model/src/main/java/io/openbas/database/model/AssetGroupTarget.java
--- a/openbas-model/src/main/java/io/openbas/database/model/AssetGroupTarget.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/AssetGroupTarget.java
@@ -20,6 +20,6 @@ public AssetGroupTarget(String id, String name, Set<String> tags) {
 
   @Override
   protected String getTargetSubtype() {
-    return this.getTargetType();
+    return "N/A";
   }
 }
diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectTarget.java b/openbas-model/src/main/java/io/openbas/database/model/InjectTarget.java
--- a/openbas-model/src/main/java/io/openbas/database/model/InjectTarget.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/InjectTarget.java
@@ -12,12 +12,19 @@
 @Data
 @Schema(
     discriminatorProperty = "target_type",
-    oneOf = {AssetGroupTarget.class, TeamTarget.class, PlayerTarget.class},
+    oneOf = {
+      AssetGroupTarget.class,
+      TeamTarget.class,
+      EndpointTarget.class,
+      AgentTarget.class,
+      PlayerTarget.class
+    },
     discriminatorMapping = {
       @DiscriminatorMapping(value = "ASSETS_GROUPS", schema = AssetGroupTarget.class),
       @DiscriminatorMapping(value = "ASSETS", schema = EndpointTarget.class),
       @DiscriminatorMapping(value = "TEAMS", schema = TeamTarget.class),
-      @DiscriminatorMapping(value = "PLAYERS", schema = PlayerTarget.class)
+      @DiscriminatorMapping(value = "PLAYERS", schema = PlayerTarget.class),
+      @DiscriminatorMapping(value = "AGENT", schema = AgentTarget.class),
     })
 public abstract class InjectTarget {
   @Id
diff --git a/openbas-model/src/main/java/io/openbas/database/model/TeamTarget.java b/openbas-model/src/main/java/io/openbas/database/model/TeamTarget.java
--- a/openbas-model/src/main/java/io/openbas/database/model/TeamTarget.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/TeamTarget.java
@@ -20,6 +20,6 @@ public TeamTarget(String id, String name, Set<String> tags) {
 
   @Override
   protected String getTargetSubtype() {
-    return this.getTargetType();
+    return "N/A";
   }
 }


Test patch:
diff --git a/openbas-api/src/test/java/io/openbas/injector_contract/InjectorContratApiTest.java b/openbas-api/src/test/java/io/openbas/injector_contract/InjectorContratApiTest.java
--- a/openbas-api/src/test/java/io/openbas/injector_contract/InjectorContratApiTest.java
+++ b/openbas-api/src/test/java/io/openbas/injector_contract/InjectorContratApiTest.java
@@ -106,7 +106,7 @@ class FilteringPageOfContracts {
       void given_search_input_with_label_type_should_return_a_page_of_contrats_ignoring_case()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter(
+            PaginationFixture.simpleSearchWithAndOperator(
                 "injector_contract_labels", "multi-recipients", contains);
 
         mvc.perform(
@@ -121,7 +121,7 @@ void given_search_input_with_label_type_should_return_a_page_of_contrats_ignorin
       @Test
       void given_search_input_with_label_should_return_a_page_of_contrats() throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter(
+            PaginationFixture.simpleSearchWithAndOperator(
                 "injector_contract_labels", "Send multi-recipients mail", eq);
 
         mvc.perform(
@@ -137,7 +137,7 @@ void given_search_input_with_label_should_return_a_page_of_contrats() throws Exc
       void given_search_input_with_label_should_return_a_page_of_contrats_ignoring_case()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter(
+            PaginationFixture.simpleSearchWithAndOperator(
                 "injector_contract_labels", "send multi-recipients mail", eq);
 
         mvc.perform(
diff --git a/openbas-api/src/test/java/io/openbas/killChainPhase/KillChainPhaseApiTest.java b/openbas-api/src/test/java/io/openbas/killChainPhase/KillChainPhaseApiTest.java
--- a/openbas-api/src/test/java/io/openbas/killChainPhase/KillChainPhaseApiTest.java
+++ b/openbas-api/src/test/java/io/openbas/killChainPhase/KillChainPhaseApiTest.java
@@ -173,7 +173,7 @@ class FilteringPageOfKillChainPhases {
         given_search_input_with_name_and_equals_operator_should_return_a_page_of_kill_chain_phases()
             throws Exception {
       SearchPaginationInput searchPaginationInput =
-          PaginationFixture.simpleFilter("phase_name", "NAME2", eq);
+          PaginationFixture.simpleSearchWithAndOperator("phase_name", "NAME2", eq);
 
       mvc.perform(
               post("/api/kill_chain_phases/search")
@@ -189,7 +189,7 @@ class FilteringPageOfKillChainPhases {
         given_search_input_with_name_and_contains_operator_should_return_a_page_of_kill_chain_phases()
             throws Exception {
       SearchPaginationInput searchPaginationInput =
-          PaginationFixture.simpleFilter("phase_name", "2", contains);
+          PaginationFixture.simpleSearchWithAndOperator("phase_name", "2", contains);
 
       mvc.perform(
               post("/api/kill_chain_phases/search")
diff --git a/openbas-api/src/test/java/io/openbas/rest/PayloadApiSearchTest.java b/openbas-api/src/test/java/io/openbas/rest/PayloadApiSearchTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/PayloadApiSearchTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/PayloadApiSearchTest.java
@@ -151,7 +151,7 @@ class FilteringPageOfPayloads {
       void given_filter_input_by_name_should_return_a_page_of_payloads_filter_by_name()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("payload_name", "command", contains);
+            PaginationFixture.simpleSearchWithAndOperator("payload_name", "command", contains);
 
         mvc.perform(
                 post(PAYLOAD_URI + "/search")
@@ -166,7 +166,8 @@ void given_filter_input_by_name_should_return_a_page_of_payloads_filter_by_name(
       void given_filter_input_by_platforms_should_return_a_page_of_payloads_filter_by_platforms()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("payload_platforms", valueOf(Linux), contains);
+            PaginationFixture.simpleSearchWithAndOperator(
+                "payload_platforms", valueOf(Linux), contains);
 
         mvc.perform(
                 post(PAYLOAD_URI + "/search")
@@ -181,7 +182,8 @@ void given_filter_input_by_platforms_should_return_a_page_of_payloads_filter_by_
       void given_filter_input_by_source_should_return_a_page_of_payloads_filter_by_source()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("payload_source", valueOf(MANUAL), contains);
+            PaginationFixture.simpleSearchWithAndOperator(
+                "payload_source", valueOf(MANUAL), contains);
 
         mvc.perform(
                 post(PAYLOAD_URI + "/search")
@@ -197,7 +199,8 @@ void given_filter_input_by_source_should_return_a_page_of_payloads_filter_by_sou
           given_filter_input_by_arch_should_return_a_page_of_executable_payloads_filtered_by_architecture()
               throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("payload_execution_arch", valueOf(arm64), contains);
+            PaginationFixture.simpleSearchWithAndOperator(
+                "payload_execution_arch", valueOf(arm64), contains);
 
         mvc.perform(
                 post(PAYLOAD_URI + "/search")
diff --git a/openbas-api/src/test/java/io/openbas/rest/ScenarioInjectApiSearchTest.java b/openbas-api/src/test/java/io/openbas/rest/ScenarioInjectApiSearchTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/ScenarioInjectApiSearchTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/ScenarioInjectApiSearchTest.java
@@ -166,7 +166,7 @@ class FilteringPageOfInjects {
       void given_filter_input_by_name_should_return_a_page_of_injects_filter_by_name()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("inject_title", "email", contains);
+            PaginationFixture.simpleSearchWithAndOperator("inject_title", "email", contains);
 
         mvc.perform(
                 post(SCENARIO_URI + "/" + SCENARIO_ID + "/injects/simple")
@@ -182,7 +182,7 @@ void given_filter_input_by_name_should_return_a_page_of_injects_filter_by_name()
           given_filter_input_by_injector_contract_should_return_a_page_of_injects_filter_by_injector_contract()
               throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter(
+            PaginationFixture.simpleSearchWithAndOperator(
                 "inject_injector_contract", EMAIL_INJECTOR_CONTRACT_ID, contains);
 
         mvc.perform(
diff --git a/openbas-api/src/test/java/io/openbas/rest/asset_group/AssetGroupApiSearchTest.java b/openbas-api/src/test/java/io/openbas/rest/asset_group/AssetGroupApiSearchTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/asset_group/AssetGroupApiSearchTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/asset_group/AssetGroupApiSearchTest.java
@@ -141,7 +141,7 @@ class FilteringPageOfAssetGroups {
       void given_filter_input_by_name_should_return_a_page_of_asset_groups_filter_by_name()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("asset_group_name", "Group", contains);
+            PaginationFixture.simpleSearchWithAndOperator("asset_group_name", "Group", contains);
 
         mvc.perform(
                 post(ASSET_GROUP_URI + "/search")
@@ -156,7 +156,7 @@ void given_filter_input_by_name_should_return_a_page_of_asset_groups_filter_by_n
       void given_filter_input_by_sttaus_should_return_a_page_of_asset_groups_filter_by_description()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter(
+            PaginationFixture.simpleSearchWithAndOperator(
                 "asset_group_description", valueOf("wrong"), not_contains);
 
         mvc.perform(
diff --git a/openbas-api/src/test/java/io/openbas/rest/exercise/ExerciseApiSearchTest.java b/openbas-api/src/test/java/io/openbas/rest/exercise/ExerciseApiSearchTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/exercise/ExerciseApiSearchTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/exercise/ExerciseApiSearchTest.java
@@ -144,7 +144,7 @@ class FilteringPageOfExercises {
       void given_filter_input_by_name_should_return_a_page_of_exercises_filter_by_name()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("exercise_name", "Crisis", contains);
+            PaginationFixture.simpleSearchWithAndOperator("exercise_name", "Crisis", contains);
 
         mvc.perform(
                 post(EXERCISE_URI + "/search")
@@ -159,7 +159,8 @@ void given_filter_input_by_name_should_return_a_page_of_exercises_filter_by_name
       void given_filter_input_by_sttaus_should_return_a_page_of_exercises_filter_by_status()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("exercise_status", valueOf(SCHEDULED), contains);
+            PaginationFixture.simpleSearchWithAndOperator(
+                "exercise_status", valueOf(SCHEDULED), contains);
 
         mvc.perform(
                 post(EXERCISE_URI + "/search")
diff --git a/openbas-api/src/test/java/io/openbas/rest/inject/InjectTargetSearchTest.java b/openbas-api/src/test/java/io/openbas/rest/inject/InjectTargetSearchTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/inject/InjectTargetSearchTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/inject/InjectTargetSearchTest.java
@@ -44,6 +44,10 @@ public class InjectTargetSearchTest extends IntegrationTest {
   @Autowired private MockMvc mvc;
   @Autowired private ObjectMapper mapper;
   @Autowired private EntityManager entityManager;
+  @Autowired private AgentComposer agentComposer;
+  @Autowired private ExecutorComposer executorComposer;
+  @Autowired private TagComposer tagComposer;
+  @Autowired private ExecutorFixture executorFixture;
 
   @BeforeEach
   public void beforeEach() {
@@ -53,6 +57,9 @@ public void beforeEach() {
     assetGroupComposer.reset();
     expectationComposer.reset();
     endpointComposer.reset();
+    agentComposer.reset();
+    executorComposer.reset();
+    tagComposer.reset();
   }
 
   private InjectComposer.Composer getInjectWrapper() {
@@ -146,17 +153,38 @@ public void withoutAuthorisation_returnNotFound() throws Exception {
 
   @Nested
   @WithMockAdminUser
-  @DisplayName("With endpoint search")
-  public class WithEndpointSearch {
-    private final TargetType targetType = TargetType.ASSETS;
+  @DisplayName("With agent search")
+  public class WithAgentSearch {
+    private final TargetType targetType = TargetType.AGENT;
 
     @Nested
     @DisplayName("When getting options for inject targets")
     public class WhenGettingOptionsForInjectTargets {
       @Test
-      @Disabled("Not yet implemented")
       @DisplayName("/options should return all possible targets")
       public void whenEndpointsAreFilters_returnAllPossibleTargets() throws Exception {
+        Inject inject = getInjectWrapper().persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        String response =
+            mvc.perform(
+                    get(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/options")
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        List<FilterUtilsJpa.Option> expected = List.of();
+
+        assertThatJson(response).isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName("/options by id should return only options matching ids")
+      public void whenEndpointsAreFilters_returnOnlyOptionsMatchingIds() throws Exception {
         InjectComposer.Composer injectWrapper = getInjectWrapper();
 
         // dynamic group 1
@@ -167,53 +195,163 @@ public void whenEndpointsAreFilters_returnAllPossibleTargets() throws Exception
         filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
         AssetGroupComposer.Composer assetGroupWrapper = getAssetGroupWrapperWithFilter(filter);
 
-        // 2
-        Filters.Filter filter2 = new Filters.Filter();
-        filter2.setKey("endpoint_platform");
-        filter2.setMode(Filters.FilterMode.and);
-        filter2.setOperator(Filters.FilterOperator.eq);
-        filter2.setValues(List.of(Endpoint.PLATFORM_TYPE.Linux.name()));
-        AssetGroupComposer.Composer assetGroupWrapper2 = getAssetGroupWrapperWithFilter(filter2);
-
+        AgentComposer.Composer agentWrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
         EndpointComposer.Composer ep1Wrapper =
-            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+            endpointComposer
+                .forEndpoint(EndpointFixture.createEndpoint())
+                .withAgent(agentWrapper)
+                .persist();
+
+        injectWrapper.withAssetGroup(assetGroupWrapper).persist();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        List<String> ids = List.of(agentWrapper.get().getId());
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/targets/" + targetType + "/options")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(ids)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        List<FilterUtilsJpa.Option> expected = List.of();
+
+        assertThatJson(response).isEqualTo(mapper.writeValueAsString(expected));
+      }
+    }
+
+    @Nested
+    @DisplayName("With OR group operator")
+    public class WithORGroupOperator {
+      @Test
+      @DisplayName(
+          "OR filter group combination should not return agent that is not a target of inject")
+      public void orFilterGroupCombinationShouldNotReturnAgentThatIsNotTargetOfInject()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        AgentComposer.Composer agent1Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        endpointComposer
+            .forEndpoint(EndpointFixture.createEndpoint())
+            .withAgent(agent1Wrapper)
+            .persist();
 
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        AgentComposer.Composer agent2Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
         Endpoint ep2 = EndpointFixture.createEndpoint();
         ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
-        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+        EndpointComposer.Composer ep2Wrapper =
+            endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
+
+        // create a new endpoint that is both part of the above groups and also will be targeted
+        // explicitly
+        AgentComposer.Composer agent3Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        EndpointComposer.Composer ep3Wrapper =
+            endpointComposer.forEndpoint(ep3).withAgent(agent3Wrapper).persist();
 
         // create a new endpoint that is not part of the above groups
-        Endpoint ep3 = EndpointFixture.createEndpoint();
-        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
-        endpointComposer.forEndpoint(ep3).persist();
+        Endpoint notTarget = EndpointFixture.createEndpoint();
+        notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer
+            .forEndpoint(notTarget)
+            .withAgent(
+                agentComposer
+                    .forAgent(AgentFixture.createDefaultAgentService())
+                    .withExecutor(
+                        executorComposer.forExecutor(executorFixture.getDefaultExecutor())))
+            .persist();
 
-        injectWrapper.withAssetGroup(assetGroupWrapper).withAssetGroup(assetGroupWrapper2);
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep3Wrapper);
         Inject inject = injectWrapper.persist().get();
 
         entityManager.flush();
         entityManager.clear();
 
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithOrOperator(
+                "target_name", "", Filters.FilterOperator.contains);
+        // add another filter that would include notTarget endpoint
+        Filters.Filter otherFilter = new Filters.Filter();
+        otherFilter.setKey("target_endpoint");
+        otherFilter.setMode(Filters.FilterMode.and);
+        otherFilter.setOperator(Filters.FilterOperator.contains);
+        otherFilter.setValues(List.of(notTarget.getId()));
+        search.getFilterGroup().getFilters().add(otherFilter);
+
         String response =
             mvc.perform(
-                    get(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/options")
-                        .contentType(MediaType.APPLICATION_JSON))
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
                 .andExpect(status().isOk())
                 .andReturn()
                 .getResponse()
                 .getContentAsString();
 
-        List<FilterUtilsJpa.Option> expected =
+        List<AgentTarget> expected =
             List.of(
-                new FilterUtilsJpa.Option(ep1Wrapper.get().getId(), ep1Wrapper.get().getName()),
-                new FilterUtilsJpa.Option(ep2Wrapper.get().getId(), ep2Wrapper.get().getName()));
+                new AgentTarget(
+                    agent1Wrapper.get().getId(),
+                    agent1Wrapper.get().getTargetDisplayName(),
+                    Set.of(),
+                    agent1Wrapper.get().getAsset().getId(),
+                    agent1Wrapper.get().getExecutor().getType()),
+                new AgentTarget(
+                    agent2Wrapper.get().getId(),
+                    agent2Wrapper.get().getTargetDisplayName(),
+                    Set.of(),
+                    agent2Wrapper.get().getAsset().getId(),
+                    agent2Wrapper.get().getExecutor().getType()),
+                new AgentTarget(
+                    agent3Wrapper.get().getId(),
+                    agent3Wrapper.get().getTargetDisplayName(),
+                    Set.of(),
+                    agent3Wrapper.get().getAsset().getId(),
+                    agent3Wrapper.get().getExecutor().getType()));
 
-        assertThatJson(response).isEqualTo(mapper.writeValueAsString(expected));
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
       }
+    }
 
+    @Nested
+    @DisplayName("With AND filter group operator")
+    public class WithAndFilterGroupOperator {
       @Test
-      @Disabled("Not yet implemented")
-      @DisplayName("/options by id should return only options matching ids")
-      public void whenEndpointsAreFilters_returnOnlyOptionsMatchingIds() throws Exception {
+      @DisplayName(
+          "When agent belongs to dynamic asset in group, they are targets of inject if group is a target")
+      public void whenAgentBelongsToDynamicAssetInGroup_thenTargetIsDynamicAssetInGroup()
+          throws Exception {
         InjectComposer.Composer injectWrapper = getInjectWrapper();
 
         // dynamic group 1
@@ -232,548 +370,1763 @@ public void whenEndpointsAreFilters_returnOnlyOptionsMatchingIds() throws Except
         filter2.setValues(List.of(Endpoint.PLATFORM_TYPE.Linux.name()));
         AssetGroupComposer.Composer assetGroupWrapper2 = getAssetGroupWrapperWithFilter(filter2);
 
-        EndpointComposer.Composer ep1Wrapper =
-            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+        AgentComposer.Composer agent1Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        endpointComposer
+            .forEndpoint(EndpointFixture.createEndpoint())
+            .withAgent(agent1Wrapper)
+            .persist();
 
+        AgentComposer.Composer agent2Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
         Endpoint ep2 = EndpointFixture.createEndpoint();
         ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
-        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+        endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
 
         // create a new endpoint that is not part of the above groups
+        AgentComposer.Composer agent3Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
         Endpoint ep3 = EndpointFixture.createEndpoint();
         ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
-        endpointComposer.forEndpoint(ep3).persist();
+        endpointComposer.forEndpoint(ep3).withAgent(agent3Wrapper).persist();
 
         injectWrapper
             .withAssetGroup(assetGroupWrapper)
+            .withExpectation(
+                expectationComposer
+                    .forExpectation(
+                        InjectExpectationFixture.createExpectationWithTypeAndStatus(
+                            InjectExpectation.EXPECTATION_TYPE.DETECTION,
+                            InjectExpectation.EXPECTATION_STATUS.SUCCESS))
+                    .withAgent(agent1Wrapper))
+            .withExpectation(
+                expectationComposer
+                    .forExpectation(
+                        InjectExpectationFixture.createExpectationWithTypeAndStatus(
+                            InjectExpectation.EXPECTATION_TYPE.PREVENTION,
+                            InjectExpectation.EXPECTATION_STATUS.PARTIAL))
+                    .withAgent(agent1Wrapper))
             .withAssetGroup(assetGroupWrapper2)
-            .persist();
+            .withExpectation(
+                expectationComposer
+                    .forExpectation(
+                        InjectExpectationFixture.createExpectationWithTypeAndStatus(
+                            InjectExpectation.EXPECTATION_TYPE.DETECTION,
+                            InjectExpectation.EXPECTATION_STATUS.SUCCESS))
+                    .withAgent(agent2Wrapper))
+            .withExpectation(
+                expectationComposer
+                    .forExpectation(
+                        InjectExpectationFixture.createExpectationWithTypeAndStatus(
+                            InjectExpectation.EXPECTATION_TYPE.PREVENTION,
+                            InjectExpectation.EXPECTATION_STATUS.PARTIAL))
+                    .withAgent(agent2Wrapper));
+        Inject inject = injectWrapper.persist().get();
 
         entityManager.flush();
         entityManager.clear();
 
-        List<String> ids = List.of(ep1Wrapper.get().getId());
+        SearchPaginationInput search = PaginationFixture.getDefault().build();
 
         String response =
             mvc.perform(
-                    post(INJECT_URI + "/targets/" + targetType + "/options")
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
                         .contentType(MediaType.APPLICATION_JSON)
-                        .content(mapper.writeValueAsString(ids)))
+                        .content(mapper.writeValueAsString(search)))
                 .andExpect(status().isOk())
                 .andReturn()
                 .getResponse()
                 .getContentAsString();
 
-        List<FilterUtilsJpa.Option> expected =
-            List.of(
-                new FilterUtilsJpa.Option(ep1Wrapper.get().getId(), ep1Wrapper.get().getName()));
+        AgentTarget expectedTarget1 =
+            new AgentTarget(
+                agent1Wrapper.get().getId(),
+                agent1Wrapper.get().getTargetDisplayName(),
+                Set.of(),
+                agent1Wrapper.get().getAsset().getId(),
+                agent1Wrapper.get().getExecutor().getType());
+        expectedTarget1.setTargetDetectionStatus(InjectExpectation.EXPECTATION_STATUS.SUCCESS);
+        expectedTarget1.setTargetPreventionStatus(InjectExpectation.EXPECTATION_STATUS.PARTIAL);
+        expectedTarget1.setTargetHumanResponseStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
+        expectedTarget1.setTargetExecutionStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
+        AgentTarget expectedTarget2 =
+            new AgentTarget(
+                agent2Wrapper.get().getId(),
+                agent2Wrapper.get().getTargetDisplayName(),
+                Set.of(),
+                agent2Wrapper.get().getAsset().getId(),
+                agent2Wrapper.get().getExecutor().getType());
+        expectedTarget2.setTargetDetectionStatus(InjectExpectation.EXPECTATION_STATUS.SUCCESS);
+        expectedTarget2.setTargetPreventionStatus(InjectExpectation.EXPECTATION_STATUS.PARTIAL);
+        expectedTarget2.setTargetHumanResponseStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
+        expectedTarget2.setTargetExecutionStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
+        // expect two out of three endpoints in the resultset, i.e. not the extra one
+        List<AgentTarget> expected = List.of(expectedTarget1, expectedTarget2);
 
-        assertThatJson(response).isEqualTo(mapper.writeValueAsString(expected));
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
       }
-    }
 
-    @Test
-    @DisplayName(
-        "When endpoint is dynamic asset in group, they are targets of inject if group is a target")
-    public void whenEndpointIsDynamicAssetInGroup_thenTargetIsDynamicAssetInGroup()
-        throws Exception {
-      InjectComposer.Composer injectWrapper = getInjectWrapper();
+      @Test
+      @DisplayName("When dynamic groups intersect, common agents are returned only once")
+      public void whenDynamicGroupsIntersect_thenReturnCommonAgentsOnce() throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
 
-      // dynamic group 1
-      Filters.Filter filter = new Filters.Filter();
-      filter.setKey("endpoint_platform");
-      filter.setMode(Filters.FilterMode.and);
-      filter.setOperator(Filters.FilterOperator.eq);
-      filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
-      AssetGroupComposer.Composer assetGroupWrapper = getAssetGroupWrapperWithFilter(filter);
-
-      // 2
-      Filters.Filter filter2 = new Filters.Filter();
-      filter2.setKey("endpoint_platform");
-      filter2.setMode(Filters.FilterMode.and);
-      filter2.setOperator(Filters.FilterOperator.eq);
-      filter2.setValues(List.of(Endpoint.PLATFORM_TYPE.Linux.name()));
-      AssetGroupComposer.Composer assetGroupWrapper2 = getAssetGroupWrapperWithFilter(filter2);
-
-      EndpointComposer.Composer ep1Wrapper =
-          endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
-      Endpoint ep2 = EndpointFixture.createEndpoint();
-      ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
-      EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
-
-      // create a new endpoint that is not part of the above groups
-      Endpoint ep3 = EndpointFixture.createEndpoint();
-      ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
-      endpointComposer.forEndpoint(ep3).persist();
-
-      injectWrapper
-          .withAssetGroup(assetGroupWrapper)
-          .withExpectation(
-              expectationComposer
-                  .forExpectation(
-                      InjectExpectationFixture.createExpectationWithTypeAndStatus(
-                          InjectExpectation.EXPECTATION_TYPE.DETECTION,
-                          InjectExpectation.EXPECTATION_STATUS.SUCCESS))
-                  .withAssetGroup(assetGroupWrapper)
-                  .withEndpoint(ep1Wrapper))
-          .withExpectation(
-              expectationComposer
-                  .forExpectation(
-                      InjectExpectationFixture.createExpectationWithTypeAndStatus(
-                          InjectExpectation.EXPECTATION_TYPE.PREVENTION,
-                          InjectExpectation.EXPECTATION_STATUS.PARTIAL))
-                  .withAssetGroup(assetGroupWrapper)
-                  .withEndpoint(ep1Wrapper))
-          .withAssetGroup(assetGroupWrapper2)
-          .withExpectation(
-              expectationComposer
-                  .forExpectation(
-                      InjectExpectationFixture.createExpectationWithTypeAndStatus(
-                          InjectExpectation.EXPECTATION_TYPE.DETECTION,
-                          InjectExpectation.EXPECTATION_STATUS.SUCCESS))
-                  .withAssetGroup(assetGroupWrapper2)
-                  .withEndpoint(ep2Wrapper))
-          .withExpectation(
-              expectationComposer
-                  .forExpectation(
-                      InjectExpectationFixture.createExpectationWithTypeAndStatus(
-                          InjectExpectation.EXPECTATION_TYPE.PREVENTION,
-                          InjectExpectation.EXPECTATION_STATUS.PARTIAL))
-                  .withAssetGroup(assetGroupWrapper2)
-                  .withEndpoint(ep2Wrapper));
-      Inject inject = injectWrapper.persist().get();
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+
+        // create two identical asset groups
+        for (int i = 0; i < 2; i++) {
+          Filters.Filter filter = new Filters.Filter();
+          filter.setKey("endpoint_platform");
+          filter.setMode(Filters.FilterMode.and);
+          filter.setOperator(Filters.FilterOperator.eq);
+          filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+          assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+        }
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        AgentComposer.Composer agent1Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        endpointComposer
+            .forEndpoint(EndpointFixture.createEndpoint())
+            .withAgent(agent1Wrapper)
+            .persist();
 
-      entityManager.flush();
-      entityManager.clear();
+        // create a new endpoint that is not part of the above groups
+        AgentComposer.Composer agent2Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
 
-      SearchPaginationInput search =
-          PaginationFixture.simpleFilter("target_name", "", Filters.FilterOperator.contains);
+        // create a new endpoint that is not part of the above groups
+        AgentComposer.Composer agent3Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).withAgent(agent3Wrapper).persist();
 
-      String response =
-          mvc.perform(
-                  post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
-                      .contentType(MediaType.APPLICATION_JSON)
-                      .content(mapper.writeValueAsString(search)))
-              .andExpect(status().isOk())
-              .andReturn()
-              .getResponse()
-              .getContentAsString();
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        Inject inject = injectWrapper.persist().get();
 
-      EndpointTarget expectedTarget1 =
-          new EndpointTarget(
-              ep1Wrapper.get().getId(),
-              ep1Wrapper.get().getName(),
-              ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-              ep1Wrapper.get().getPlatform().name());
-      expectedTarget1.setTargetDetectionStatus(InjectExpectation.EXPECTATION_STATUS.SUCCESS);
-      expectedTarget1.setTargetPreventionStatus(InjectExpectation.EXPECTATION_STATUS.PARTIAL);
-      expectedTarget1.setTargetHumanResponseStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
-      expectedTarget1.setTargetExecutionStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
-      EndpointTarget expectedTarget2 =
-          new EndpointTarget(
-              ep2Wrapper.get().getId(),
-              ep2Wrapper.get().getName(),
-              ep2Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-              ep2Wrapper.get().getPlatform().name());
-      expectedTarget2.setTargetDetectionStatus(InjectExpectation.EXPECTATION_STATUS.SUCCESS);
-      expectedTarget2.setTargetPreventionStatus(InjectExpectation.EXPECTATION_STATUS.PARTIAL);
-      expectedTarget2.setTargetHumanResponseStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
-      expectedTarget2.setTargetExecutionStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
-      // expect two out of three endpoints in the resultset, i.e. not the extra one
-      List<EndpointTarget> expected = List.of(expectedTarget1, expectedTarget2);
+        entityManager.flush();
+        entityManager.clear();
 
-      assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
-    }
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_name", "", Filters.FilterOperator.contains);
 
-    @Test
-    @DisplayName("When dynamic groups intersect, common endpoints are returned only once")
-    public void whenDynamicGroupsIntersect_thenReturnCommonEndpointsOnce() throws Exception {
-      InjectComposer.Composer injectWrapper = getInjectWrapper();
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        AgentTarget expectedTarget =
+            new AgentTarget(
+                agent1Wrapper.get().getId(),
+                agent1Wrapper.get().getTargetDisplayName(),
+                Set.of(),
+                agent1Wrapper.get().getAsset().getId(),
+                agent1Wrapper.get().getExecutor().getType());
+        List<AgentTarget> expected = List.of(expectedTarget);
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
 
-      List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+      @Test
+      @DisplayName(
+          "When no applied AssetGroup filter, at most a full page of targeted agents should be returned")
+      public void whenNoAppliedAssetGroupFilter_returnPageOfAllTargetedAgents() throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
 
-      // create two identical asset groups
-      for (int i = 0; i < 2; i++) {
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
         Filters.Filter filter = new Filters.Filter();
         filter.setKey("endpoint_platform");
         filter.setMode(Filters.FilterMode.and);
         filter.setOperator(Filters.FilterOperator.eq);
         filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
         assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
-      }
-
-      // create several endpoints; only the Windows (first) endpoint should be involved in the above
-      // groups
-      EndpointComposer.Composer ep1Wrapper =
-          endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
 
-      // create a new endpoint that is not part of the above groups
-      Endpoint ep2 = EndpointFixture.createEndpoint();
-      ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
-      EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
-
-      // create a new endpoint that is not part of the above groups
-      Endpoint ep3 = EndpointFixture.createEndpoint();
-      ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
-      endpointComposer.forEndpoint(ep3).persist();
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        AgentComposer.Composer agent1Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        endpointComposer
+            .forEndpoint(EndpointFixture.createEndpoint())
+            .withAgent(agent1Wrapper)
+            .persist();
 
-      assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
-      Inject inject = injectWrapper.persist().get();
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        AgentComposer.Composer agent2Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper =
+            endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
+
+        // create a new endpoint that is both part of the above groups and also will be targeted
+        // explicitly
+        AgentComposer.Composer agent3Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        EndpointComposer.Composer ep3Wrapper =
+            endpointComposer.forEndpoint(ep3).withAgent(agent3Wrapper).persist();
 
-      entityManager.flush();
-      entityManager.clear();
+        // create a new endpoint that is not part of the above groups
+        Endpoint notTarget = EndpointFixture.createEndpoint();
+        notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer
+            .forEndpoint(notTarget)
+            .withAgent(
+                agentComposer
+                    .forAgent(AgentFixture.createDefaultAgentService())
+                    .withExecutor(
+                        executorComposer.forExecutor(executorFixture.getDefaultExecutor())))
+            .persist();
 
-      SearchPaginationInput search =
-          PaginationFixture.simpleFilter("target_name", "", Filters.FilterOperator.contains);
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep3Wrapper);
+        Inject inject = injectWrapper.persist().get();
 
-      String response =
-          mvc.perform(
-                  post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
-                      .contentType(MediaType.APPLICATION_JSON)
-                      .content(mapper.writeValueAsString(search)))
-              .andExpect(status().isOk())
-              .andReturn()
-              .getResponse()
-              .getContentAsString();
+        entityManager.flush();
+        entityManager.clear();
 
-      EndpointTarget expectedTarget =
-          new EndpointTarget(
-              ep1Wrapper.get().getId(),
-              ep1Wrapper.get().getName(),
-              ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-              ep1Wrapper.get().getPlatform().name());
-      List<EndpointTarget> expected = List.of(expectedTarget);
+        SearchPaginationInput search = PaginationFixture.getDefault().build();
 
-      assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
-    }
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
 
-    @Test
-    @DisplayName(
-        "When no applied AssetGroup filter, at most a full page of targeted endpoints should be returned")
-    public void whenNoAppliedAssetGroupFilter_returnPageOfAllTargetedEndpoints() throws Exception {
-      InjectComposer.Composer injectWrapper = getInjectWrapper();
+        List<AgentTarget> expected =
+            List.of(
+                new AgentTarget(
+                    agent1Wrapper.get().getId(),
+                    agent1Wrapper.get().getTargetDisplayName(),
+                    Set.of(),
+                    agent1Wrapper.get().getAsset().getId(),
+                    agent1Wrapper.get().getExecutor().getType()),
+                new AgentTarget(
+                    agent2Wrapper.get().getId(),
+                    agent2Wrapper.get().getTargetDisplayName(),
+                    Set.of(),
+                    agent2Wrapper.get().getAsset().getId(),
+                    agent2Wrapper.get().getExecutor().getType()),
+                new AgentTarget(
+                    agent3Wrapper.get().getId(),
+                    agent3Wrapper.get().getTargetDisplayName(),
+                    Set.of(),
+                    agent3Wrapper.get().getAsset().getId(),
+                    agent3Wrapper.get().getExecutor().getType()));
 
-      List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
-      Filters.Filter filter = new Filters.Filter();
-      filter.setKey("endpoint_platform");
-      filter.setMode(Filters.FilterMode.and);
-      filter.setOperator(Filters.FilterOperator.eq);
-      filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
-      assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
-
-      // create several endpoints; only the Windows (first) endpoint should be involved in the above
-      // groups
-      EndpointComposer.Composer ep1Wrapper =
-          endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
-
-      // create a new endpoint that is not part of the above groups but will be targeted explicitly
-      Endpoint ep2 = EndpointFixture.createEndpoint();
-      ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
-      EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
-
-      // create a new endpoint that is both part of the above groups and also will be targeted
-      // explicitly
-      Endpoint ep3 = EndpointFixture.createEndpoint();
-      EndpointComposer.Composer ep3Wrapper = endpointComposer.forEndpoint(ep3).persist();
-
-      // create a new endpoint that is not part of the above groups
-      Endpoint notTarget = EndpointFixture.createEndpoint();
-      notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
-      endpointComposer.forEndpoint(notTarget).persist();
-
-      assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
-      injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep3Wrapper);
-      Inject inject = injectWrapper.persist().get();
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
 
-      entityManager.flush();
-      entityManager.clear();
+      @Test
+      @DisplayName(
+          "When endpoints are explicitly targeted, 'is empty' Asset group filter should only return agents from endpoints that are not in any group'")
+      public void whenEndpointsAreExplicitlyTargeted_thenReturnOnlyAgentsFromEndpointsWithNoGroup()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
 
-      SearchPaginationInput search = PaginationFixture.getDefault().build();
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
 
-      String response =
-          mvc.perform(
-                  post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
-                      .contentType(MediaType.APPLICATION_JSON)
-                      .content(mapper.writeValueAsString(search)))
-              .andExpect(status().isOk())
-              .andReturn()
-              .getResponse()
-              .getContentAsString();
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        endpointComposer
+            .forEndpoint(EndpointFixture.createEndpoint())
+            .withAgent(
+                agentComposer
+                    .forAgent(AgentFixture.createDefaultAgentService())
+                    .withExecutor(
+                        executorComposer.forExecutor(executorFixture.getDefaultExecutor())))
+            .persist();
 
-      List<EndpointTarget> expected =
-          List.of(
-              new EndpointTarget(
-                  ep1Wrapper.get().getId(),
-                  ep1Wrapper.get().getName(),
-                  ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-                  ep1Wrapper.get().getPlatform().name()),
-              new EndpointTarget(
-                  ep2Wrapper.get().getId(),
-                  ep2Wrapper.get().getName(),
-                  ep2Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-                  ep2Wrapper.get().getPlatform().name()),
-              new EndpointTarget(
-                  ep3Wrapper.get().getId(),
-                  ep3Wrapper.get().getName(),
-                  ep3Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-                  ep3Wrapper.get().getPlatform().name()));
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        AgentComposer.Composer agent2Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper =
+            endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
+
+        // create a new endpoint that is both part of the above groups and also will be targeted
+        // explicitly
+        AgentComposer.Composer agent3Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        EndpointComposer.Composer ep3Wrapper =
+            endpointComposer.forEndpoint(ep3).withAgent(agent3Wrapper).persist();
 
-      assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
-    }
+        // create a new endpoint that is not part of the above groups
+        Endpoint notTarget = EndpointFixture.createEndpoint();
+        notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer
+            .forEndpoint(notTarget)
+            .withAgent(
+                agentComposer
+                    .forAgent(AgentFixture.createDefaultAgentService())
+                    .withExecutor(
+                        executorComposer.forExecutor(executorFixture.getDefaultExecutor())))
+            .persist();
 
-    @Test
-    @DisplayName(
-        "When endpoints are explicitly targeted, 'is empty' Asset group filter should only return those that are not in any group'")
-    public void whenEndpointsAreExplicitlyTargeted_thenReturnOnlyEndpointsWithNoGroup()
-        throws Exception {
-      InjectComposer.Composer injectWrapper = getInjectWrapper();
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep3Wrapper);
+        Inject inject = injectWrapper.persist().get();
 
-      List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
-      Filters.Filter filter = new Filters.Filter();
-      filter.setKey("endpoint_platform");
-      filter.setMode(Filters.FilterMode.and);
-      filter.setOperator(Filters.FilterOperator.eq);
-      filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
-      assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
-
-      // create several endpoints; only the Windows (first) endpoint should be involved in the above
-      // groups
-      endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
-
-      // create a new endpoint that is not part of the above groups but will be targeted explicitly
-      Endpoint ep2 = EndpointFixture.createEndpoint();
-      ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
-      EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
-
-      // create a new endpoint that is both part of the above groups and also will be targeted
-      // explicitly
-      Endpoint ep3 = EndpointFixture.createEndpoint();
-      EndpointComposer.Composer ep3Wrapper = endpointComposer.forEndpoint(ep3).persist();
-
-      // create a new endpoint that is not part of the above groups
-      Endpoint notTarget = EndpointFixture.createEndpoint();
-      notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
-      endpointComposer.forEndpoint(notTarget).persist();
-
-      assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
-      injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep3Wrapper);
-      Inject inject = injectWrapper.persist().get();
+        entityManager.flush();
+        entityManager.clear();
 
-      entityManager.flush();
-      entityManager.clear();
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_asset_groups", null, Filters.FilterOperator.empty);
 
-      SearchPaginationInput search =
-          PaginationFixture.simpleFilter("target_asset_groups", null, Filters.FilterOperator.empty);
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
 
-      String response =
-          mvc.perform(
-                  post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
-                      .contentType(MediaType.APPLICATION_JSON)
-                      .content(mapper.writeValueAsString(search)))
-              .andExpect(status().isOk())
-              .andReturn()
-              .getResponse()
-              .getContentAsString();
+        AgentTarget expectedTarget =
+            new AgentTarget(
+                agent2Wrapper.get().getId(),
+                agent2Wrapper.get().getTargetDisplayName(),
+                Set.of(),
+                agent2Wrapper.get().getAsset().getId(),
+                agent2Wrapper.get().getExecutor().getType());
+        List<AgentTarget> expected = List.of(expectedTarget);
 
-      EndpointTarget expectedTarget =
-          new EndpointTarget(
-              ep2Wrapper.get().getId(),
-              ep2Wrapper.get().getName(),
-              ep2Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-              ep2Wrapper.get().getPlatform().name());
-      List<EndpointTarget> expected = List.of(expectedTarget);
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
 
-      assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
-    }
+      @Test
+      @DisplayName(
+          "When endpoints are explicitly targeted, 'not empty' Asset group filter should only return agents that belong to endpoints in any group'")
+      public void whenEndpointsAreExplicitlyTargeted_thenReturnOnlyAgentsFromEndpointsWithGroup()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
 
-    @Test
-    @DisplayName(
-        "When endpoints are explicitly targeted, 'not empty' Asset group filter should only return those that are in any group'")
-    public void whenEndpointsAreExplicitlyTargeted_thenReturnOnlyEndpointsWithGroup()
-        throws Exception {
-      InjectComposer.Composer injectWrapper = getInjectWrapper();
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
 
-      List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
-      Filters.Filter filter = new Filters.Filter();
-      filter.setKey("endpoint_platform");
-      filter.setMode(Filters.FilterMode.and);
-      filter.setOperator(Filters.FilterOperator.eq);
-      filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
-      assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
-
-      // create several endpoints; only the Windows (first) endpoint should be involved in the above
-      // groups
-      EndpointComposer.Composer ep1Wrapper =
-          endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
-
-      // create a new endpoint that is not part of the above groups but will be targeted explicitly
-      Endpoint ep2 = EndpointFixture.createEndpoint();
-      ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
-      EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
-
-      // create a new endpoint that is not part of the above groups
-      Endpoint notTarget = EndpointFixture.createEndpoint();
-      notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
-      endpointComposer.forEndpoint(notTarget).persist();
-
-      assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
-      injectWrapper.withEndpoint(ep2Wrapper);
-      Inject inject = injectWrapper.persist().get();
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        AgentComposer.Composer agent1Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        endpointComposer
+            .forEndpoint(EndpointFixture.createEndpoint())
+            .withAgent(agent1Wrapper)
+            .persist();
 
-      entityManager.flush();
-      entityManager.clear();
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        AgentComposer.Composer agent2Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper =
+            endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
 
-      SearchPaginationInput search =
-          PaginationFixture.simpleFilter(
-              "target_asset_groups", null, Filters.FilterOperator.not_empty);
+        // create a new endpoint that is not part of the above groups
+        AgentComposer.Composer agent3Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint notTarget = EndpointFixture.createEndpoint();
+        notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(notTarget).withAgent(agent3Wrapper).persist();
+
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        injectWrapper.withEndpoint(ep2Wrapper);
+        Inject inject = injectWrapper.persist().get();
 
-      String response =
-          mvc.perform(
-                  post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
-                      .contentType(MediaType.APPLICATION_JSON)
-                      .content(mapper.writeValueAsString(search)))
-              .andExpect(status().isOk())
-              .andReturn()
-              .getResponse()
-              .getContentAsString();
+        entityManager.flush();
+        entityManager.clear();
 
-      EndpointTarget expectedTarget =
-          new EndpointTarget(
-              ep1Wrapper.get().getId(),
-              ep1Wrapper.get().getName(),
-              ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-              ep1Wrapper.get().getPlatform().name());
-      List<EndpointTarget> expected = List.of(expectedTarget);
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_asset_groups", null, Filters.FilterOperator.not_empty);
 
-      assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
-    }
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
 
-    @Test
-    @DisplayName(
-        "When are part of a group, 'not contains' operator on this group should exclude endpoint")
-    public void whenArePartOfAGroupAndGroupFilteredOut_thenExcludeAllEndpointsOfGroup()
-        throws Exception {
-      InjectComposer.Composer injectWrapper = getInjectWrapper();
+        AgentTarget expectedTarget =
+            new AgentTarget(
+                agent1Wrapper.get().getId(),
+                agent1Wrapper.get().getTargetDisplayName(),
+                Set.of(),
+                agent1Wrapper.get().getAsset().getId(),
+                agent1Wrapper.get().getExecutor().getType());
+        List<AgentTarget> expected = List.of(expectedTarget);
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName(
+          "When are part of a group, 'not contains' operator on this group should exclude agents of member endpoints")
+      public void whenArePartOfAGroupAndGroupFilteredOut_thenExcludeAllAgentsFromEndpointsOfGroup()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
 
-      List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
-      Filters.Filter filter = new Filters.Filter();
-      filter.setKey("endpoint_platform");
-      filter.setMode(Filters.FilterMode.and);
-      filter.setOperator(Filters.FilterOperator.eq);
-      filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
-      assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        endpointComposer
+            .forEndpoint(EndpointFixture.createEndpoint())
+            .withAgent(
+                agentComposer
+                    .forAgent(AgentFixture.createDefaultAgentService())
+                    .withExecutor(
+                        executorComposer.forExecutor(executorFixture.getDefaultExecutor())))
+            .persist();
 
-      // create several endpoints; only the Windows (first) endpoint should be involved in the above
-      // groups
-      EndpointComposer.Composer ep1Wrapper =
-          endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        AgentComposer.Composer agent2Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper =
+            endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
 
-      // create a new endpoint that is not part of the above groups but will be targeted explicitly
-      Endpoint ep2 = EndpointFixture.createEndpoint();
-      ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
-      EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+        // create a new endpoint that is not part of the above groups
+        AgentComposer.Composer agent3Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).withAgent(agent3Wrapper).persist();
 
-      // create a new endpoint that is not part of the above groups
-      Endpoint ep3 = EndpointFixture.createEndpoint();
-      ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
-      endpointComposer.forEndpoint(ep3).persist();
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        Inject inject = injectWrapper.withEndpoint(ep2Wrapper).persist().get();
 
-      assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
-      Inject inject = injectWrapper.withEndpoint(ep2Wrapper).persist().get();
+        entityManager.flush();
+        entityManager.clear();
 
-      entityManager.flush();
-      entityManager.clear();
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_asset_groups",
+                assetGroupWrappers.getFirst().get().getId(),
+                Filters.FilterOperator.not_contains);
 
-      SearchPaginationInput search =
-          PaginationFixture.simpleFilter(
-              "target_asset_groups",
-              assetGroupWrappers.getFirst().get().getId(),
-              Filters.FilterOperator.not_contains);
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
 
-      String response =
-          mvc.perform(
-                  post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
-                      .contentType(MediaType.APPLICATION_JSON)
-                      .content(mapper.writeValueAsString(search)))
-              .andExpect(status().isOk())
-              .andReturn()
-              .getResponse()
-              .getContentAsString();
+        AgentTarget expectedTarget =
+            new AgentTarget(
+                agent2Wrapper.get().getId(),
+                agent2Wrapper.get().getTargetDisplayName(),
+                Set.of(),
+                agent2Wrapper.get().getAsset().getId(),
+                agent2Wrapper.get().getExecutor().getType());
+        List<AgentTarget> expected = List.of(expectedTarget);
 
-      EndpointTarget expectedTarget =
-          new EndpointTarget(
-              ep2Wrapper.get().getId(),
-              ep2Wrapper.get().getName(),
-              ep2Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-              ep2Wrapper.get().getPlatform().name());
-      List<EndpointTarget> expected = List.of(expectedTarget);
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
 
-      assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      @Test
+      @DisplayName(
+          "When are part of a group, 'contains' operator on this group should include agents from endpoints of this group and exclude others")
+      public void
+          whenArePartOfAGroupAndGroupSelectedAsFilter_thenIncludeOnlyAgentsFromEndpointsOfGroup()
+              throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        AgentComposer.Composer agent1Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer
+                .forEndpoint(EndpointFixture.createEndpoint())
+                .withAgent(agent1Wrapper)
+                .persist();
+
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        AgentComposer.Composer agent2Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper =
+            endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
+
+        // create a new endpoint that is not part of the above groups
+        AgentComposer.Composer agent3Wrapper =
+            agentComposer
+                .forAgent(AgentFixture.createDefaultAgentService())
+                .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).withAgent(agent3Wrapper).persist();
+
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        // add two endpoints as direct targets, including the endpoint part of the excluded group
+        injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep1Wrapper);
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_asset_groups",
+                assetGroupWrappers.getFirst().get().getId(),
+                Filters.FilterOperator.contains);
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        AgentTarget expectedTarget =
+            new AgentTarget(
+                agent1Wrapper.get().getId(),
+                agent1Wrapper.get().getTargetDisplayName(),
+                Set.of(),
+                agent1Wrapper.get().getAsset().getId(),
+                agent1Wrapper.get().getExecutor().getType());
+        List<AgentTarget> expected = List.of(expectedTarget);
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Nested
+      @DisplayName("When filtering with tags")
+      public class WhenFilteringWithTags {
+        @Test
+        @DisplayName(
+            "'contains' tag filter should only return agents that belong to endpoints with specified tags'")
+        public void
+            whenEndpointsAreExplicitlyTargeted_thenReturnOnlyAgentsFromEndpointsWithSpecifiedTags()
+                throws Exception {
+          InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+          List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+          Filters.Filter filter = new Filters.Filter();
+          filter.setKey("endpoint_platform");
+          filter.setMode(Filters.FilterMode.and);
+          filter.setOperator(Filters.FilterOperator.eq);
+          filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+          assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+          // create several endpoints; only the Windows (first) endpoint should be involved in the
+          // above
+          // groups
+          TagComposer.Composer tagWrapper =
+              tagComposer.forTag(TagFixture.getTagWithText("search_this_tag"));
+          AgentComposer.Composer agent1Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          endpointComposer
+              .forEndpoint(EndpointFixture.createEndpoint())
+              .withTag(tagWrapper)
+              .withAgent(agent1Wrapper)
+              .persist();
+
+          // create a new endpoint that is not part of the above groups but will be targeted
+          // explicitly
+          AgentComposer.Composer agent2Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          Endpoint ep2 = EndpointFixture.createEndpoint();
+          ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+          EndpointComposer.Composer ep2Wrapper =
+              endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
+
+          // create a new endpoint that is not part of the above groups
+          AgentComposer.Composer agent3Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          Endpoint notTarget = EndpointFixture.createEndpoint();
+          notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+          endpointComposer.forEndpoint(notTarget).withAgent(agent3Wrapper).persist();
+
+          assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+          injectWrapper.withEndpoint(ep2Wrapper);
+          Inject inject = injectWrapper.persist().get();
+
+          entityManager.flush();
+          entityManager.clear();
+
+          SearchPaginationInput search =
+              PaginationFixture.simpleSearchWithAndOperator(
+                  "target_tags", tagWrapper.get().getId(), Filters.FilterOperator.contains);
+
+          String response =
+              mvc.perform(
+                      post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                          .contentType(MediaType.APPLICATION_JSON)
+                          .content(mapper.writeValueAsString(search)))
+                  .andExpect(status().isOk())
+                  .andReturn()
+                  .getResponse()
+                  .getContentAsString();
+
+          AgentTarget expectedTarget =
+              new AgentTarget(
+                  agent1Wrapper.get().getId(),
+                  agent1Wrapper.get().getTargetDisplayName(),
+                  Set.of(),
+                  agent1Wrapper.get().getAsset().getId(),
+                  agent1Wrapper.get().getExecutor().getType());
+          List<AgentTarget> expected = List.of(expectedTarget);
+
+          assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+        }
+
+        @Test
+        @DisplayName(
+            "'not_contains' tag filter should only return agents that don't belong to endpoints with specified tags'")
+        public void
+            whenEndpointsAreExplicitlyTargeted_thenReturnOnlyAgentsNotFromEndpointsWithSpecifiedTags()
+                throws Exception {
+          InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+          List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+          Filters.Filter filter = new Filters.Filter();
+          filter.setKey("endpoint_platform");
+          filter.setMode(Filters.FilterMode.and);
+          filter.setOperator(Filters.FilterOperator.eq);
+          filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+          assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+          // create several endpoints; only the Windows (first) endpoint should be involved in the
+          // above
+          // groups
+          TagComposer.Composer tagWrapper =
+              tagComposer.forTag(TagFixture.getTagWithText("search_this_tag"));
+          AgentComposer.Composer agent1Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          endpointComposer
+              .forEndpoint(EndpointFixture.createEndpoint())
+              .withTag(tagWrapper)
+              .withAgent(agent1Wrapper)
+              .persist();
+
+          // create a new endpoint that is not part of the above groups but will be targeted
+          // explicitly
+          AgentComposer.Composer agent2Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          Endpoint ep2 = EndpointFixture.createEndpoint();
+          ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+          EndpointComposer.Composer ep2Wrapper =
+              endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
+
+          // create a new endpoint that is not part of the above groups
+          AgentComposer.Composer agent3Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          Endpoint notTarget = EndpointFixture.createEndpoint();
+          notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+          endpointComposer.forEndpoint(notTarget).withAgent(agent3Wrapper).persist();
+
+          assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+          injectWrapper.withEndpoint(ep2Wrapper);
+          Inject inject = injectWrapper.persist().get();
+
+          entityManager.flush();
+          entityManager.clear();
+
+          SearchPaginationInput search =
+              PaginationFixture.simpleSearchWithAndOperator(
+                  "target_tags", tagWrapper.get().getId(), Filters.FilterOperator.not_contains);
+
+          String response =
+              mvc.perform(
+                      post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                          .contentType(MediaType.APPLICATION_JSON)
+                          .content(mapper.writeValueAsString(search)))
+                  .andExpect(status().isOk())
+                  .andReturn()
+                  .getResponse()
+                  .getContentAsString();
+
+          AgentTarget expectedTarget =
+              new AgentTarget(
+                  agent2Wrapper.get().getId(),
+                  agent2Wrapper.get().getTargetDisplayName(),
+                  Set.of(),
+                  agent2Wrapper.get().getAsset().getId(),
+                  agent2Wrapper.get().getExecutor().getType());
+          List<AgentTarget> expected = List.of(expectedTarget);
+
+          assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+        }
+
+        @Test
+        @DisplayName(
+            "'empty' tag filter should only return agents that belong to endpoints with no tags'")
+        public void whenEndpointsAreExplicitlyTargeted_thenReturnOnlyAgentsFromEndpointsWithNoTags()
+            throws Exception {
+          InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+          List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+          Filters.Filter filter = new Filters.Filter();
+          filter.setKey("endpoint_platform");
+          filter.setMode(Filters.FilterMode.and);
+          filter.setOperator(Filters.FilterOperator.eq);
+          filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+          assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+          // create several endpoints; only the Windows (first) endpoint should be involved in the
+          // above
+          // groups
+          TagComposer.Composer tagWrapper =
+              tagComposer.forTag(TagFixture.getTagWithText("search_this_tag"));
+          AgentComposer.Composer agent1Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          endpointComposer
+              .forEndpoint(EndpointFixture.createEndpoint())
+              .withTag(tagWrapper)
+              .withAgent(agent1Wrapper)
+              .persist();
+
+          // create a new endpoint that is not part of the above groups but will be targeted
+          // explicitly
+          AgentComposer.Composer agent2Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          Endpoint ep2 = EndpointFixture.createEndpoint();
+          ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+          EndpointComposer.Composer ep2Wrapper =
+              endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
+
+          // create a new endpoint that is not part of the above groups
+          AgentComposer.Composer agent3Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          Endpoint notTarget = EndpointFixture.createEndpoint();
+          notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+          endpointComposer.forEndpoint(notTarget).withAgent(agent3Wrapper).persist();
+
+          assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+          injectWrapper.withEndpoint(ep2Wrapper);
+          Inject inject = injectWrapper.persist().get();
+
+          entityManager.flush();
+          entityManager.clear();
+
+          SearchPaginationInput search =
+              PaginationFixture.simpleSearchWithAndOperator(
+                  "target_tags", tagWrapper.get().getId(), Filters.FilterOperator.empty);
+
+          String response =
+              mvc.perform(
+                      post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                          .contentType(MediaType.APPLICATION_JSON)
+                          .content(mapper.writeValueAsString(search)))
+                  .andExpect(status().isOk())
+                  .andReturn()
+                  .getResponse()
+                  .getContentAsString();
+
+          AgentTarget expectedTarget =
+              new AgentTarget(
+                  agent2Wrapper.get().getId(),
+                  agent2Wrapper.get().getTargetDisplayName(),
+                  Set.of(),
+                  agent2Wrapper.get().getAsset().getId(),
+                  agent2Wrapper.get().getExecutor().getType());
+          List<AgentTarget> expected = List.of(expectedTarget);
+
+          assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+        }
+
+        @Test
+        @DisplayName(
+            "'not_empty' tag filter should only return agents that belong to endpoints with no tags'")
+        public void
+            whenEndpointsAreExplicitlyTargeted_thenReturnOnlyAgentsFromEndpointsWithSomeTags()
+                throws Exception {
+          InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+          List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+          Filters.Filter filter = new Filters.Filter();
+          filter.setKey("endpoint_platform");
+          filter.setMode(Filters.FilterMode.and);
+          filter.setOperator(Filters.FilterOperator.eq);
+          filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+          assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+          // create several endpoints; only the Windows (first) endpoint should be involved in the
+          // above
+          // groups
+          TagComposer.Composer tagWrapper =
+              tagComposer.forTag(TagFixture.getTagWithText("search_this_tag"));
+          AgentComposer.Composer agent1Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          endpointComposer
+              .forEndpoint(EndpointFixture.createEndpoint())
+              .withTag(tagWrapper)
+              .withAgent(agent1Wrapper)
+              .persist();
+
+          // create a new endpoint that is not part of the above groups but will be targeted
+          // explicitly
+          AgentComposer.Composer agent2Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          Endpoint ep2 = EndpointFixture.createEndpoint();
+          ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+          EndpointComposer.Composer ep2Wrapper =
+              endpointComposer.forEndpoint(ep2).withAgent(agent2Wrapper).persist();
+
+          // create a new endpoint that is not part of the above groups
+          AgentComposer.Composer agent3Wrapper =
+              agentComposer
+                  .forAgent(AgentFixture.createDefaultAgentService())
+                  .withExecutor(executorComposer.forExecutor(executorFixture.getDefaultExecutor()));
+          Endpoint notTarget = EndpointFixture.createEndpoint();
+          notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+          endpointComposer.forEndpoint(notTarget).withAgent(agent3Wrapper).persist();
+
+          assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+          injectWrapper.withEndpoint(ep2Wrapper);
+          Inject inject = injectWrapper.persist().get();
+
+          entityManager.flush();
+          entityManager.clear();
+
+          SearchPaginationInput search =
+              PaginationFixture.simpleSearchWithAndOperator(
+                  "target_tags", tagWrapper.get().getId(), Filters.FilterOperator.not_empty);
+
+          String response =
+              mvc.perform(
+                      post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                          .contentType(MediaType.APPLICATION_JSON)
+                          .content(mapper.writeValueAsString(search)))
+                  .andExpect(status().isOk())
+                  .andReturn()
+                  .getResponse()
+                  .getContentAsString();
+
+          AgentTarget expectedTarget =
+              new AgentTarget(
+                  agent1Wrapper.get().getId(),
+                  agent1Wrapper.get().getTargetDisplayName(),
+                  Set.of(),
+                  agent1Wrapper.get().getAsset().getId(),
+                  agent1Wrapper.get().getExecutor().getType());
+          List<AgentTarget> expected = List.of(expectedTarget);
+
+          assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+        }
+      }
     }
+  }
 
-    @Test
-    @DisplayName(
-        "When are part of a group, 'contains' operator on this group should include endpoints of this group and exclude others")
-    public void whenArePartOfAGroupAndGroupSelectedAsFilter_thenIncludeOnlyEndpointsOfGroup()
-        throws Exception {
-      InjectComposer.Composer injectWrapper = getInjectWrapper();
+  @Nested
+  @WithMockAdminUser
+  @DisplayName("With endpoint search")
+  public class WithEndpointSearch {
+    private final TargetType targetType = TargetType.ASSETS;
 
-      List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
-      Filters.Filter filter = new Filters.Filter();
-      filter.setKey("endpoint_platform");
-      filter.setMode(Filters.FilterMode.and);
-      filter.setOperator(Filters.FilterOperator.eq);
-      filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
-      assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
-
-      // create several endpoints; only the Windows (first) endpoint should be involved in the above
-      // groups
-      EndpointComposer.Composer ep1Wrapper =
-          endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
-
-      // create a new endpoint that is not part of the above groups but will be targeted explicitly
-      Endpoint ep2 = EndpointFixture.createEndpoint();
-      ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
-      EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
-
-      // create a new endpoint that is not part of the above groups
-      Endpoint ep3 = EndpointFixture.createEndpoint();
-      ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
-      endpointComposer.forEndpoint(ep3).persist();
-
-      assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
-      // add two endpoints as direct targets, including the endpoint part of the excluded group
-      injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep1Wrapper);
-      Inject inject = injectWrapper.persist().get();
+    @Nested
+    @DisplayName("When getting options for inject targets")
+    public class WhenGettingOptionsForInjectTargets {
+      @Test
+      @DisplayName("/options should return all possible targets")
+      public void whenEndpointsAreFilters_returnAllPossibleTargets() throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
 
-      entityManager.flush();
-      entityManager.clear();
+        // dynamic group 1
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        AssetGroupComposer.Composer assetGroupWrapper = getAssetGroupWrapperWithFilter(filter);
 
-      SearchPaginationInput search =
-          PaginationFixture.simpleFilter(
-              "target_asset_groups",
-              assetGroupWrappers.getFirst().get().getId(),
-              Filters.FilterOperator.contains);
+        // 2
+        Filters.Filter filter2 = new Filters.Filter();
+        filter2.setKey("endpoint_platform");
+        filter2.setMode(Filters.FilterMode.and);
+        filter2.setOperator(Filters.FilterOperator.eq);
+        filter2.setValues(List.of(Endpoint.PLATFORM_TYPE.Linux.name()));
+        AssetGroupComposer.Composer assetGroupWrapper2 = getAssetGroupWrapperWithFilter(filter2);
 
-      String response =
-          mvc.perform(
-                  post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
-                      .contentType(MediaType.APPLICATION_JSON)
-                      .content(mapper.writeValueAsString(search)))
-              .andExpect(status().isOk())
-              .andReturn()
-              .getResponse()
-              .getContentAsString();
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
 
-      EndpointTarget expectedTarget =
-          new EndpointTarget(
-              ep1Wrapper.get().getId(),
-              ep1Wrapper.get().getName(),
-              ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
-              ep1Wrapper.get().getPlatform().name());
-      List<EndpointTarget> expected = List.of(expectedTarget);
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
 
-      assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+        // create a new endpoint that is not part of the above groups
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).persist();
+
+        injectWrapper.withAssetGroup(assetGroupWrapper).withAssetGroup(assetGroupWrapper2);
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        String response =
+            mvc.perform(
+                    get(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/options")
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        List<FilterUtilsJpa.Option> expected =
+            List.of(
+                new FilterUtilsJpa.Option(ep1Wrapper.get().getId(), ep1Wrapper.get().getName()),
+                new FilterUtilsJpa.Option(ep2Wrapper.get().getId(), ep2Wrapper.get().getName()));
+
+        assertThatJson(response).isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName("/options should return all targets matching search text")
+      public void whenEndpointsAreFilters_returnAllTargetsMatchingSearchText() throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        // dynamic group 1
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        AssetGroupComposer.Composer assetGroupWrapper = getAssetGroupWrapperWithFilter(filter);
+
+        // 2
+        Filters.Filter filter2 = new Filters.Filter();
+        filter2.setKey("endpoint_platform");
+        filter2.setMode(Filters.FilterMode.and);
+        filter2.setOperator(Filters.FilterOperator.eq);
+        filter2.setValues(List.of(Endpoint.PLATFORM_TYPE.Linux.name()));
+        AssetGroupComposer.Composer assetGroupWrapper2 = getAssetGroupWrapperWithFilter(filter2);
+
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        ep2.setName("search this name!");
+        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).persist();
+
+        injectWrapper.withAssetGroup(assetGroupWrapper).withAssetGroup(assetGroupWrapper2);
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        String response =
+            mvc.perform(
+                    get(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/options")
+                        .queryParam("searchText", "search this")
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        List<FilterUtilsJpa.Option> expected =
+            List.of(
+                new FilterUtilsJpa.Option(ep2Wrapper.get().getId(), ep2Wrapper.get().getName()));
+
+        assertThatJson(response).isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName("/options by id should return only options matching ids")
+      public void whenEndpointsAreFilters_returnOnlyOptionsMatchingIds() throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        // dynamic group 1
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        AssetGroupComposer.Composer assetGroupWrapper = getAssetGroupWrapperWithFilter(filter);
+
+        // 2
+        Filters.Filter filter2 = new Filters.Filter();
+        filter2.setKey("endpoint_platform");
+        filter2.setMode(Filters.FilterMode.and);
+        filter2.setOperator(Filters.FilterOperator.eq);
+        filter2.setValues(List.of(Endpoint.PLATFORM_TYPE.Linux.name()));
+        AssetGroupComposer.Composer assetGroupWrapper2 = getAssetGroupWrapperWithFilter(filter2);
+
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).persist();
+
+        injectWrapper
+            .withAssetGroup(assetGroupWrapper)
+            .withAssetGroup(assetGroupWrapper2)
+            .persist();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        List<String> ids = List.of(ep1Wrapper.get().getId());
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/targets/" + targetType + "/options")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(ids)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        List<FilterUtilsJpa.Option> expected =
+            List.of(
+                new FilterUtilsJpa.Option(ep1Wrapper.get().getId(), ep1Wrapper.get().getName()));
+
+        assertThatJson(response).isEqualTo(mapper.writeValueAsString(expected));
+      }
+    }
+
+    @Nested
+    @DisplayName("With OR group operator")
+    public class WithORGroupOperator {
+      @Test
+      @DisplayName(
+          "When endpoints are explicitly targeted, 'is empty' Asset group filter should only return those that are not in any group'")
+      public void whenEndpointsAreExplicitlyTargeted_thenReturnOnlyEndpointsWithNoGroup()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        ep2.setName("target endpoint 2");
+        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is both part of the above groups and also will be targeted
+        // explicitly
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setName("target endpoint 3");
+        EndpointComposer.Composer ep3Wrapper = endpointComposer.forEndpoint(ep3).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint notTarget = EndpointFixture.createEndpoint();
+        notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        notTarget.setName("not target");
+        endpointComposer.forEndpoint(notTarget).persist();
+
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep3Wrapper);
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithOrOperator(
+                "target_asset_groups", null, Filters.FilterOperator.not_empty);
+        // add another filter that would include notTarget endpoint
+        Filters.Filter otherFilter = new Filters.Filter();
+        otherFilter.setKey("target_name");
+        otherFilter.setMode(Filters.FilterMode.and);
+        otherFilter.setOperator(Filters.FilterOperator.contains);
+        otherFilter.setValues(List.of("target"));
+        search.getFilterGroup().getFilters().add(otherFilter);
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        List<EndpointTarget> expected =
+            List.of(
+                new EndpointTarget(
+                    ep1Wrapper.get().getId(),
+                    ep1Wrapper.get().getName(),
+                    ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                    ep1Wrapper.get().getPlatform().name()),
+                new EndpointTarget(
+                    ep2Wrapper.get().getId(),
+                    ep2Wrapper.get().getName(),
+                    ep2Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                    ep2Wrapper.get().getPlatform().name()),
+                new EndpointTarget(
+                    ep3Wrapper.get().getId(),
+                    ep3Wrapper.get().getName(),
+                    ep3Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                    ep3Wrapper.get().getPlatform().name()));
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
+    }
+
+    @Nested
+    @DisplayName("With AND filter group operator")
+    class WithANDFilterGroupOperator {
+      @Test
+      @DisplayName(
+          "When endpoint is dynamic asset in group, they are targets of inject if group is a target")
+      public void whenEndpointIsDynamicAssetInGroup_thenTargetIsDynamicAssetInGroup()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        // dynamic group 1
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        AssetGroupComposer.Composer assetGroupWrapper = getAssetGroupWrapperWithFilter(filter);
+
+        // 2
+        Filters.Filter filter2 = new Filters.Filter();
+        filter2.setKey("endpoint_platform");
+        filter2.setMode(Filters.FilterMode.and);
+        filter2.setOperator(Filters.FilterOperator.eq);
+        filter2.setValues(List.of(Endpoint.PLATFORM_TYPE.Linux.name()));
+        AssetGroupComposer.Composer assetGroupWrapper2 = getAssetGroupWrapperWithFilter(filter2);
+
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).persist();
+
+        injectWrapper
+            .withAssetGroup(assetGroupWrapper)
+            .withExpectation(
+                expectationComposer
+                    .forExpectation(
+                        InjectExpectationFixture.createExpectationWithTypeAndStatus(
+                            InjectExpectation.EXPECTATION_TYPE.DETECTION,
+                            InjectExpectation.EXPECTATION_STATUS.SUCCESS))
+                    .withAssetGroup(assetGroupWrapper)
+                    .withEndpoint(ep1Wrapper))
+            .withExpectation(
+                expectationComposer
+                    .forExpectation(
+                        InjectExpectationFixture.createExpectationWithTypeAndStatus(
+                            InjectExpectation.EXPECTATION_TYPE.PREVENTION,
+                            InjectExpectation.EXPECTATION_STATUS.PARTIAL))
+                    .withAssetGroup(assetGroupWrapper)
+                    .withEndpoint(ep1Wrapper))
+            .withAssetGroup(assetGroupWrapper2)
+            .withExpectation(
+                expectationComposer
+                    .forExpectation(
+                        InjectExpectationFixture.createExpectationWithTypeAndStatus(
+                            InjectExpectation.EXPECTATION_TYPE.DETECTION,
+                            InjectExpectation.EXPECTATION_STATUS.SUCCESS))
+                    .withAssetGroup(assetGroupWrapper2)
+                    .withEndpoint(ep2Wrapper))
+            .withExpectation(
+                expectationComposer
+                    .forExpectation(
+                        InjectExpectationFixture.createExpectationWithTypeAndStatus(
+                            InjectExpectation.EXPECTATION_TYPE.PREVENTION,
+                            InjectExpectation.EXPECTATION_STATUS.PARTIAL))
+                    .withAssetGroup(assetGroupWrapper2)
+                    .withEndpoint(ep2Wrapper));
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        SearchPaginationInput search = PaginationFixture.getDefault().build();
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        EndpointTarget expectedTarget1 =
+            new EndpointTarget(
+                ep1Wrapper.get().getId(),
+                ep1Wrapper.get().getName(),
+                ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                ep1Wrapper.get().getPlatform().name());
+        expectedTarget1.setTargetDetectionStatus(InjectExpectation.EXPECTATION_STATUS.SUCCESS);
+        expectedTarget1.setTargetPreventionStatus(InjectExpectation.EXPECTATION_STATUS.PARTIAL);
+        expectedTarget1.setTargetHumanResponseStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
+        expectedTarget1.setTargetExecutionStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
+        EndpointTarget expectedTarget2 =
+            new EndpointTarget(
+                ep2Wrapper.get().getId(),
+                ep2Wrapper.get().getName(),
+                ep2Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                ep2Wrapper.get().getPlatform().name());
+        expectedTarget2.setTargetDetectionStatus(InjectExpectation.EXPECTATION_STATUS.SUCCESS);
+        expectedTarget2.setTargetPreventionStatus(InjectExpectation.EXPECTATION_STATUS.PARTIAL);
+        expectedTarget2.setTargetHumanResponseStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
+        expectedTarget2.setTargetExecutionStatus(InjectExpectation.EXPECTATION_STATUS.UNKNOWN);
+        // expect two out of three endpoints in the resultset, i.e. not the extra one
+        List<EndpointTarget> expected = List.of(expectedTarget1, expectedTarget2);
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName("When dynamic groups intersect, common endpoints are returned only once")
+      public void whenDynamicGroupsIntersect_thenReturnCommonEndpointsOnce() throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+
+        // create two identical asset groups
+        for (int i = 0; i < 2; i++) {
+          Filters.Filter filter = new Filters.Filter();
+          filter.setKey("endpoint_platform");
+          filter.setMode(Filters.FilterMode.and);
+          filter.setOperator(Filters.FilterOperator.eq);
+          filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+          assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+        }
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).persist();
+
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_name", "", Filters.FilterOperator.contains);
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        EndpointTarget expectedTarget =
+            new EndpointTarget(
+                ep1Wrapper.get().getId(),
+                ep1Wrapper.get().getName(),
+                ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                ep1Wrapper.get().getPlatform().name());
+        List<EndpointTarget> expected = List.of(expectedTarget);
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName(
+          "When no applied AssetGroup filter, at most a full page of targeted endpoints should be returned")
+      public void whenNoAppliedAssetGroupFilter_returnPageOfAllTargetedEndpoints()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is both part of the above groups and also will be targeted
+        // explicitly
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        EndpointComposer.Composer ep3Wrapper = endpointComposer.forEndpoint(ep3).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint notTarget = EndpointFixture.createEndpoint();
+        notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(notTarget).persist();
+
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep3Wrapper);
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        SearchPaginationInput search = PaginationFixture.getDefault().build();
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        List<EndpointTarget> expected =
+            List.of(
+                new EndpointTarget(
+                    ep1Wrapper.get().getId(),
+                    ep1Wrapper.get().getName(),
+                    ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                    ep1Wrapper.get().getPlatform().name()),
+                new EndpointTarget(
+                    ep2Wrapper.get().getId(),
+                    ep2Wrapper.get().getName(),
+                    ep2Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                    ep2Wrapper.get().getPlatform().name()),
+                new EndpointTarget(
+                    ep3Wrapper.get().getId(),
+                    ep3Wrapper.get().getName(),
+                    ep3Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                    ep3Wrapper.get().getPlatform().name()));
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName(
+          "When endpoints are explicitly targeted, 'is empty' Asset group filter should only return those that are not in any group'")
+      public void whenEndpointsAreExplicitlyTargeted_thenReturnOnlyEndpointsWithNoGroup()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is both part of the above groups and also will be targeted
+        // explicitly
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        EndpointComposer.Composer ep3Wrapper = endpointComposer.forEndpoint(ep3).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint notTarget = EndpointFixture.createEndpoint();
+        notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(notTarget).persist();
+
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep3Wrapper);
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_asset_groups", null, Filters.FilterOperator.empty);
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        EndpointTarget expectedTarget =
+            new EndpointTarget(
+                ep2Wrapper.get().getId(),
+                ep2Wrapper.get().getName(),
+                ep2Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                ep2Wrapper.get().getPlatform().name());
+        List<EndpointTarget> expected = List.of(expectedTarget);
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName(
+          "When endpoints are explicitly targeted, 'not empty' Asset group filter should only return those that are in any group'")
+      public void whenEndpointsAreExplicitlyTargeted_thenReturnOnlyEndpointsWithGroup()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint notTarget = EndpointFixture.createEndpoint();
+        notTarget.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(notTarget).persist();
+
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        injectWrapper.withEndpoint(ep2Wrapper);
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_asset_groups", null, Filters.FilterOperator.not_empty);
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        EndpointTarget expectedTarget =
+            new EndpointTarget(
+                ep1Wrapper.get().getId(),
+                ep1Wrapper.get().getName(),
+                ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                ep1Wrapper.get().getPlatform().name());
+        List<EndpointTarget> expected = List.of(expectedTarget);
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName(
+          "When are part of a group, 'not contains' operator on this group should exclude endpoint")
+      public void whenArePartOfAGroupAndGroupFilteredOut_thenExcludeAllEndpointsOfGroup()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).persist();
+
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        Inject inject = injectWrapper.withEndpoint(ep2Wrapper).persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_asset_groups",
+                assetGroupWrappers.getFirst().get().getId(),
+                Filters.FilterOperator.not_contains);
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        EndpointTarget expectedTarget =
+            new EndpointTarget(
+                ep2Wrapper.get().getId(),
+                ep2Wrapper.get().getName(),
+                ep2Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                ep2Wrapper.get().getPlatform().name());
+        List<EndpointTarget> expected = List.of(expectedTarget);
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
+
+      @Test
+      @DisplayName(
+          "When are part of a group, 'contains' operator on this group should include endpoints of this group and exclude others")
+      public void whenArePartOfAGroupAndGroupSelectedAsFilter_thenIncludeOnlyEndpointsOfGroup()
+          throws Exception {
+        InjectComposer.Composer injectWrapper = getInjectWrapper();
+
+        List<AssetGroupComposer.Composer> assetGroupWrappers = new ArrayList<>();
+        Filters.Filter filter = new Filters.Filter();
+        filter.setKey("endpoint_platform");
+        filter.setMode(Filters.FilterMode.and);
+        filter.setOperator(Filters.FilterOperator.eq);
+        filter.setValues(List.of(Endpoint.PLATFORM_TYPE.Windows.name()));
+        assetGroupWrappers.add(getAssetGroupWrapperWithFilter(filter));
+
+        // create several endpoints; only the Windows (first) endpoint should be involved in the
+        // above
+        // groups
+        EndpointComposer.Composer ep1Wrapper =
+            endpointComposer.forEndpoint(EndpointFixture.createEndpoint()).persist();
+
+        // create a new endpoint that is not part of the above groups but will be targeted
+        // explicitly
+        Endpoint ep2 = EndpointFixture.createEndpoint();
+        ep2.setPlatform(Endpoint.PLATFORM_TYPE.Linux);
+        EndpointComposer.Composer ep2Wrapper = endpointComposer.forEndpoint(ep2).persist();
+
+        // create a new endpoint that is not part of the above groups
+        Endpoint ep3 = EndpointFixture.createEndpoint();
+        ep3.setPlatform(Endpoint.PLATFORM_TYPE.MacOS);
+        endpointComposer.forEndpoint(ep3).persist();
+
+        assetGroupWrappers.forEach(injectWrapper::withAssetGroup);
+        // add two endpoints as direct targets, including the endpoint part of the excluded group
+        injectWrapper.withEndpoint(ep2Wrapper).withEndpoint(ep1Wrapper);
+        Inject inject = injectWrapper.persist().get();
+
+        entityManager.flush();
+        entityManager.clear();
+
+        SearchPaginationInput search =
+            PaginationFixture.simpleSearchWithAndOperator(
+                "target_asset_groups",
+                assetGroupWrappers.getFirst().get().getId(),
+                Filters.FilterOperator.contains);
+
+        String response =
+            mvc.perform(
+                    post(INJECT_URI + "/" + inject.getId() + "/targets/" + targetType + "/search")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(mapper.writeValueAsString(search)))
+                .andExpect(status().isOk())
+                .andReturn()
+                .getResponse()
+                .getContentAsString();
+
+        EndpointTarget expectedTarget =
+            new EndpointTarget(
+                ep1Wrapper.get().getId(),
+                ep1Wrapper.get().getName(),
+                ep1Wrapper.get().getTags().stream().map(Tag::getId).collect(Collectors.toSet()),
+                ep1Wrapper.get().getPlatform().name());
+        List<EndpointTarget> expected = List.of(expectedTarget);
+
+        assertThatJson(response).node("content").isEqualTo(mapper.writeValueAsString(expected));
+      }
     }
   }
 
@@ -952,7 +2305,7 @@ public void withNoAssetGroupTargets_returnNoItemsInPage() throws Exception {
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter(
+          PaginationFixture.simpleSearchWithAndOperator(
               "target_name", "target asset group", Filters.FilterOperator.eq);
       String response =
           mvc.perform(
@@ -985,7 +2338,7 @@ public void withSomeAssetGroupTargets_returnMatchingItemsInPage1() throws Except
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter(
+          PaginationFixture.simpleSearchWithAndOperator(
               "target_name", searchTerm, Filters.FilterOperator.contains);
       String response =
           mvc.perform(
@@ -1032,7 +2385,7 @@ public void withSomeAssetGroupTargets_returnMatchingItemsInPage2() throws Except
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter(
+          PaginationFixture.simpleSearchWithAndOperator(
               "target_name", searchTerm, Filters.FilterOperator.contains);
       search.setPage(1); // 0-based
       String response =
@@ -1107,7 +2460,7 @@ public void givenSpecificScores_expectationTypeResultsAreOfCorrectStatus() throw
         entityManager.clear();
 
         SearchPaginationInput search =
-            PaginationFixture.simpleFilter(
+            PaginationFixture.simpleSearchWithAndOperator(
                 "target_name", searchTerm, Filters.FilterOperator.contains);
         String response =
             mvc.perform(
@@ -1367,7 +2720,8 @@ public void withNoTeamTargets_returnNoItemsInPage() throws Exception {
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter("target_name", "target team", Filters.FilterOperator.eq);
+          PaginationFixture.simpleSearchWithAndOperator(
+              "target_name", "target team", Filters.FilterOperator.eq);
       String response =
           mvc.perform(
                   post(INJECT_URI
@@ -1399,7 +2753,7 @@ public void withSomeTeamTargets_returnMatchingItemsInPage1() throws Exception {
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter(
+          PaginationFixture.simpleSearchWithAndOperator(
               "target_name", searchTerm, Filters.FilterOperator.contains);
       String response =
           mvc.perform(
@@ -1444,7 +2798,7 @@ public void withSomeTeamTargets_returnMatchingItemsInPage2() throws Exception {
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter(
+          PaginationFixture.simpleSearchWithAndOperator(
               "target_name", searchTerm, Filters.FilterOperator.contains);
       search.setPage(1); // 0-based
       String response =
@@ -1503,7 +2857,7 @@ public void withSomeTeamsTargets_returnMatchingItemsForExercisesWithInjectAllTea
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter(
+          PaginationFixture.simpleSearchWithAndOperator(
               "target_name", "team target", Filters.FilterOperator.contains);
       String response =
           mvc.perform(
@@ -1561,7 +2915,7 @@ public void givenSpecificScores_expectationTypeResultsAreOfCorrectStatus() throw
         entityManager.clear();
 
         SearchPaginationInput search =
-            PaginationFixture.simpleFilter(
+            PaginationFixture.simpleSearchWithAndOperator(
                 "target_name", searchTerm, Filters.FilterOperator.contains);
         String response =
             mvc.perform(
@@ -1610,7 +2964,8 @@ public void withNoPlayerTargets_returnNoItemsInPage() throws Exception {
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter("", "target player", Filters.FilterOperator.eq);
+          PaginationFixture.simpleSearchWithAndOperator(
+              "", "target player", Filters.FilterOperator.eq);
       String response =
           mvc.perform(
                   post(INJECT_URI
@@ -1648,7 +3003,8 @@ public void withSomePlayersTargets_returnMatchingItemsInPage1() throws Exception
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter("", searchTerm, Filters.FilterOperator.contains);
+          PaginationFixture.simpleSearchWithAndOperator(
+              "", searchTerm, Filters.FilterOperator.contains);
       String response =
           mvc.perform(
                   post(INJECT_URI
@@ -1701,7 +3057,8 @@ public void withSomePlayersTargets_returnMatchingItemsInPage2() throws Exception
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter("", searchTerm, Filters.FilterOperator.contains);
+          PaginationFixture.simpleSearchWithAndOperator(
+              "", searchTerm, Filters.FilterOperator.contains);
       search.setPage(1); // 0-based
       String response =
           mvc.perform(
@@ -1762,7 +3119,8 @@ public void withSomePlayersTargets_returnMatchingItemsForExercisesWithInjectAllT
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter("", "player target", Filters.FilterOperator.contains);
+          PaginationFixture.simpleSearchWithAndOperator(
+              "", "player target", Filters.FilterOperator.contains);
       String response =
           mvc.perform(
                   post(INJECT_URI
@@ -1820,7 +3178,8 @@ public void withSomePlayersTargets_returnMatchingItemsForExercisesWithInjectWith
       entityManager.clear();
 
       SearchPaginationInput search =
-          PaginationFixture.simpleFilter("", "player target", Filters.FilterOperator.contains);
+          PaginationFixture.simpleSearchWithAndOperator(
+              "", "player target", Filters.FilterOperator.contains);
       String response =
           mvc.perform(
                   post(INJECT_URI
@@ -1883,7 +3242,8 @@ public void givenSpecificScores_expectationTypeResultsAreOfCorrectStatus() throw
         entityManager.clear();
 
         SearchPaginationInput search =
-            PaginationFixture.simpleFilter("", searchTerm, Filters.FilterOperator.contains);
+            PaginationFixture.simpleSearchWithAndOperator(
+                "", searchTerm, Filters.FilterOperator.contains);
         String response =
             mvc.perform(
                     post(INJECT_URI
diff --git a/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioApiSearchTest.java b/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioApiSearchTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioApiSearchTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioApiSearchTest.java
@@ -144,7 +144,7 @@ class FilteringPageOfScenarios {
       void given_filter_input_by_name_should_return_a_page_of_scenarios_filter_by_name()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("scenario_name", "Crisis", contains);
+            PaginationFixture.simpleSearchWithAndOperator("scenario_name", "Crisis", contains);
 
         mvc.perform(
                 post(SCENARIO_URI + "/search")
@@ -159,7 +159,8 @@ void given_filter_input_by_name_should_return_a_page_of_scenarios_filter_by_name
       void given_filter_input_by_category_should_return_a_page_of_scenarios_filter_by_category()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("scenario_category", "incident-response", contains);
+            PaginationFixture.simpleSearchWithAndOperator(
+                "scenario_category", "incident-response", contains);
 
         mvc.perform(
                 post(SCENARIO_URI + "/search")
@@ -174,7 +175,8 @@ void given_filter_input_by_category_should_return_a_page_of_scenarios_filter_by_
       void given_filter_input_by_severity_should_return_a_page_of_scenarios_filter_by_severity()
           throws Exception {
         SearchPaginationInput searchPaginationInput =
-            PaginationFixture.simpleFilter("scenario_severity", valueOf(critical), contains);
+            PaginationFixture.simpleSearchWithAndOperator(
+                "scenario_severity", valueOf(critical), contains);
 
         mvc.perform(
                 post(SCENARIO_URI + "/search")
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/PaginationFixture.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/PaginationFixture.java
--- a/openbas-api/src/test/java/io/openbas/utils/fixtures/PaginationFixture.java
+++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/PaginationFixture.java
@@ -2,6 +2,7 @@
 
 import io.openbas.database.model.Filters;
 import io.openbas.utils.pagination.SearchPaginationInput;
+import java.util.ArrayList;
 import java.util.List;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
@@ -12,7 +13,7 @@ public static SearchPaginationInput.SearchPaginationInputBuilder getDefault() {
     return SearchPaginationInput.builder().page(0).size(10);
   }
 
-  public static SearchPaginationInput simpleFilter(
+  public static SearchPaginationInput simpleSearchWithAndOperator(
       String key, String value, Filters.FilterOperator operator) {
     Filters.Filter filter = new Filters.Filter();
     filter.setKey(key);
@@ -20,7 +21,19 @@ public static SearchPaginationInput simpleFilter(
     filter.setOperator(operator);
     Filters.FilterGroup filterGroup = new Filters.FilterGroup();
     filterGroup.setMode(Filters.FilterMode.and);
-    filterGroup.setFilters(List.of(filter));
+    filterGroup.setFilters(new ArrayList<>(List.of(filter)));
+    return getDefault().filterGroup(filterGroup).build();
+  }
+
+  public static SearchPaginationInput simpleSearchWithOrOperator(
+      String key, String value, Filters.FilterOperator operator) {
+    Filters.Filter filter = new Filters.Filter();
+    filter.setKey(key);
+    filter.setValues(value == null ? List.of() : List.of(value));
+    filter.setOperator(operator);
+    Filters.FilterGroup filterGroup = new Filters.FilterGroup();
+    filterGroup.setMode(Filters.FilterMode.or);
+    filterGroup.setFilters(new ArrayList<>(List.of(filter)));
     return getDefault().filterGroup(filterGroup).build();
   }
 
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/AgentComposer.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/AgentComposer.java
--- a/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/AgentComposer.java
+++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/AgentComposer.java
@@ -2,6 +2,7 @@
 
 import io.openbas.database.model.Agent;
 import io.openbas.database.repository.AgentRepository;
+import java.util.Optional;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -11,19 +12,28 @@ public class AgentComposer extends ComposerBase<Agent> {
 
   public class Composer extends InnerComposerBase<Agent> {
     private final Agent agent;
+    private Optional<ExecutorComposer.Composer> executorComposer = Optional.empty();
 
     public Composer(Agent agent) {
       this.agent = agent;
     }
 
+    public Composer withExecutor(ExecutorComposer.Composer executorComposer) {
+      this.executorComposer = Optional.of(executorComposer);
+      this.agent.setExecutor(executorComposer.get());
+      return this;
+    }
+
     @Override
     public AgentComposer.Composer persist() {
+      executorComposer.ifPresent(ExecutorComposer.Composer::persist);
       agentRepository.save(agent);
       return this;
     }
 
     @Override
     public AgentComposer.Composer delete() {
+      executorComposer.ifPresent(ExecutorComposer.Composer::delete);
       agentRepository.delete(agent);
       return this;
     }
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/EndpointComposer.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/EndpointComposer.java
--- a/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/EndpointComposer.java
+++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/EndpointComposer.java
@@ -2,9 +2,11 @@
 
 import io.openbas.database.model.Agent;
 import io.openbas.database.model.Endpoint;
+import io.openbas.database.model.Tag;
 import io.openbas.database.repository.EndpointRepository;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -15,6 +17,7 @@ public class EndpointComposer extends ComposerBase<Endpoint> {
   public class Composer extends InnerComposerBase<Endpoint> {
     private final Endpoint endpoint;
     private final List<AgentComposer.Composer> agentComposers = new ArrayList<>();
+    private final List<TagComposer.Composer> tagComposers = new ArrayList<>();
 
     public Composer(Endpoint endpoint) {
       this.endpoint = endpoint;
@@ -30,15 +33,25 @@ public Composer withAgent(AgentComposer.Composer agentComposer) {
       return this;
     }
 
+    public Composer withTag(TagComposer.Composer tagComposer) {
+      tagComposers.add(tagComposer);
+      Set<Tag> tags = endpoint.getTags();
+      tags.add(tagComposer.get());
+      this.endpoint.setTags(tags);
+      return this;
+    }
+
     @Override
     public Composer persist() {
       endpointRepository.save(endpoint);
       agentComposers.forEach(AgentComposer.Composer::persist);
+      tagComposers.forEach(TagComposer.Composer::persist);
       return this;
     }
 
     @Override
     public Composer delete() {
+      tagComposers.forEach(TagComposer.Composer::delete);
       agentComposers.forEach(AgentComposer.Composer::delete);
       endpointRepository.delete(endpoint);
       return this;
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/ExecutorComposer.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/ExecutorComposer.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/ExecutorComposer.java
@@ -0,0 +1,41 @@
+package io.openbas.utils.fixtures.composers;
+
+import io.openbas.database.model.Executor;
+import io.openbas.database.repository.ExecutorRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ExecutorComposer extends ComposerBase<Executor> {
+  @Autowired private ExecutorRepository executorRepository;
+
+  public class Composer extends InnerComposerBase<Executor> {
+    private final Executor executor;
+
+    public Composer(Executor executor) {
+      this.executor = executor;
+    }
+
+    @Override
+    public Composer persist() {
+      executorRepository.save(executor);
+      return this;
+    }
+
+    @Override
+    public Composer delete() {
+      executorRepository.delete(executor);
+      return this;
+    }
+
+    @Override
+    public Executor get() {
+      return this.executor;
+    }
+  }
+
+  public Composer forExecutor(Executor executor) {
+    generatedItems.add(executor);
+    return new Composer(executor);
+  }
+}
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/ExpectationComposer.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/ExpectationComposer.java
--- a/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/ExpectationComposer.java
+++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/ExpectationComposer.java
@@ -16,6 +16,7 @@ public class Composer extends InnerComposerBase<InjectExpectation> {
     private Optional<TeamComposer.Composer> teamComposer = Optional.empty();
     private Optional<UserComposer.Composer> userComposer = Optional.empty();
     private Optional<EndpointComposer.Composer> endpointComposer = Optional.empty();
+    private Optional<AgentComposer.Composer> agentComposer = Optional.empty();
 
     public Composer(InjectExpectation injectExpectation) {
       this.injectExpectation = injectExpectation;
@@ -45,10 +46,18 @@ public Composer withEndpoint(EndpointComposer.Composer endpointComposer) {
       return this;
     }
 
+    public Composer withAgent(AgentComposer.Composer agentComposer) {
+      this.agentComposer = Optional.of(agentComposer);
+      this.injectExpectation.setAgent(agentComposer.get());
+      this.injectExpectation.setAsset(agentComposer.get().getAsset());
+      return this;
+    }
+
     @Override
     public Composer persist() {
       assetGroupComposer.ifPresent(AssetGroupComposer.Composer::persist);
       endpointComposer.ifPresent(EndpointComposer.Composer::persist);
+      agentComposer.ifPresent(AgentComposer.Composer::persist);
       teamComposer.ifPresent(TeamComposer.Composer::persist);
       userComposer.ifPresent(UserComposer.Composer::persist);
       injectExpectationRepository.save(injectExpectation);
@@ -59,6 +68,7 @@ public Composer persist() {
     public InnerComposerBase<InjectExpectation> delete() {
       assetGroupComposer.ifPresent(AssetGroupComposer.Composer::delete);
       endpointComposer.ifPresent(EndpointComposer.Composer::delete);
+      agentComposer.ifPresent(AgentComposer.Composer::delete);
       teamComposer.ifPresent(TeamComposer.Composer::delete);
       userComposer.ifPresent(UserComposer.Composer::delete);
       injectExpectationRepository.delete(injectExpectation);
diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTesting.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTesting.tsx
--- a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTesting.tsx
+++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTesting.tsx
@@ -60,6 +60,8 @@ const AtomicTesting = () => {
   const [hasAssetsGroupChecked, setHasAssetsGroupChecked] = useState(false);
   const [hasEndpoints, setHasEndpoints] = useState(false);
   const [hasEndpointsChecked, setHasEndpointsChecked] = useState(false);
+  const [hasAgents, setHasAgents] = useState(false);
+  const [hasAgentsChecked, setHasAgentsChecked] = useState(false);
   const [reloadContentCount, setReloadContentCount] = useState(0);
   const [hasTeams, setHasTeams] = useState(false);
   const [hasTeamsChecked, setHasTeamsChecked] = useState(false);
@@ -107,6 +109,14 @@ const AtomicTesting = () => {
         entityPrefix: 'player_target',
       });
     }
+    if (hasAgents) {
+      tabs.push({
+        key: index++,
+        label: t('Agents'),
+        type: 'AGENT',
+        entityPrefix: 'agent_target',
+      });
+    }
 
     tabs.push({
       key: index++,
@@ -116,7 +126,7 @@ const AtomicTesting = () => {
     });
 
     return tabs;
-  }, [hasAssetsGroup, hasTeams, hasEndpoints, hasPlayers]);
+  }, [hasAssetsGroup, hasTeams, hasEndpoints, hasAgents, hasPlayers]);
 
   const injectId = injectResultOverviewOutput?.inject_id || '';
 
@@ -172,6 +182,16 @@ const AtomicTesting = () => {
         setHasPlayersChecked(true);
       });
 
+    searchTargets(injectId, 'AGENT', searchPaginationInput1Result)
+      .then((response) => {
+        if (response.data.content.length > 0) {
+          setHasAgents(true);
+        } else { setHasAgents(false); }
+      })
+      .finally(() => {
+        setHasAgentsChecked(true);
+      });
+
     setReloadContentCount(reloadContentCount + 1);
     setActiveTab(0);
   }, [injectResultOverviewOutput]);
@@ -281,7 +301,7 @@ const AtomicTesting = () => {
         </Typography>
         <div className="clearfix" />
         <Paper classes={{ root: classes.paper }} variant="outlined">
-          {hasAssetsGroupChecked && hasTeamsChecked && hasEndpointsChecked && hasPlayersChecked && (
+          {hasAssetsGroupChecked && hasTeamsChecked && hasEndpointsChecked && hasAgentsChecked && hasPlayersChecked && (
             <>
               <Tabs
                 value={activeTab}
diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/NewTargetListItem.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/NewTargetListItem.tsx
--- a/openbas-front/src/admin/components/atomic_testings/atomic_testing/NewTargetListItem.tsx
+++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/NewTargetListItem.tsx
@@ -37,6 +37,17 @@ const NewTargetListItem: React.FC<Props> = ({ onClick, target, selected }) => {
       ASSETS: <PlatformIcon platform={target?.target_subtype ?? 'Unknown'} width={20} marginRight={theme.spacing(2)} />,
       TEAMS: <Groups3Outlined />,
       PLAYERS: <PersonOutlined fontSize="small" />,
+      AGENT: (
+        <img
+          src={`/api/images/executors/icons/${target.target_subtype}`}
+          alt={target.target_subtype}
+          style={{
+            width: 20,
+            height: 20,
+            borderRadius: 4,
+          }}
+        />
+      ),
     };
 
     return iconMap[target.target_type];