diff --git a/openbas-api/src/main/java/io/openbas/importer/V1_DataImporter.java b/openbas-api/src/main/java/io/openbas/importer/V1_DataImporter.java index 95e4a9fe55..b0048454e0 100644 --- a/openbas-api/src/main/java/io/openbas/importer/V1_DataImporter.java +++ b/openbas-api/src/main/java/io/openbas/importer/V1_DataImporter.java @@ -284,7 +284,10 @@ private Scenario importScenario(JsonNode importNode, Map baseIds) scenario.setSubtitle(scenarioNode.get("scenario_subtitle").textValue()); scenario.setCategory(scenarioNode.get("scenario_category").textValue()); scenario.setMainFocus(scenarioNode.get("scenario_main_focus").textValue()); - scenario.setSeverity(scenarioNode.get("scenario_severity").textValue()); + if (scenarioNode.get("scenario_severity") != null) { + String severity = scenarioNode.get("scenario_severity").textValue(); + scenario.setSeverity(Scenario.SEVERITY.valueOf(severity)); + } if (scenarioNode.get("scenario_recurrence") != null) { scenario.setRecurrence(scenarioNode.get("scenario_recurrence").textValue()); } diff --git a/openbas-api/src/main/java/io/openbas/rest/exercise/ExerciseApi.java b/openbas-api/src/main/java/io/openbas/rest/exercise/ExerciseApi.java index 8ef89b7da5..2a390cede8 100644 --- a/openbas-api/src/main/java/io/openbas/rest/exercise/ExerciseApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/exercise/ExerciseApi.java @@ -39,7 +39,6 @@ import java.io.InputStream; import java.time.Instant; import java.util.*; -import java.util.function.UnaryOperator; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -53,7 +52,6 @@ import static io.openbas.database.specification.ExerciseSpecification.findGrantedFor; import static io.openbas.helper.StreamHelper.fromIterable; import static io.openbas.helper.StreamHelper.iterableToSet; -import static io.openbas.rest.exercise.utils.ExerciseUtils.handleCustomFilter; import static io.openbas.service.ImportService.EXPORT_ENTRY_ATTACHMENT; import static io.openbas.service.ImportService.EXPORT_ENTRY_EXERCISE; import static io.openbas.utils.pagination.PaginationUtils.buildPaginationCriteriaBuilder; @@ -644,23 +642,16 @@ public List exercises() { @PostMapping(EXERCISE_URI + "/search") public Page exercises(@RequestBody @Valid final SearchPaginationInput searchPaginationInput) { - UnaryOperator> finalSpecification = handleCustomFilter( - searchPaginationInput - ); - if (currentUser().isAdmin()) { return buildPaginationCriteriaBuilder( - (Specification specification, Pageable pageable) -> this.exerciseService.exercises( - finalSpecification.apply(specification), - pageable - ), + this.exerciseService::exercises, searchPaginationInput, Exercise.class ); } else { return buildPaginationCriteriaBuilder( (Specification specification, Pageable pageable) -> this.exerciseService.exercises( - finalSpecification.apply(findGrantedFor(currentUser().getId()).and(specification)), + findGrantedFor(currentUser().getId()).and(specification), pageable ), searchPaginationInput, diff --git a/openbas-api/src/main/java/io/openbas/rest/exercise/utils/ExerciseUtils.java b/openbas-api/src/main/java/io/openbas/rest/exercise/utils/ExerciseUtils.java deleted file mode 100644 index 2f958d6b97..0000000000 --- a/openbas-api/src/main/java/io/openbas/rest/exercise/utils/ExerciseUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.openbas.rest.exercise.utils; - -import io.openbas.database.model.Exercise; -import io.openbas.utils.CustomFilterUtils; -import io.openbas.utils.pagination.SearchPaginationInput; -import org.jetbrains.annotations.NotNull; -import org.springframework.data.jpa.domain.Specification; - -import java.util.Collections; -import java.util.Map; -import java.util.function.UnaryOperator; - -public class ExerciseUtils { - - private static final String EXERCISE_KILL_CHAIN_PHASES_FILTER = "exercise_kill_chain_phases"; - private static final Map CORRESPONDENCE_MAP = Collections.singletonMap( - EXERCISE_KILL_CHAIN_PHASES_FILTER, "injects.injectorContract.attackPatterns.killChainPhases.id" - ); - - private ExerciseUtils() { - - } - - /** - * Manage filters that are not directly managed by the generic mechanics -> exercise_kill_chain_phases - */ - public static UnaryOperator> handleCustomFilter( - @NotNull final SearchPaginationInput searchPaginationInput) { - return CustomFilterUtils.handleCustomFilter( - searchPaginationInput, - EXERCISE_KILL_CHAIN_PHASES_FILTER, - CORRESPONDENCE_MAP - ); - } - -} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/ExerciseInjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/ExerciseInjectApi.java index df93fb25b5..a6a5a4d61d 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/ExerciseInjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/ExerciseInjectApi.java @@ -1,7 +1,7 @@ package io.openbas.rest.inject; +import io.openbas.database.model.Inject; import io.openbas.database.model.InjectTestStatus; -import io.openbas.database.specification.InjectSpecification; import io.openbas.rest.helper.RestBehavior; import io.openbas.rest.inject.output.InjectOutput; import io.openbas.service.InjectService; @@ -16,13 +16,17 @@ import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import java.util.List; +import static io.openbas.database.specification.InjectSpecification.fromExercise; import static io.openbas.rest.exercise.ExerciseApi.EXERCISE_URI; +import static io.openbas.utils.pagination.PaginationUtils.buildPaginationCriteriaBuilder; @RestController @RequiredArgsConstructor @@ -45,7 +49,22 @@ public class ExerciseInjectApi extends RestBehavior { @PreAuthorize("isExerciseObserver(#exerciseId)") @Transactional(readOnly = true) public Iterable exerciseInjectsSimple(@PathVariable @NotBlank final String exerciseId) { - return injectService.injects(InjectSpecification.fromExercise(exerciseId)); + return injectService.injects(fromExercise(exerciseId)); + } + + @PostMapping(EXERCISE_URI + "/{exerciseId}/injects/simple") + @PreAuthorize("isExerciseObserver(#exerciseId)") + @Transactional(readOnly = true) + public Iterable exerciseInjectsSimple( + @PathVariable @NotBlank final String exerciseId, + @RequestBody @Valid final SearchPaginationInput searchPaginationInput) { + return buildPaginationCriteriaBuilder( + (Specification specification, Pageable pageable) -> this.injectService.injects( + fromExercise(exerciseId).and(specification), pageable + ), + searchPaginationInput, + Inject.class + ); } @DeleteMapping(EXERCISE_URI + "/{exerciseId}/injects") diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/ScenarioInjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/ScenarioInjectApi.java index f5b8a1577e..3b1af35d9e 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/ScenarioInjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/ScenarioInjectApi.java @@ -1,7 +1,7 @@ package io.openbas.rest.inject; +import io.openbas.database.model.Inject; import io.openbas.database.model.InjectTestStatus; -import io.openbas.database.specification.InjectSpecification; import io.openbas.rest.helper.RestBehavior; import io.openbas.rest.inject.output.InjectOutput; import io.openbas.service.InjectService; @@ -11,13 +11,17 @@ import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import java.util.List; +import static io.openbas.database.specification.InjectSpecification.fromScenario; import static io.openbas.rest.scenario.ScenarioApi.SCENARIO_URI; +import static io.openbas.utils.pagination.PaginationUtils.buildPaginationCriteriaBuilder; @RestController @RequiredArgsConstructor @@ -31,7 +35,22 @@ public class ScenarioInjectApi extends RestBehavior { @PreAuthorize("isScenarioObserver(#scenarioId)") @Transactional(readOnly = true) public Iterable scenarioInjectsSimple(@PathVariable @NotBlank final String scenarioId) { - return injectService.injects(InjectSpecification.fromScenario(scenarioId)); + return injectService.injects(fromScenario(scenarioId)); + } + + @PostMapping(SCENARIO_URI + "/{scenarioId}/injects/simple") + @PreAuthorize("isScenarioObserver(#scenarioId)") + @Transactional(readOnly = true) + public Iterable scenarioInjectsSimple( + @PathVariable @NotBlank final String scenarioId, + @RequestBody @Valid final SearchPaginationInput searchPaginationInput) { + return buildPaginationCriteriaBuilder( + (Specification specification, Pageable pageable) -> this.injectService.injects( + fromScenario(scenarioId).and(specification), pageable + ), + searchPaginationInput, + Inject.class + ); } @DeleteMapping(SCENARIO_URI + "/{scenarioId}/injects") diff --git a/openbas-api/src/main/java/io/openbas/rest/injector_contract/InjectorContractApi.java b/openbas-api/src/main/java/io/openbas/rest/injector_contract/InjectorContractApi.java index b18adcd0cd..46a1af9ae5 100644 --- a/openbas-api/src/main/java/io/openbas/rest/injector_contract/InjectorContractApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/injector_contract/InjectorContractApi.java @@ -16,18 +16,14 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; import java.time.Instant; -import java.util.function.UnaryOperator; import static io.openbas.database.model.User.ROLE_ADMIN; import static io.openbas.helper.DatabaseHelper.updateRelation; import static io.openbas.helper.StreamHelper.fromIterable; -import static io.openbas.rest.injector_contract.utils.InjectorContractUtils.handleCustomFilter; import static io.openbas.utils.pagination.PaginationUtils.buildPaginationCriteriaBuilder; @RequiredArgsConstructor @@ -49,15 +45,8 @@ public Iterable injectContracts() { @PostMapping("/api/injector_contracts/search") public Page injectorContracts(@RequestBody @Valid final SearchPaginationInput searchPaginationInput) { - UnaryOperator> finalSpecification = handleCustomFilter( - searchPaginationInput - ); - return buildPaginationCriteriaBuilder( - (Specification specification, Pageable pageable) -> this.injectorContractService.injectorContracts( - finalSpecification.apply(specification), - pageable - ), + this.injectorContractService::injectorContracts, searchPaginationInput, InjectorContract.class ); diff --git a/openbas-api/src/main/java/io/openbas/rest/injector_contract/utils/InjectorContractUtils.java b/openbas-api/src/main/java/io/openbas/rest/injector_contract/utils/InjectorContractUtils.java deleted file mode 100644 index a899e520d2..0000000000 --- a/openbas-api/src/main/java/io/openbas/rest/injector_contract/utils/InjectorContractUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.openbas.rest.injector_contract.utils; - -import io.openbas.database.model.InjectorContract; -import io.openbas.utils.CustomFilterUtils; -import io.openbas.utils.pagination.SearchPaginationInput; -import org.jetbrains.annotations.NotNull; -import org.springframework.data.jpa.domain.Specification; - -import java.util.Collections; -import java.util.Map; -import java.util.function.UnaryOperator; - -public class InjectorContractUtils { - - public static final String INJECTOR_CONTRACT_KILL_CHAIN_PHASES_FILTER = "injector_contract_kill_chain_phases"; - private static final Map CORRESPONDENCE_MAP = Collections.singletonMap( - INJECTOR_CONTRACT_KILL_CHAIN_PHASES_FILTER, "attackPatterns.killChainPhases.id" - ); - - private InjectorContractUtils() { - - } - - /** - * Manage filters that are not directly managed by the generic mechanics -> injector_contract_kill_chain_phases - */ - public static UnaryOperator> handleCustomFilter( - @NotNull final SearchPaginationInput searchPaginationInput) { - return CustomFilterUtils.handleCustomFilter( - searchPaginationInput, - INJECTOR_CONTRACT_KILL_CHAIN_PHASES_FILTER, - CORRESPONDENCE_MAP - ); - } - -} diff --git a/openbas-api/src/main/java/io/openbas/rest/payload/PayloadApi.java b/openbas-api/src/main/java/io/openbas/rest/payload/PayloadApi.java index 8dedfdcc7b..186c2d5dd2 100644 --- a/openbas-api/src/main/java/io/openbas/rest/payload/PayloadApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/payload/PayloadApi.java @@ -72,11 +72,6 @@ public void setDocumentRepository(DocumentRepository documentRepository) { this.documentRepository = documentRepository; } - @GetMapping("/api/payloads") - public Iterable payloads() { - return payloadRepository.findAll(); - } - @PostMapping("/api/payloads/search") public Page payloads(@RequestBody @Valid final SearchPaginationInput searchPaginationInput) { return buildPaginationJPA( diff --git a/openbas-api/src/main/java/io/openbas/rest/scenario/utils/ScenarioUtils.java b/openbas-api/src/main/java/io/openbas/rest/scenario/utils/ScenarioUtils.java index b94a10e602..b4bfd00f62 100644 --- a/openbas-api/src/main/java/io/openbas/rest/scenario/utils/ScenarioUtils.java +++ b/openbas-api/src/main/java/io/openbas/rest/scenario/utils/ScenarioUtils.java @@ -1,36 +1,57 @@ package io.openbas.rest.scenario.utils; +import io.openbas.database.model.Filters; import io.openbas.database.model.Scenario; -import io.openbas.utils.CustomFilterUtils; +import io.openbas.database.specification.ScenarioSpecification; import io.openbas.utils.pagination.SearchPaginationInput; import org.jetbrains.annotations.NotNull; import org.springframework.data.jpa.domain.Specification; -import java.util.Collections; -import java.util.Map; +import java.util.Optional; +import java.util.function.Function; import java.util.function.UnaryOperator; -public class ScenarioUtils { +import static io.openbas.utils.CustomFilterUtils.computeMode; +import static java.util.Optional.ofNullable; - private static final String SCENARIO_KILL_CHAIN_PHASES_FILTER = "scenario_kill_chain_phases"; - private static final Map CORRESPONDENCE_MAP = Collections.singletonMap( - SCENARIO_KILL_CHAIN_PHASES_FILTER, "injects.injectorContract.attackPatterns.killChainPhases.id" - ); +public class ScenarioUtils { private ScenarioUtils() { } + private static final String SCENARIO_RECURRENCE_FILTER = "scenario_recurrence"; + /** * Manage filters that are not directly managed by the generic mechanics -> scenario_kill_chain_phases */ - public static UnaryOperator> handleCustomFilter( + public static Function, Specification> handleDeepFilter( + @NotNull final SearchPaginationInput searchPaginationInput) { + return handleCustomFilter(searchPaginationInput); + } + + private static UnaryOperator> handleCustomFilter( @NotNull final SearchPaginationInput searchPaginationInput) { - return CustomFilterUtils.handleCustomFilter( - searchPaginationInput, - SCENARIO_KILL_CHAIN_PHASES_FILTER, - CORRESPONDENCE_MAP - ); + // Existence of the filter + Optional scenarioRecurrenceFilterOpt = ofNullable(searchPaginationInput.getFilterGroup()) + .flatMap(f -> f.findByKey(SCENARIO_RECURRENCE_FILTER)); + + if (scenarioRecurrenceFilterOpt.isPresent()) { + // Purge filter + searchPaginationInput.getFilterGroup().removeByKey(SCENARIO_RECURRENCE_FILTER); + Specification customSpecification = null; + if (scenarioRecurrenceFilterOpt.get().getValues().contains("Scheduled")) { + customSpecification = ScenarioSpecification.isRecurring(); + } else if (scenarioRecurrenceFilterOpt.get().getValues().contains("Not planned")) { + customSpecification = ScenarioSpecification.noRecurring(); + } + if (customSpecification != null) { + return computeMode(searchPaginationInput, customSpecification); + } + return (Specification specification) -> specification; + } else { + return (Specification specification) -> specification; + } } } diff --git a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java index e5b5d9593c..d3a593fa85 100644 --- a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java +++ b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java @@ -266,7 +266,7 @@ public void deleteAtomicTesting(String injectId) { // -- PAGINATION -- - public Page findAllAtomicTestings(SearchPaginationInput searchPaginationInput) { + public Page findAllAtomicTestings(@NotNull final SearchPaginationInput searchPaginationInput) { Specification customSpec = Specification.where((root, query, cb) -> { Predicate predicate = cb.conjunction(); predicate = cb.and(predicate, cb.isNull(root.get("scenario"))); @@ -275,7 +275,7 @@ public Page findAllAtomicTestings(SearchPaginationInput sea }); return buildPaginationCriteriaBuilder( (Specification specification, Pageable pageable) -> this.atomicTestings( - specification.and(customSpec), pageable), + customSpec.and(specification), pageable), searchPaginationInput, Inject.class ); diff --git a/openbas-api/src/main/java/io/openbas/service/InjectService.java b/openbas-api/src/main/java/io/openbas/service/InjectService.java index 06f2cfee4a..846ec9b03b 100644 --- a/openbas-api/src/main/java/io/openbas/service/InjectService.java +++ b/openbas-api/src/main/java/io/openbas/service/InjectService.java @@ -30,6 +30,9 @@ import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellReference; import org.jetbrains.annotations.NotNull; +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.Service; import org.springframework.util.CollectionUtils; @@ -59,8 +62,10 @@ import java.util.stream.StreamSupport; import static io.openbas.config.SessionHelper.currentUser; +import static io.openbas.database.criteria.GenericCriteria.countQuery; import static io.openbas.utils.JpaUtils.createJoinArrayAggOnId; import static io.openbas.utils.JpaUtils.createLeftJoin; +import static io.openbas.utils.pagination.SortUtilsCriteriaBuilder.toSortCriteriaBuilder; import static java.time.Instant.now; @RequiredArgsConstructor @@ -164,6 +169,41 @@ public List injects(Specification specification) { return execInject(query); } + public Page injects(Specification specification, Pageable pageable) { + CriteriaBuilder cb = this.entityManager.getCriteriaBuilder(); + + CriteriaQuery cq = cb.createTupleQuery(); + Root injectRoot = cq.from(Inject.class); + selectForInject(cb, cq, injectRoot); + + // -- Text Search and Filters -- + if (specification != null) { + Predicate predicate = specification.toPredicate(injectRoot, cq, cb); + if (predicate != null) { + cq.where(predicate); + } + } + + // -- Sorting -- + List orders = toSortCriteriaBuilder(cb, injectRoot, pageable.getSort()); + cq.orderBy(orders); + + // Type Query + TypedQuery query = this.entityManager.createQuery(cq); + + // -- Pagination -- + query.setFirstResult((int) pageable.getOffset()); + query.setMaxResults(pageable.getPageSize()); + + // -- EXECUTION -- + List injects = execInject(query); + + // -- Count Query -- + Long total = countQuery(cb, this.entityManager, Inject.class, specification); + + return new PageImpl<>(injects, pageable, total); + } + /** * Create inject programmatically based on rawInject, rawInjectExpectation, rawAsset, rawAssetGroup, rawTeam */ diff --git a/openbas-api/src/main/java/io/openbas/service/ScenarioService.java b/openbas-api/src/main/java/io/openbas/service/ScenarioService.java index b8bfd35067..ab05e2e10a 100644 --- a/openbas-api/src/main/java/io/openbas/service/ScenarioService.java +++ b/openbas-api/src/main/java/io/openbas/service/ScenarioService.java @@ -46,7 +46,7 @@ import java.io.InputStream; import java.time.Instant; import java.util.*; -import java.util.function.UnaryOperator; +import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -56,7 +56,7 @@ import static io.openbas.database.criteria.GenericCriteria.countQuery; import static io.openbas.database.specification.ScenarioSpecification.findGrantedFor; import static io.openbas.helper.StreamHelper.fromIterable; -import static io.openbas.rest.scenario.utils.ScenarioUtils.handleCustomFilter; +import static io.openbas.rest.scenario.utils.ScenarioUtils.handleDeepFilter; import static io.openbas.service.ImportService.EXPORT_ENTRY_ATTACHMENT; import static io.openbas.service.ImportService.EXPORT_ENTRY_SCENARIO; import static io.openbas.utils.Constants.ARTICLES; @@ -122,10 +122,7 @@ public List scenarios() { } public Page scenarios(@NotNull final SearchPaginationInput searchPaginationInput) { - UnaryOperator> finalSpecification = handleCustomFilter( - searchPaginationInput - ); - + Function, Specification> finalSpecification = handleDeepFilter(searchPaginationInput); if (currentUser().isAdmin()) { return buildPaginationCriteriaBuilder( (Specification specification, Pageable pageable) -> this.findAllWithCriteriaBuilder( @@ -138,7 +135,7 @@ public Page scenarios(@NotNull final SearchPaginationInpu } else { return buildPaginationCriteriaBuilder( (Specification specification, Pageable pageable) -> this.findAllWithCriteriaBuilder( - finalSpecification.apply(findGrantedFor(currentUser().getId()).and(specification)), + findGrantedFor(currentUser().getId()).and(finalSpecification.apply(specification)), pageable ), searchPaginationInput, @@ -213,7 +210,7 @@ private Page findAllWithCriteriaBuilder( .map(tuple -> new RawPaginationScenario( tuple.get("scenario_id", String.class), tuple.get("scenario_name", String.class), - tuple.get("scenario_severity", String.class), + tuple.get("scenario_severity", Scenario.SEVERITY.class), tuple.get("scenario_category", String.class), tuple.get("scenario_recurrence", String.class), tuple.get("scenario_updated_at", Instant.class), diff --git a/openbas-api/src/main/java/io/openbas/utils/CustomFilterUtils.java b/openbas-api/src/main/java/io/openbas/utils/CustomFilterUtils.java index 7bff1bc168..5b131f9c30 100644 --- a/openbas-api/src/main/java/io/openbas/utils/CustomFilterUtils.java +++ b/openbas-api/src/main/java/io/openbas/utils/CustomFilterUtils.java @@ -3,53 +3,27 @@ import io.openbas.database.model.Base; import io.openbas.database.model.Filters; import io.openbas.utils.pagination.SearchPaginationInput; -import jakarta.validation.constraints.NotBlank; import org.jetbrains.annotations.NotNull; import org.springframework.data.jpa.domain.Specification; -import java.util.Map; -import java.util.Optional; import java.util.function.UnaryOperator; -import static io.openbas.utils.FilterUtilsJpa.computeFilterFromSpecificPath; -import static java.util.Optional.ofNullable; - public class CustomFilterUtils { private CustomFilterUtils() { } - /** - * Manage filters that are not directly managed by the generic mechanics - */ - public static UnaryOperator> handleCustomFilter( + public static UnaryOperator> computeMode( @NotNull final SearchPaginationInput searchPaginationInput, - @NotBlank final String customFilterKey, - @NotNull final Map correspondenceMap) { - UnaryOperator> finalSpecification; - // Existence of the filter - Optional killChainPhaseFilterOpt = ofNullable(searchPaginationInput.getFilterGroup()) - .flatMap(f -> f.findByKey(customFilterKey)); - - if (killChainPhaseFilterOpt.isPresent()) { - // Purge filter - searchPaginationInput.getFilterGroup().removeByKey(customFilterKey); - Specification customSpecification = computeFilterFromSpecificPath( - killChainPhaseFilterOpt.get(), correspondenceMap.get(customFilterKey) - ); - // Final specification - if (Filters.FilterMode.and.equals(searchPaginationInput.getFilterGroup().getMode())) { - finalSpecification = customSpecification::and; - } else if (Filters.FilterMode.or.equals(searchPaginationInput.getFilterGroup().getMode())) { - finalSpecification = customSpecification::or; - } else { - finalSpecification = (Specification specification) -> specification; - } + Specification customSpecification) { + if (Filters.FilterMode.and.equals(searchPaginationInput.getFilterGroup().getMode())) { + return customSpecification::and; + } else if (Filters.FilterMode.or.equals(searchPaginationInput.getFilterGroup().getMode())) { + return customSpecification::or; } else { - finalSpecification = (Specification specification) -> specification; + return (Specification specification) -> specification; } - return finalSpecification; } } diff --git a/openbas-api/src/main/java/io/openbas/utils/InjectUtils.java b/openbas-api/src/main/java/io/openbas/utils/InjectUtils.java index 75a7bb0e0a..fd06a5caf2 100644 --- a/openbas-api/src/main/java/io/openbas/utils/InjectUtils.java +++ b/openbas-api/src/main/java/io/openbas/utils/InjectUtils.java @@ -7,19 +7,23 @@ public class InjectUtils { - public static boolean checkIfRowIsEmpty(Row row) { - if (row == null) { - return true; - } - if (row.getLastCellNum() <= 0) { - return true; - } - for (int cellNum = row.getFirstCellNum(); cellNum < row.getLastCellNum(); cellNum++) { - Cell cell = row.getCell(cellNum); - if (cell != null && cell.getCellType() != CellType.BLANK && StringUtils.isNotBlank(cell.toString())) { - return false; - } - } - return true; + private InjectUtils() { + + } + + public static boolean checkIfRowIsEmpty(Row row) { + if (row == null) { + return true; + } + if (row.getLastCellNum() <= 0) { + return true; + } + for (int cellNum = row.getFirstCellNum(); cellNum < row.getLastCellNum(); cellNum++) { + Cell cell = row.getCell(cellNum); + if (cell != null && cell.getCellType() != CellType.BLANK && StringUtils.isNotBlank(cell.toString())) { + return false; + } } + return true; + } } diff --git a/openbas-api/src/main/java/io/openbas/utils/pagination/SearchUtilsRuntime.java b/openbas-api/src/main/java/io/openbas/utils/pagination/SearchUtilsRuntime.java deleted file mode 100644 index 5fbee967e1..0000000000 --- a/openbas-api/src/main/java/io/openbas/utils/pagination/SearchUtilsRuntime.java +++ /dev/null @@ -1,88 +0,0 @@ -package io.openbas.utils.pagination; - -import io.openbas.utils.schema.PropertySchema; -import io.openbas.utils.schema.SchemaUtils; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -import javax.annotation.Nullable; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; - -import static io.openbas.utils.OperationUtilsRuntime.containsText; -import static io.openbas.utils.schema.SchemaUtils.getSearchableProperties; -import static org.springframework.util.StringUtils.hasText; - -public class SearchUtilsRuntime { - - private static final Predicate EMPTY_PREDICATE = (value) -> true; - - public static Predicate computeSearchRuntime(@Nullable final String search) { - if (!hasText(search)) { - return EMPTY_PREDICATE; - } - - return (value) -> { - List propertySchemas = SchemaUtils.schema(value.getClass()); - List searchableProperties = getSearchableProperties(propertySchemas); - List values = getSearchableValues(value, searchableProperties); - return getPropertyValue(values, search); - }; - } - - @SuppressWarnings("unchecked") - private static boolean getPropertyValue(@NotNull final List values, @NotBlank final String search) { - return values.stream() - .anyMatch(v -> { - if (v.getClass().isAssignableFrom(Map.class) - || v.getClass().getName().contains("ImmutableCollections")) { - return ((Map) v).values() - .stream() - .anyMatch(mapValue -> containsText(mapValue, search)); - } else if (v.getClass().isAssignableFrom(String.class)) { - return containsText(v, search); - } else { - throw new UnsupportedOperationException( - "Searching is not implemented for other property than Map and String"); - } - }); - } - - /** - * Search values on direct property and representative child - */ - private static List getSearchableValues(Object obj, List propertySchemas) { - if (propertySchemas.isEmpty()) { - return Collections.emptyList(); - } - - List values = new ArrayList<>(); - - Field field; - try { - for (PropertySchema propertySchema : propertySchemas) { - field = obj.getClass().getDeclaredField(propertySchema.getName()); - field.setAccessible(true); - - // Search on child - if (propertySchema.isSearchable() && hasText(propertySchema.getPropertyRepresentative())) { - Object childObj = field.get(obj); - Field childField = childObj.getClass().getDeclaredField(propertySchema.getPropertyRepresentative()); - childField.setAccessible(true); - values.add(childField.get(childObj)); - // Direct property - } else { - values.add(field.get(obj)); - } - } - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - return values; - } - -} diff --git a/openbas-api/src/main/java/io/openbas/utils/pagination/SortUtilsJpa.java b/openbas-api/src/main/java/io/openbas/utils/pagination/SortUtilsJpa.java index a2bc5596b6..e7e49fc783 100644 --- a/openbas-api/src/main/java/io/openbas/utils/pagination/SortUtilsJpa.java +++ b/openbas-api/src/main/java/io/openbas/utils/pagination/SortUtilsJpa.java @@ -8,6 +8,8 @@ import javax.annotation.Nullable; import java.util.List; +import static io.openbas.utils.schema.SchemaUtils.getSortableProperties; + public class SortUtilsJpa { private SortUtilsJpa() { @@ -15,7 +17,7 @@ private SortUtilsJpa() { } public static Sort toSortJpa(@Nullable final List sorts, @NotNull final Class clazz) { - List propertySchemas = SchemaUtils.schema(clazz); + List propertySchemas = getSortableProperties(SchemaUtils.schema(clazz)); List orders; @@ -36,7 +38,7 @@ public static Sort toSortJpa(@Nullable final List sorts, @NotNull .filter(p -> p.getJsonName().equals(property)) .findFirst() .map(PropertySchema::getName) - .orElseThrow(); + .orElseThrow(() -> new IllegalArgumentException("Property not sortable: " + property + " for class " + clazz)); return new Sort.Order(direction, javaProperty); }).toList(); } diff --git a/openbas-api/src/main/java/io/openbas/utils/pagination/SortUtilsRuntime.java b/openbas-api/src/main/java/io/openbas/utils/pagination/SortUtilsRuntime.java index 45c0e477a7..c619ba1fbf 100644 --- a/openbas-api/src/main/java/io/openbas/utils/pagination/SortUtilsRuntime.java +++ b/openbas-api/src/main/java/io/openbas/utils/pagination/SortUtilsRuntime.java @@ -1,19 +1,9 @@ package io.openbas.utils.pagination; -import io.openbas.helper.SupportedLanguage; -import io.openbas.utils.schema.PropertySchema; -import io.openbas.utils.schema.SchemaUtils; import org.springframework.data.domain.Sort; import javax.annotation.Nullable; -import java.lang.reflect.Field; -import java.util.*; -import java.util.Map.Entry; - -import static io.openbas.config.SessionHelper.currentUser; -import static io.openbas.utils.schema.SchemaUtils.*; -import static java.util.Comparator.comparing; -import static org.springframework.util.StringUtils.hasText; +import java.util.List; public class SortUtilsRuntime { @@ -38,87 +28,5 @@ public static Sort toSortRuntime(@Nullable final List sorts) { return Sort.by(orders); } - public static Comparator computeSortRuntime(@Nullable final List sorts) { - Sort sort = toSortRuntime(sorts); - - Comparator comparator = (a, b) -> 0; - - for (Sort.Order order : sort) { - Comparator propertyComparator = comparing( - value -> { - List propertySchemas = SchemaUtils.schema(value.getClass()); - List filterableProperties = getSortableProperties(propertySchemas); - PropertySchema sortableProperty = retrieveProperty(filterableProperties, order.getProperty()); - Entry, Object> entry = getPropertyInfo(value, sortableProperty); - return getPropertyValue(entry); - }); - - comparator = comparator.thenComparing( - order.getDirection().equals(Sort.Direction.ASC) - ? propertyComparator - : propertyComparator.reversed() - ); - } - - return comparator; - } - - private static final Comparable EMPTY_COMPARABLE = o -> 0; - - @SuppressWarnings("unchecked") - private static Comparable getPropertyValue(Entry, Object> entry) { - if (entry == null || entry.getValue() == null) { - return EMPTY_COMPARABLE; - } - if (Arrays.stream(BASE_CLASSES).anyMatch(c -> entry.getKey().isAssignableFrom(c))) { - return (Comparable) entry.getValue(); - // Handle map with Supported language - } else if (entry.getKey().isAssignableFrom(Map.class) - || entry.getKey().getName().contains("ImmutableCollections")) { - Set entries = ((Map) entry.getValue()).entrySet(); - if (entries.stream().anyMatch(e -> e.getKey().getClass().isAssignableFrom(SupportedLanguage.class))) { - SupportedLanguage lang = SupportedLanguage.of(currentUser().getLang()); - return (Comparable) entries.stream() - .filter(e -> lang.equals(e.getKey())) - .findFirst() - .map(Entry::getValue) - .orElse(EMPTY_COMPARABLE); - } else { - return EMPTY_COMPARABLE; - } - } else { - throw new UnsupportedOperationException("Sorting is not implemented for other property than String and Long"); - } - } - - @SuppressWarnings("unchecked") - private static Map.Entry, Object> getPropertyInfo(Object obj, PropertySchema propertySchema) { - if (obj == null) { - return null; - } - - Field field; - Object currentObject; - try { - field = obj.getClass().getDeclaredField(propertySchema.getName()); - field.setAccessible(true); - - // Search on child - if (propertySchema.isSortable() && hasText(propertySchema.getPropertyRepresentative())) { - Object childObj = field.get(obj); - Field childField = childObj.getClass().getDeclaredField(propertySchema.getPropertyRepresentative()); - childField.setAccessible(true); - currentObject = childField.get(childObj); - // Direct property - } else { - currentObject = field.get(obj); - } - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - - return Map.entry((Class) currentObject.getClass(), currentObject); - } - } diff --git a/openbas-api/src/test/java/io/openbas/rest/ExerciseApiSearchTest.java b/openbas-api/src/test/java/io/openbas/rest/ExerciseApiSearchTest.java new file mode 100644 index 0000000000..9fbe87ad15 --- /dev/null +++ b/openbas-api/src/test/java/io/openbas/rest/ExerciseApiSearchTest.java @@ -0,0 +1,159 @@ +package io.openbas.rest; + +import io.openbas.IntegrationTest; +import io.openbas.database.model.Exercise; +import io.openbas.database.repository.ExerciseRepository; +import io.openbas.utils.fixtures.ExerciseFixture; +import io.openbas.utils.fixtures.PaginationFixture; +import io.openbas.utils.mockUser.WithMockAdminUser; +import io.openbas.utils.pagination.SearchPaginationInput; +import io.openbas.utils.pagination.SortField; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.List; + +import static io.openbas.database.model.ExerciseStatus.SCHEDULED; +import static io.openbas.database.model.Filters.FilterOperator.contains; +import static io.openbas.rest.exercise.ExerciseApi.EXERCISE_URI; +import static io.openbas.utils.JsonUtils.asJsonString; +import static java.lang.String.valueOf; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestInstance(PER_CLASS) +public class ExerciseApiSearchTest extends IntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private ExerciseRepository exerciseRepository; + + private static final List EXERCISE_IDS = new ArrayList<>(); + + @BeforeAll + void beforeAll() { + Exercise exercise1 = ExerciseFixture.createDefaultCrisisExercise(); + Exercise exercise1Saved = this.exerciseRepository.save(exercise1); + EXERCISE_IDS.add(exercise1Saved.getId()); + + Exercise exercise2 = ExerciseFixture.createDefaultIncidentResponseExercise(); + Exercise exercise2Saved = this.exerciseRepository.save(exercise2); + EXERCISE_IDS.add(exercise2Saved.getId()); + } + + @AfterAll + void afterAll() { + this.exerciseRepository.deleteAllById(EXERCISE_IDS); + } + + @Nested + @WithMockAdminUser + @DisplayName("Retrieving exercises") + class RetrievingExercises { + // -- PREPARE -- + + @Nested + @DisplayName("Searching page of exercises") + class SearchingPageOfExercises { + + @Test + @DisplayName("Retrieving first page of exercises by text search") + void given_working_search_input_should_return_a_page_of_exercises() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().textSearch("Crisis").build(); + + mvc.perform(post(EXERCISE_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + @Test + @DisplayName("Not retrieving first page of exercises by text search") + void given_not_working_search_input_should_return_a_page_of_exercises() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().textSearch("wrong").build(); + + mvc.perform(post(EXERCISE_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(0)); + } + } + + @Nested + @DisplayName("Sorting page of exercises") + class SortingPageOfExercises { + + @Test + @DisplayName("Sorting page of exercises by name") + void given_sorting_input_by_name_should_return_a_page_of_exercises_sort_by_name() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault() + .sorts(List.of(SortField.builder().property("exercise_name").build())) + .build(); + + mvc.perform(post(EXERCISE_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.content.[0].exercise_name").value("Crisis exercise")) + .andExpect(jsonPath("$.content.[1].exercise_name").value("Incident response exercise")); + } + + @Test + @DisplayName("Sorting page of exercises by start date") + void given_sorting_input_by_start_date_should_return_a_page_of_exercises_sort_by_start_date() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault() + .sorts(List.of(SortField.builder().property("exercise_start_date").direction("desc").build())) + .build(); + + mvc.perform(post(EXERCISE_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.content.[1].exercise_name").value("Incident response exercise")) + .andExpect(jsonPath("$.content.[0].exercise_name").value("Crisis exercise")); + } + } + + @Nested + @DisplayName("Filtering page of exercises") + class FilteringPageOfExercises { + + @Test + @DisplayName("Filtering page of exercises by name") + 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 + ); + + mvc.perform(post(EXERCISE_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + @Test + @DisplayName("Filtering page of exercises by status") + 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 + ); + + mvc.perform(post(EXERCISE_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(2)); + } + } + } +} diff --git a/openbas-api/src/test/java/io/openbas/rest/PayloadApiSearchTest.java b/openbas-api/src/test/java/io/openbas/rest/PayloadApiSearchTest.java new file mode 100644 index 0000000000..f32d8e00ab --- /dev/null +++ b/openbas-api/src/test/java/io/openbas/rest/PayloadApiSearchTest.java @@ -0,0 +1,179 @@ +package io.openbas.rest; + +import io.openbas.IntegrationTest; +import io.openbas.database.model.Payload; +import io.openbas.database.repository.PayloadRepository; +import io.openbas.utils.fixtures.PaginationFixture; +import io.openbas.utils.mockUser.WithMockAdminUser; +import io.openbas.utils.pagination.SearchPaginationInput; +import io.openbas.utils.pagination.SortField; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.List; + +import static io.openbas.database.model.Endpoint.PLATFORM_TYPE.Linux; +import static io.openbas.database.model.Filters.FilterOperator.contains; +import static io.openbas.database.model.Payload.PAYLOAD_SOURCE.MANUAL; +import static io.openbas.rest.payload.PayloadApi.PAYLOAD_URI; +import static io.openbas.utils.JsonUtils.asJsonString; +import static io.openbas.utils.fixtures.PayloadFixture.createDefaultCommand; +import static io.openbas.utils.fixtures.PayloadFixture.createDefaultDnsResolution; +import static java.lang.String.valueOf; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestInstance(PER_CLASS) +public class PayloadApiSearchTest extends IntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private PayloadRepository payloadRepository; + + private static final List PAYLOAD_COMMAND_IDS = new ArrayList<>(); + + @BeforeAll + void beforeAll() { + Payload command = createDefaultCommand(); + Payload commandSaved = this.payloadRepository.save(command); + PAYLOAD_COMMAND_IDS.add(commandSaved.getId()); + + Payload dnsResolution = createDefaultDnsResolution(); + Payload dnsResolutionSaved = this.payloadRepository.save(dnsResolution); + PAYLOAD_COMMAND_IDS.add(dnsResolutionSaved.getId()); + } + + @AfterAll + void afterAll() { + this.payloadRepository.deleteAllById(PAYLOAD_COMMAND_IDS); + } + + @Nested + @WithMockAdminUser + @DisplayName("Retrieving payloads") + class RetrievingPayloads { + // -- PREPARE -- + + @Nested + @DisplayName("Searching page of payloads") + class SearchingPageOfPayloads { + + @Test + @DisplayName("Retrieving first page of payloads by textsearch") + void given_working_search_input_should_return_a_page_of_payloads() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().textSearch("command").build(); + + mvc.perform(post(PAYLOAD_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + @Test + @DisplayName("Not retrieving first page of payloads by textsearch") + void given_not_working_search_input_should_return_a_page_of_payloads() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().textSearch("wrong").build(); + + mvc.perform(post(PAYLOAD_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(0)); + } + } + + @Nested + @DisplayName("Sorting page of payloads") + class SortingPageOfPayloads { + + @Test + @DisplayName("Sorting page of payloads by name") + void given_sorting_input_by_name_should_return_a_page_of_payloads_sort_by_name() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault() + .sorts(List.of(SortField.builder().property("payload_name").build())) + .build(); + + mvc.perform(post(PAYLOAD_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.content.[0].payload_name").value("command payload")) + .andExpect(jsonPath("$.content.[1].payload_name").value("dns resolution payload")); + } + + @Test + @DisplayName("Sorting page of payloads by platforms") + void given_sorting_input_by_updated_at_should_return_a_page_of_payloads_sort_by_updated_at() + throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault() + .sorts(List.of(SortField.builder().property("payload_updated_at").direction("desc").build())) + .build(); + + mvc.perform(post(PAYLOAD_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.content.[0].payload_name").value("dns resolution payload")) + .andExpect(jsonPath("$.content.[1].payload_name").value("command payload")); + } + } + + @Nested + @DisplayName("Filtering page of payloads") + class FilteringPageOfPayloads { + + @Test + @DisplayName("Filtering page of payloads by name") + 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 + ); + + mvc.perform(post(PAYLOAD_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + @Test + @DisplayName("Filtering page of payloads by platforms") + 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 + ); + + mvc.perform(post(PAYLOAD_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + @Test + @DisplayName("Filtering page of payloads by source") + 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 + ); + + mvc.perform(post(PAYLOAD_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(2)); + } + + } + + } + +} 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 new file mode 100644 index 0000000000..cb46e86e0f --- /dev/null +++ b/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioApiSearchTest.java @@ -0,0 +1,177 @@ +package io.openbas.rest.scenario; + +import io.openbas.IntegrationTest; +import io.openbas.database.model.Scenario; +import io.openbas.database.repository.ScenarioRepository; +import io.openbas.utils.fixtures.PaginationFixture; +import io.openbas.utils.fixtures.ScenarioFixture; +import io.openbas.utils.mockUser.WithMockAdminUser; +import io.openbas.utils.pagination.SearchPaginationInput; +import io.openbas.utils.pagination.SortField; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.List; + +import static io.openbas.database.model.Filters.FilterOperator.contains; +import static io.openbas.database.model.Scenario.SEVERITY.critical; +import static io.openbas.rest.scenario.ScenarioApi.SCENARIO_URI; +import static io.openbas.utils.JsonUtils.asJsonString; +import static java.lang.String.valueOf; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestInstance(PER_CLASS) +public class ScenarioApiSearchTest extends IntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private ScenarioRepository scenarioRepository; + + private static final List SCENARIO_IDS = new ArrayList<>(); + + @BeforeAll + void beforeAll() { + Scenario scenario1 = ScenarioFixture.createDefaultCrisisScenario(); + Scenario scenario1Saved = this.scenarioRepository.save(scenario1); + SCENARIO_IDS.add(scenario1Saved.getId()); + + Scenario scenario2 = ScenarioFixture.createDefaultIncidentResponseScenario(); + Scenario scenario2Saved = this.scenarioRepository.save(scenario2); + SCENARIO_IDS.add(scenario2Saved.getId()); + } + + @AfterAll + void afterAll() { + this.scenarioRepository.deleteAllById(SCENARIO_IDS); + } + + @Nested + @WithMockAdminUser + @DisplayName("Retrieving scenarios") + class RetrievingScenarios { + // -- PREPARE -- + + @Nested + @DisplayName("Searching page of scenarios") + class SearchingPageOfScenarios { + + @Test + @DisplayName("Retrieving first page of scenarios by textsearch") + void given_working_search_input_should_return_a_page_of_scenarios() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().textSearch("Crisis").build(); + + mvc.perform(post(SCENARIO_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + @Test + @DisplayName("Not retrieving first page of scenario by textsearch") + void given_not_working_search_input_should_return_a_page_of_scenarios() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().textSearch("wrong").build(); + + mvc.perform(post(SCENARIO_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(0)); + } + } + + @Nested + @DisplayName("Sorting page of scenarios") + class SortingPageOfScenarios { + + @Test + @DisplayName("Sorting page of scenarios by name") + void given_sorting_input_by_name_should_return_a_page_of_scenarios_sort_by_name() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault() + .sorts(List.of(SortField.builder().property("scenario_name").build())) + .build(); + + mvc.perform(post(SCENARIO_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.content.[0].scenario_name").value("Crisis scenario")) + .andExpect(jsonPath("$.content.[1].scenario_name").value("Incident response scenario")); + } + + @Test + @DisplayName("Sorting page of scenarios by category") + void given_sorting_input_by_category_should_return_a_page_of_scenarios_sort_by_category() + throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault() + .sorts(List.of(SortField.builder().property("scenario_category").direction("desc").build())) + .build(); + + mvc.perform(post(SCENARIO_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.content.[0].scenario_name").value("Incident response scenario")) + .andExpect(jsonPath("$.content.[1].scenario_name").value("Crisis scenario")); + } + } + + @Nested + @DisplayName("Filtering page of scenarios") + class FilteringPageOfScenarios { + + @Test + @DisplayName("Filtering page of scenarios by name") + 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 + ); + + mvc.perform(post(SCENARIO_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + @Test + @DisplayName("Filtering page of scenarios by category") + 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 + ); + + mvc.perform(post(SCENARIO_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + @Test + @DisplayName("Filtering page of scenarios by severity") + 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 + ); + + mvc.perform(post(SCENARIO_URI + "/search") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + } + + } + +} diff --git a/openbas-api/src/test/java/io/openbas/scenario/ScenarioApiTest.java b/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioApiTest.java similarity index 99% rename from openbas-api/src/test/java/io/openbas/scenario/ScenarioApiTest.java rename to openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioApiTest.java index 215dd79432..f41a7589c0 100644 --- a/openbas-api/src/test/java/io/openbas/scenario/ScenarioApiTest.java +++ b/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioApiTest.java @@ -1,4 +1,4 @@ -package io.openbas.scenario; +package io.openbas.rest.scenario; import com.jayway.jsonpath.JsonPath; import io.openbas.database.repository.ScenarioRepository; diff --git a/openbas-api/src/test/java/io/openbas/scenario/ScenarioImportApiTest.java b/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioImportApiTest.java similarity index 99% rename from openbas-api/src/test/java/io/openbas/scenario/ScenarioImportApiTest.java rename to openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioImportApiTest.java index a90ab5c21b..dc5a02c726 100644 --- a/openbas-api/src/test/java/io/openbas/scenario/ScenarioImportApiTest.java +++ b/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioImportApiTest.java @@ -1,4 +1,4 @@ -package io.openbas.scenario; +package io.openbas.rest.scenario; import io.openbas.database.model.ImportMapper; import io.openbas.database.repository.ImportMapperRepository; diff --git a/openbas-api/src/test/java/io/openbas/scenario/ScenarioToExerciseServiceTest.java b/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioToExerciseServiceTest.java similarity index 98% rename from openbas-api/src/test/java/io/openbas/scenario/ScenarioToExerciseServiceTest.java rename to openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioToExerciseServiceTest.java index 7b06c9a96c..c54e9a89e8 100644 --- a/openbas-api/src/test/java/io/openbas/scenario/ScenarioToExerciseServiceTest.java +++ b/openbas-api/src/test/java/io/openbas/rest/scenario/ScenarioToExerciseServiceTest.java @@ -1,10 +1,11 @@ -package io.openbas.scenario; +package io.openbas.rest.scenario; import io.openbas.database.model.*; import io.openbas.database.repository.*; import io.openbas.service.LoadService; import io.openbas.service.ScenarioService; import io.openbas.service.ScenarioToExerciseService; +import io.openbas.utils.fixtures.ScenarioFixture; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -99,7 +100,7 @@ public void teardown() { void scenarioToExerciseTest() { // -- PREPARE -- // Base - Scenario scenario = getScenario(); + Scenario scenario = ScenarioFixture.getScenario(); String name = scenario.getName(); // User & Teams User user = getUser(); diff --git a/openbas-api/src/test/java/io/openbas/scheduler/jobs/ScenarioExecutionJobTest.java b/openbas-api/src/test/java/io/openbas/scheduler/jobs/ScenarioExecutionJobTest.java index b6c04594c1..e350004156 100644 --- a/openbas-api/src/test/java/io/openbas/scheduler/jobs/ScenarioExecutionJobTest.java +++ b/openbas-api/src/test/java/io/openbas/scheduler/jobs/ScenarioExecutionJobTest.java @@ -4,6 +4,7 @@ import io.openbas.database.model.Scenario; import io.openbas.database.repository.ExerciseRepository; import io.openbas.service.ScenarioService; +import io.openbas.utils.fixtures.ScenarioFixture; import org.junit.jupiter.api.*; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; @@ -54,7 +55,7 @@ void given_cron_in_one_hour_should_not_create_simulation() throws JobExecutionEx ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("UTC")); int hourToStart = (zonedDateTime.getHour() + 1) % 24; - Scenario scenario = getScenario(); + Scenario scenario = ScenarioFixture.getScenario(); scenario.setRecurrence("0 " + zonedDateTime.getMinute() + " " + hourToStart + " * * *"); // Every day now + 1 hour Scenario scenarioSaved = this.scenarioService.createScenario(scenario); SCENARIO_ID_1 = scenarioSaved.getId(); @@ -79,7 +80,7 @@ void given_cron_in_one_minute_should_create_simulation() throws JobExecutionExce // -- PREPARE -- ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("UTC")); - Scenario scenario = getScenario(); + Scenario scenario = ScenarioFixture.getScenario(); int minuteToStart = (zonedDateTime.getMinute() + 1) % 60; scenario.setRecurrence("0 " + minuteToStart + " " + zonedDateTime.getHour() + " * * *"); // Every day now + 1 minute Scenario scenarioSaved = this.scenarioService.createScenario(scenario); @@ -125,7 +126,7 @@ void given_end_date_before_now_should_not_create_second_simulation() throws JobE // -- PREPARE -- ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("UTC")); - Scenario scenario = getScenario(); + Scenario scenario = ScenarioFixture.getScenario(); int minuteToStart = (zonedDateTime.getMinute() + 1) % 60; scenario.setRecurrence("0 " + minuteToStart + " " + zonedDateTime.getHour() + " * * *"); // Every day now + 1 minute scenario.setRecurrenceEnd(Instant.now().minus(0, ChronoUnit.DAYS)); diff --git a/openbas-api/src/test/java/io/openbas/service/ScenarioServiceTest.java b/openbas-api/src/test/java/io/openbas/service/ScenarioServiceTest.java index 824653b4bd..dcd840852c 100644 --- a/openbas-api/src/test/java/io/openbas/service/ScenarioServiceTest.java +++ b/openbas-api/src/test/java/io/openbas/service/ScenarioServiceTest.java @@ -3,6 +3,7 @@ import io.openbas.database.model.*; import io.openbas.database.repository.*; import io.openbas.rest.inject.service.InjectDuplicateService; +import io.openbas.utils.fixtures.ScenarioFixture; import jakarta.transaction.Transactional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -78,7 +79,7 @@ void createNewContextualTeamsDuringScenarioDuplication(){ inject.setTeams(scenarioTeams); Set scenarioInjects = new HashSet<>(); scenarioInjects.add(this.injectRepository.save(inject)); - Scenario scenario = this.scenarioRepository.save(getScenario(scenarioTeams, scenarioInjects)); + Scenario scenario = this.scenarioRepository.save(ScenarioFixture.getScenario(scenarioTeams, scenarioInjects)); // -- EXECUTE -- Scenario scenarioDuplicated = scenarioService.getDuplicateScenario(scenario.getId()); diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/ExerciseFixture.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/ExerciseFixture.java index fe7667c0ac..309d0ddfc8 100644 --- a/openbas-api/src/test/java/io/openbas/utils/fixtures/ExerciseFixture.java +++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/ExerciseFixture.java @@ -1,8 +1,10 @@ package io.openbas.utils.fixtures; import io.openbas.database.model.Exercise; +import io.openbas.database.model.ExerciseStatus; import io.openbas.database.model.Team; +import java.time.Instant; import java.util.List; public class ExerciseFixture { @@ -21,4 +23,27 @@ public static Exercise getExercise(List exerciseTeams) { } return exercise; } + + public static Exercise createDefaultCrisisExercise() { + Exercise exercise = new Exercise(); + exercise.setName("Crisis exercise"); + exercise.setDescription("A crisis exercise for my enterprise"); + exercise.setSubtitle("A crisis exercise"); + exercise.setFrom("exercise@mail.fr"); + exercise.setCategory("crisis-communication"); + return exercise; + } + + public static Exercise createDefaultIncidentResponseExercise() { + Exercise exercise = new Exercise(); + exercise.setName("Incident response exercise"); + exercise.setDescription("An incident response exercise for my enterprise"); + exercise.setSubtitle("An incident response exercise"); + exercise.setFrom("exercise@mail.fr"); + exercise.setCategory("incident-response"); + exercise.setStatus(ExerciseStatus.SCHEDULED); + exercise.setStart(Instant.now()); + return exercise; + } + } diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/PayloadFixture.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/PayloadFixture.java new file mode 100644 index 0000000000..6f09002c28 --- /dev/null +++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/PayloadFixture.java @@ -0,0 +1,35 @@ +package io.openbas.utils.fixtures; + +import io.openbas.database.model.*; + +import java.util.Collections; + +import static io.openbas.database.model.Command.COMMAND_TYPE; +import static io.openbas.database.model.DnsResolution.DNS_RESOLUTION_TYPE; +import static io.openbas.database.model.Payload.PAYLOAD_SOURCE.MANUAL; +import static io.openbas.database.model.Payload.PAYLOAD_STATUS.VERIFIED; + +public class PayloadFixture { + + public static Payload createDefaultCommand() { + Command command = new Command("command-id", COMMAND_TYPE, "command payload"); + command.setContent("cd .."); + command.setExecutor("PowerShell"); + command.setPlatforms(new Endpoint.PLATFORM_TYPE[]{Endpoint.PLATFORM_TYPE.Windows}); + command.setSource(MANUAL); + command.setStatus(VERIFIED); + command.setAttackPatterns(Collections.emptyList()); + return command; + } + + public static Payload createDefaultDnsResolution() { + DnsResolution dnsResolution = new DnsResolution("dns-resolution-id", DNS_RESOLUTION_TYPE, "dns resolution payload"); + dnsResolution.setHostname("localhost"); + dnsResolution.setPlatforms(new Endpoint.PLATFORM_TYPE[]{Endpoint.PLATFORM_TYPE.Linux}); + dnsResolution.setSource(MANUAL); + dnsResolution.setStatus(VERIFIED); + dnsResolution.setAttackPatterns(Collections.emptyList()); + return dnsResolution; + } + +} diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/ScenarioFixture.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/ScenarioFixture.java index c868020624..3010a9b0a8 100644 --- a/openbas-api/src/test/java/io/openbas/utils/fixtures/ScenarioFixture.java +++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/ScenarioFixture.java @@ -4,10 +4,11 @@ import io.openbas.database.model.Scenario; import io.openbas.database.model.Team; -import java.util.HashSet; import java.util.List; import java.util.Set; +import static io.openbas.database.model.Scenario.SEVERITY.critical; + public class ScenarioFixture { public static Scenario getScenario() { @@ -29,4 +30,25 @@ public static Scenario getScenario(List scenarioTeams, Set scenari return scenario; } + public static Scenario createDefaultCrisisScenario() { + Scenario scenario = new Scenario(); + scenario.setName("Crisis scenario"); + scenario.setDescription("A crisis scenario for my enterprise"); + scenario.setSubtitle("A crisis scenario"); + scenario.setFrom("scenario@mail.fr"); + scenario.setCategory("crisis-communication"); + return scenario; + } + + public static Scenario createDefaultIncidentResponseScenario() { + Scenario scenario = new Scenario(); + scenario.setName("Incident response scenario"); + scenario.setDescription("An incident response scenario for my enterprise"); + scenario.setSubtitle("An incident response scenario"); + scenario.setFrom("scenario@mail.fr"); + scenario.setCategory("incident-response"); + scenario.setSeverity(critical); + return scenario; + } + } diff --git a/openbas-api/src/test/java/io/openbas/utils/mockUser/WithMockAdminUserSecurityContextFactory.java b/openbas-api/src/test/java/io/openbas/utils/mockUser/WithMockAdminUserSecurityContextFactory.java index b1f64cc69f..23d946dc76 100644 --- a/openbas-api/src/test/java/io/openbas/utils/mockUser/WithMockAdminUserSecurityContextFactory.java +++ b/openbas-api/src/test/java/io/openbas/utils/mockUser/WithMockAdminUserSecurityContextFactory.java @@ -1,10 +1,6 @@ package io.openbas.utils.mockUser; import io.openbas.database.model.User; -import io.openbas.database.repository.UserRepository; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; diff --git a/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsJpa.java b/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsJpa.java index b56d5aa3b4..03a4075d98 100644 --- a/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsJpa.java +++ b/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsJpa.java @@ -6,8 +6,9 @@ import io.openbas.database.model.Filters.FilterOperator; import io.openbas.utils.schema.PropertySchema; import io.openbas.utils.schema.SchemaUtils; -import jakarta.persistence.criteria.*; -import jakarta.validation.constraints.NotBlank; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; import jakarta.validation.constraints.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.data.jpa.domain.Specification; @@ -88,33 +89,6 @@ private static Specification computeFilter(@Nullable final Filter filt }; } - /** - * Allows to manage deep paths not currently managed by the queryable annotation Next step: improvement of the - * queryable annotation in order to directly manage filters on deep properties as well as having several possible - * filters on these properties - */ - @SuppressWarnings("unchecked") - public static Specification computeFilterFromSpecificPath( - @Nullable final Filter filter, - @NotBlank final String jsonPath) { - if (filter == null) { - return (Specification) EMPTY_SPECIFICATION; - } - - String[] jsonPaths = jsonPath.split("\\."); - return (root, query, cb) -> { - if (jsonPaths.length > 0) { - Join paths = root.join(jsonPaths[0], JoinType.LEFT); - for (int i = 1; i < jsonPaths.length - 1; i++) { - paths = paths.join(jsonPaths[i], JoinType.LEFT); - } - Path finalPath = paths.get(jsonPaths[jsonPaths.length - 1]); - return toPredicate(finalPath, filter, cb, String.class); - } - throw new IllegalArgumentException(); - }; - } - private static Predicate toPredicate( @NotNull final Expression paths, @NotNull final Filter filter, @@ -140,9 +114,9 @@ private static BiFunction, List, Predicate> computeOpe } else if (operator.equals(FilterOperator.contains)) { return (Expression paths, List texts) -> containsTexts((Expression) paths, cb, texts, type); } else if (operator.equals(FilterOperator.not_starts_with)) { - return (Expression paths, List texts) -> notStartWithTexts((Expression) paths, cb, texts); + return (Expression paths, List texts) -> notStartWithTexts((Expression) paths, cb, texts, type); } else if (operator.equals(FilterOperator.starts_with)) { - return (Expression paths, List texts) -> startWithTexts((Expression) paths, cb, texts); + return (Expression paths, List texts) -> startWithTexts((Expression) paths, cb, texts, type); } else if (operator.equals(FilterOperator.empty)) { return (Expression paths, List texts) -> empty((Expression) paths, cb, type); } else if (operator.equals(FilterOperator.not_empty)) { diff --git a/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsRuntime.java b/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsRuntime.java index 5ff689fed6..25eae3d90c 100644 --- a/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsRuntime.java +++ b/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsRuntime.java @@ -14,10 +14,10 @@ import java.util.function.BiFunction; import java.util.function.Predicate; -import static io.openbas.database.model.Filters.FilterMode.*; +import static io.openbas.database.model.Filters.FilterMode.and; +import static io.openbas.database.model.Filters.FilterMode.or; import static io.openbas.utils.schema.SchemaUtils.getFilterableProperties; import static io.openbas.utils.schema.SchemaUtils.retrieveProperty; -import static org.springframework.util.StringUtils.hasText; public class FilterUtilsRuntime { @@ -109,16 +109,7 @@ private static Map.Entry, Object> getPropertyInfo(Object obj, Prop field = obj.getClass().getDeclaredField(propertySchema.getName()); field.setAccessible(true); - // Search on child - if (propertySchema.isFilterable() && hasText(propertySchema.getPropertyRepresentative())) { - Object childObj = field.get(obj); - Field childField = childObj.getClass().getDeclaredField(propertySchema.getPropertyRepresentative()); - childField.setAccessible(true); - currentObject = childField.get(childObj); - // Direct property - } else { - currentObject = field.get(obj); - } + currentObject = field.get(obj); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/openbas-framework/src/main/java/io/openbas/utils/JpaUtils.java b/openbas-framework/src/main/java/io/openbas/utils/JpaUtils.java index 0ad8bd2c14..a3d539bd5e 100644 --- a/openbas-framework/src/main/java/io/openbas/utils/JpaUtils.java +++ b/openbas-framework/src/main/java/io/openbas/utils/JpaUtils.java @@ -4,8 +4,6 @@ import jakarta.persistence.criteria.*; import jakarta.validation.constraints.NotNull; -import java.util.Optional; - import static org.springframework.util.StringUtils.hasText; public class JpaUtils { @@ -17,16 +15,21 @@ private JpaUtils() { public static Expression toPath( @NotNull final PropertySchema propertySchema, @NotNull final Root root) { + // Path + if (hasText(propertySchema.getPath())) { + String[] jsonPaths = propertySchema.getPath().split("\\."); + if (jsonPaths.length > 0) { + Join paths = root.join(jsonPaths[0], JoinType.LEFT); + for (int i = 1; i < jsonPaths.length - 1; i++) { + paths = paths.join(jsonPaths[i], JoinType.LEFT); + } + return paths.get(jsonPaths[jsonPaths.length - 1]); + } + } // Join if (propertySchema.getJoinTable() != null) { PropertySchema.JoinTable joinTable = propertySchema.getJoinTable(); - return root.join(joinTable.getJoinOn(), JoinType.LEFT) - .get(Optional.ofNullable(propertySchema.getPropertyRepresentative()).orElse("id")); - } - // Search on child - else if (hasText(propertySchema.getPropertyRepresentative())) { - return root.get(propertySchema.getName()).get(propertySchema.getPropertyRepresentative()); - // Direct property + return root.join(joinTable.getJoinOn(), JoinType.LEFT).get("id"); } else { return root.get(propertySchema.getName()); } diff --git a/openbas-framework/src/main/java/io/openbas/utils/OperationUtilsJpa.java b/openbas-framework/src/main/java/io/openbas/utils/OperationUtilsJpa.java index 82b64fa8c6..116c86ccf7 100644 --- a/openbas-framework/src/main/java/io/openbas/utils/OperationUtilsJpa.java +++ b/openbas-framework/src/main/java/io/openbas/utils/OperationUtilsJpa.java @@ -35,6 +35,10 @@ public static Predicate notContainsTexts( public static Predicate notContainsText( Expression paths, CriteriaBuilder cb, String text, Class type) { + if (isEmpty(text)) { + return cb.conjunction(); + } + return containsText(paths, cb, text, type).not(); } @@ -54,7 +58,7 @@ public static Predicate containsTexts( } public static Predicate containsText(Expression paths, CriteriaBuilder cb, String text, Class type) { - if (text == null) { + if (isEmpty(text)) { return cb.conjunction(); } @@ -62,7 +66,7 @@ public static Predicate containsText(Expression paths, CriteriaBuilder c Expression values = lower(arrayToString(avals(paths, cb), cb), cb); return cb.like(values, "%" + text.toLowerCase() + "%"); } - if (type.isArray()) { + if (type.isArray() || type.isAssignableFrom(List.class)) { return cb.like( lower(arrayToString(paths, cb), cb), "%" + text.toLowerCase() + "%" @@ -89,6 +93,10 @@ public static Predicate notEqualsTexts(Expression paths, CriteriaBuilder } private static Predicate notEqualsText(Expression paths, CriteriaBuilder cb, String text, Class type) { + if (isEmpty(text)) { + return cb.conjunction(); + } + return equalsText(paths, cb, text, type).not(); } @@ -107,7 +115,7 @@ public static Predicate equalsTexts(Expression paths, CriteriaBuilder cb } private static Predicate equalsText(Expression paths, CriteriaBuilder cb, String text, Class type) { - if (text == null) { + if (isEmpty(text)) { return cb.conjunction(); } @@ -124,33 +132,46 @@ private static Predicate equalsText(Expression paths, CriteriaBuilder cb // -- NOT START WITH -- - public static Predicate notStartWithTexts(Expression paths, CriteriaBuilder cb, List texts) { + public static Predicate notStartWithTexts(Expression paths, CriteriaBuilder cb, List texts, Class type) { if (isEmpty(texts)) { return cb.conjunction(); } - Predicate[] predicates = texts.stream().map(text -> notStartWithText(paths, cb, text)).toArray(Predicate[]::new); + Predicate[] predicates = texts.stream().map(text -> notStartWithText(paths, cb, text, type)).toArray(Predicate[]::new); return cb.or(predicates); } - public static Predicate notStartWithText(Expression paths, CriteriaBuilder cb, String text) { - return startWithText(paths, cb, text).not(); + public static Predicate notStartWithText(Expression paths, CriteriaBuilder cb, String text, Class type) { + if (isEmpty(text)) { + return cb.conjunction(); + } + + return startWithText(paths, cb, text, type).not(); } // -- START WITH -- - public static Predicate startWithTexts(Expression paths, CriteriaBuilder cb, List texts) { + public static Predicate startWithTexts(Expression paths, CriteriaBuilder cb, List texts, Class type) { if (isEmpty(texts)) { return cb.conjunction(); } - Predicate[] predicates = texts.stream().map(text -> startWithText(paths, cb, text)).toArray(Predicate[]::new); + Predicate[] predicates = texts.stream().map(text -> startWithText(paths, cb, text, type)).toArray(Predicate[]::new); return cb.or(predicates); } - public static Predicate startWithText(Expression paths, CriteriaBuilder cb, String text) { + public static Predicate startWithText(Expression paths, CriteriaBuilder cb, String text, Class type) { + if (isEmpty(text)) { + return cb.conjunction(); + } + + if (type.isAssignableFrom(Map.class) || type.getName().contains("ImmutableCollections")) { + Expression values = lower(arrayToString(avals(paths, cb), cb), cb); + return cb.like(cb.lower(values), text.toLowerCase() + "%"); + } + return cb.like(cb.lower(paths), text.toLowerCase() + "%"); } @@ -164,7 +185,7 @@ public static Predicate notEmpty(Expression paths, CriteriaBuilder cb, C public static Predicate empty(Expression paths, CriteriaBuilder cb, Class type) { Expression finalPaths; - if (type.isArray()) { + if (type.isArray() || type.isAssignableFrom(List.class)) { finalPaths = arrayToString(paths, cb); } else { finalPaths = paths; @@ -191,6 +212,10 @@ public static Predicate greaterThanTexts(Expression paths, CriteriaBuil } public static Predicate greaterThanText(Expression paths, CriteriaBuilder cb, String text) { + if (isEmpty(text)) { + return cb.conjunction(); + } + return cb.greaterThan(paths, Instant.parse(text)); } @@ -207,6 +232,10 @@ public static Predicate greaterThanOrEqualTexts(Expression paths, Crite } public static Predicate greaterThanOrEqualText(Expression paths, CriteriaBuilder cb, String text) { + if (isEmpty(text)) { + return cb.conjunction(); + } + return cb.greaterThanOrEqualTo(paths, Instant.parse(text)); } @@ -223,6 +252,10 @@ public static Predicate lessThanTexts(Expression paths, CriteriaBuilder } public static Predicate lessThanText(Expression paths, CriteriaBuilder cb, String text) { + if (isEmpty(text)) { + return cb.conjunction(); + } + return cb.lessThan(paths, Instant.parse(text)); } @@ -239,10 +272,13 @@ public static Predicate lessThanOrEqualTexts(Expression paths, Criteria } public static Predicate lessThanOrEqualText(Expression paths, CriteriaBuilder cb, String text) { + if (isEmpty(text)) { + return cb.conjunction(); + } + return cb.lessThanOrEqualTo(paths, Instant.parse(text)); } - // -- CUSTOM FUNCTION -- private static Expression lowerArray(Expression paths, CriteriaBuilder cb) { @@ -275,4 +311,8 @@ private static boolean isEmpty(List texts) { return texts == null || texts.isEmpty() || texts.stream().anyMatch(s -> !hasText(s)); } + private static boolean isEmpty(String text) { + return !hasText(text); + } + } diff --git a/openbas-framework/src/main/java/io/openbas/utils/schema/PropertySchema.java b/openbas-framework/src/main/java/io/openbas/utils/schema/PropertySchema.java index 8c504559ed..54579f58ab 100644 --- a/openbas-framework/src/main/java/io/openbas/utils/schema/PropertySchema.java +++ b/openbas-framework/src/main/java/io/openbas/utils/schema/PropertySchema.java @@ -34,9 +34,9 @@ public class PropertySchema { private final List availableValues; private final boolean dynamicValues; private final boolean sortable; - private final String propertyRepresentative; private final JoinTable joinTable; + private final String path; @Singular("propertySchema") private final List propertiesSchema; diff --git a/openbas-framework/src/main/java/io/openbas/utils/schema/SchemaUtils.java b/openbas-framework/src/main/java/io/openbas/utils/schema/SchemaUtils.java index 0ce99ce4d4..251a4f6b1a 100644 --- a/openbas-framework/src/main/java/io/openbas/utils/schema/SchemaUtils.java +++ b/openbas-framework/src/main/java/io/openbas/utils/schema/SchemaUtils.java @@ -10,12 +10,16 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import static org.springframework.util.StringUtils.hasText; @@ -57,19 +61,16 @@ private SchemaUtils() { // -- SCHEMA -- public static List schema(@NotNull Class clazz) { - List properties = cacheMap.get(clazz); - - if (properties == null) { - Field[] fields = clazz.getDeclaredFields(); - properties = new ArrayList<>(computeProperties(fields)); + return cacheMap.computeIfAbsent(clazz, SchemaUtils::computeSchema); + } - while (clazz.getSuperclass() != null) { - clazz = clazz.getSuperclass(); - fields = clazz.getDeclaredFields(); - properties.addAll(computeProperties(fields)); - } + private static List computeSchema(Class clazz) { + List properties = new ArrayList<>(); - cacheMap.put(clazz, properties); + while (clazz != null) { + properties.addAll(computeProperties(clazz.getDeclaredFields())); + properties.addAll(computeMethods(clazz.getDeclaredMethods())); + clazz = clazz.getSuperclass(); } return properties; @@ -77,126 +78,127 @@ public static List schema(@NotNull Class clazz) { // -- PROPERTIES -- - private static List computeProperties( - @NotNull final Field[] fields) { - return Arrays.stream(fields).map(field -> { - PropertySchema.PropertySchemaBuilder builder = PropertySchema.builder() - .name(field.getName()) - .type(field.getType()) - .multiple(field.getType().isArray() || Collection.class.isAssignableFrom(field.getType())); - - // Enum type -> compute available values - if (field.getType().isEnum()) { - Object[] enumValues = field.getType().getEnumConstants(); - List enumNames = new ArrayList<>(); - for (Object enumValue : enumValues) { - enumNames.add(enumValue.toString()); - } - builder.availableValues(enumNames); - } - if (field.getType().isArray() && field.getType().getComponentType().isEnum()) { - Object[] enumValues = field.getType().getComponentType().getEnumConstants(); - List enumNames = new ArrayList<>(); - for (Object enumValue : enumValues) { - enumNames.add(enumValue.toString()); - } - builder.availableValues(enumNames); - } + private static List computeProperties(@NotNull Field[] fields) { + return Arrays.stream(fields) + .map(SchemaUtils::buildPropertySchemaFromField) + .collect(Collectors.toList()); + } - Annotation[] annotations = field.getDeclaredAnnotations(); - for (Annotation annotation : annotations) { - // Json property name - computeJsonName(builder, annotation); - // Unicity - computeUnicity(builder, annotation); - // Required - computeRequired(builder, annotation); - // Queryable - computeQueryable(builder, annotation, field); - // Join table - computeJoinTable(builder, annotation, field); - } + private static PropertySchema buildPropertySchemaFromField(Field field) { + PropertySchema.PropertySchemaBuilder builder = PropertySchema.builder() + .name(field.getName()) + .type(field.getType()) + .multiple(field.getType().isArray() || Collection.class.isAssignableFrom(field.getType())); - return builder.build(); - }).toList(); - } + if (field.getType().isEnum() || (field.getType().isArray() && field.getType().getComponentType().isEnum())) { + builder.availableValues( + getEnumNames(field.getType().isArray() ? field.getType().getComponentType() : field.getType())); + } - public static PropertySchema retrieveProperty(List propertySchemas, String jsonFieldPath) { - if (jsonFieldPath.contains("\\.")) { - throw new IllegalArgumentException("Deep path is not allowed"); + for (Annotation annotation : field.getDeclaredAnnotations()) { + processAnnotations(builder, annotation, field); } - return propertySchemas.stream() - .filter(p -> jsonFieldPath.equals(p.getJsonName())) - .findFirst() - .orElseThrow(); + return builder.build(); } - public static List getSearchableProperties(List propertySchemas) { - return propertySchemas.stream().filter(PropertySchema::isSearchable).toList(); - } + // -- METHODS -- - public static List getFilterableProperties(List propertySchemas) { - return propertySchemas.stream().filter(PropertySchema::isFilterable).toList(); + private static List computeMethods(@NotNull Method[] methods) { + return Arrays.stream(methods) + .map(SchemaUtils::buildPropertySchemaFromMethod) + .collect(Collectors.toList()); } - public static List getSortableProperties(List propertySchemas) { - return propertySchemas.stream().filter(PropertySchema::isSortable).toList(); + private static PropertySchema buildPropertySchemaFromMethod(Method method) { + PropertySchema.PropertySchemaBuilder builder = PropertySchema.builder() + .name(method.getName()) + .type(method.getReturnType()) + .multiple(method.getReturnType().isArray() || Collection.class.isAssignableFrom(method.getReturnType())); + + if (method.getReturnType().isEnum()) { + builder.availableValues(getEnumNames(method.getReturnType())); + } else if (method.getReturnType().isArray() || method.getGenericReturnType() instanceof ParameterizedType) { + Class enumType = null; + if (method.getReturnType().isArray()) { + enumType = method.getReturnType().getComponentType(); + } else { + Type typeArgument = ((ParameterizedType) method.getGenericReturnType()).getActualTypeArguments()[0]; + if (typeArgument instanceof Class) { + enumType = (Class) typeArgument; + } + } + if (enumType != null && enumType.isEnum()) { + builder.availableValues(getEnumNames(enumType)); + } + } + + for (Annotation annotation : method.getDeclaredAnnotations()) { + processAnnotations(builder, annotation, method); + } + + return builder.build(); } - // -- PRIVATE -- + private static List getEnumNames(Class enumType) { + return Arrays.stream(enumType.getEnumConstants()) + .map(Object::toString) + .collect(Collectors.toList()); + } - private static void computeJsonName( + private static void processAnnotations( @NotNull final PropertySchema.PropertySchemaBuilder builder, - @NotNull final Annotation annotation) { + @NotNull final Annotation annotation, + @NotNull final Object member) { + if (annotation.annotationType().equals(JsonProperty.class)) { builder.jsonName(((JsonProperty) annotation).value()); - } - } - - private static void computeUnicity( - @NotNull final PropertySchema.PropertySchemaBuilder builder, - @NotNull final Annotation annotation) { - if (annotation.annotationType().equals(Column.class)) { + } else if (annotation.annotationType().equals(Column.class)) { builder.unicity(((Column) annotation).unique()); + } else if (REQUIRED_ANNOTATIONS.contains(annotation.annotationType())) { + builder.mandatory(true); + } else if (annotation.annotationType().equals(Queryable.class)) { + Queryable queryable = member instanceof Field + ? ((Field) member).getAnnotation(Queryable.class) + : ((Method) member).getAnnotation(Queryable.class); + if (queryable != null) { + builder.searchable(queryable.searchable()) + .filterable(queryable.filterable()) + .dynamicValues(queryable.dynamicValues()) + .sortable(queryable.sortable()) + .path(queryable.path()); + if (member instanceof Method) { + builder.type(queryable.clazz()); // Override + } else if (member instanceof Field && hasText(queryable.path())) { + builder.type(queryable.clazz()); // Override + } + } + } else if (annotation.annotationType().equals(JoinTable.class)) { + builder.joinTable(PropertySchema.JoinTable.builder().joinOn(((Field) member).getName()).build()); } } - private static void computeRequired( - @NotNull final PropertySchema.PropertySchemaBuilder builder, - @NotNull final Annotation annotation) { - if (REQUIRED_ANNOTATIONS.contains(annotation.annotationType())) { - builder.mandatory(true); + public static PropertySchema retrieveProperty(List propertySchemas, String jsonFieldPath) { + if (jsonFieldPath.contains("\\.")) { + throw new IllegalArgumentException("Deep path is not allowed"); } + + return propertySchemas.stream() + .filter(p -> jsonFieldPath.equals(p.getJsonName())) + .findFirst() + .orElseThrow( + () -> new IllegalArgumentException("This path is not handled by Queryable annotation: " + jsonFieldPath)); } - private static void computeQueryable( - @NotNull final PropertySchema.PropertySchemaBuilder builder, - @NotNull final Annotation annotation, - @NotNull final Field field) { - if (annotation.annotationType().equals(Queryable.class)) { - Queryable queryable = field.getAnnotation(Queryable.class); - builder.searchable(queryable.searchable()); - builder.filterable(queryable.filterable()); - builder.dynamicValues(queryable.dynamicValues()); - builder.sortable(queryable.sortable()); - String propertyValue = queryable.property(); - if (hasText(propertyValue)) { - builder.propertyRepresentative(propertyValue); - } - } + public static List getSearchableProperties(List propertySchemas) { + return propertySchemas.stream().filter(PropertySchema::isSearchable).collect(Collectors.toList()); } - private static void computeJoinTable( - @NotNull final PropertySchema.PropertySchemaBuilder builder, - @NotNull final Annotation annotation, - @NotNull final Field field) { - if (annotation.annotationType().equals(JoinTable.class)) { - PropertySchema.JoinTable joinTableProperty = PropertySchema.JoinTable.builder() - .joinOn(field.getName()) - .build(); - builder.joinTable(joinTableProperty); - } + public static List getFilterableProperties(List propertySchemas) { + return propertySchemas.stream().filter(PropertySchema::isFilterable).collect(Collectors.toList()); } + public static List getSortableProperties(List propertySchemas) { + return propertySchemas.stream().filter(PropertySchema::isSortable).collect(Collectors.toList()); + } } diff --git a/openbas-front/src/actions/Payload.js b/openbas-front/src/actions/Payload.js index 2e9fe89187..d54b772008 100644 --- a/openbas-front/src/actions/Payload.js +++ b/openbas-front/src/actions/Payload.js @@ -1,15 +1,5 @@ import * as schema from './Schema'; -import { getReferential, putReferential, postReferential, delReferential, simplePostCall } from '../utils/Action'; - -export const fetchPayloads = () => (dispatch) => { - const uri = '/api/payloads'; - return getReferential(schema.arrayOfPayloads, uri)(dispatch); -}; - -export const fetchPayload = (payloadId) => (dispatch) => { - const uri = `/api/payloads/${payloadId}`; - return getReferential(schema.payload, uri)(dispatch); -}; +import { delReferential, postReferential, putReferential, simplePostCall } from '../utils/Action'; export const searchPayloads = (paginationInput) => { const data = paginationInput; diff --git a/openbas-front/src/actions/injects/Inject.d.ts b/openbas-front/src/actions/injects/Inject.d.ts index 4201bdfd68..7335cfe6f0 100644 --- a/openbas-front/src/actions/injects/Inject.d.ts +++ b/openbas-front/src/actions/injects/Inject.d.ts @@ -9,7 +9,7 @@ export type InjectInput = { inject_depends_duration_seconds: number; }; -export type InjectStore = Omit & { +export type InjectStore = Omit & { inject_tags: string[] | undefined; inject_teams: string[] | undefined; inject_content: { expectationScore: number, challenges: string[] | undefined } @@ -18,6 +18,8 @@ export type InjectStore = Omit & { diff --git a/openbas-front/src/admin/components/atomic_testings/AtomicTestings.tsx b/openbas-front/src/admin/components/atomic_testings/AtomicTestings.tsx index 8405a73bf6..bcd76b41f6 100644 --- a/openbas-front/src/admin/components/atomic_testings/AtomicTestings.tsx +++ b/openbas-front/src/admin/components/atomic_testings/AtomicTestings.tsx @@ -5,10 +5,14 @@ import { useFormatter } from '../../../components/i18n'; import { useHelper } from '../../../store'; import Breadcrumbs from '../../../components/Breadcrumbs'; import type { UserHelper } from '../../../actions/helper'; -import type { Inject, InjectResultDTO } from '../../../utils/api-types'; +import type { FilterGroup, Inject, InjectResultDTO } from '../../../utils/api-types'; import { createAtomicTesting, searchAtomicTestings } from '../../../actions/atomic_testings/atomic-testing-actions'; import CreateInject from '../common/injects/CreateInject'; -import InjectList from './InjectList'; +import InjectDtoList from './InjectDtoList'; +import { buildEmptyFilter } from '../../../components/common/queryable/filter/FilterUtils'; +import useQueryable from '../../../components/common/queryable/useQueryable'; +import { buildSearchPagination } from '../../../components/common/queryable/QueryableUtils'; +import { initSorting } from '../../../components/common/queryable/Page'; import ButtonCreate from '../../../components/common/ButtonCreate'; // eslint-disable-next-line consistent-return @@ -40,12 +44,35 @@ const AtomicTestings = () => { }); }; + const availableFilterNames = [ + 'inject_kill_chain_phases', + 'inject_tags', + 'inject_title', + 'inject_type', + 'inject_updated_at', + ]; + + const quickFilter: FilterGroup = { + mode: 'and', + filters: [ + buildEmptyFilter('inject_kill_chain_phases', 'contains'), + buildEmptyFilter('inject_tags', 'contains'), + ], + }; + const { queryableHelpers, searchPaginationInput } = useQueryable('atomic-testing', buildSearchPagination({ + sorts: initSorting('inject_updated_at', 'DESC'), + filterGroup: quickFilter, + })); + return ( <> - `/admin/atomic_testings/${injectId}`} + queryableHelpers={queryableHelpers} + searchPaginationInput={searchPaginationInput} + availableFilterNames={availableFilterNames} /> {userAdmin && (<> setOpenCreateDrawer(true)} /> diff --git a/openbas-front/src/admin/components/atomic_testings/InjectList.tsx b/openbas-front/src/admin/components/atomic_testings/InjectDtoList.tsx similarity index 86% rename from openbas-front/src/admin/components/atomic_testings/InjectList.tsx rename to openbas-front/src/admin/components/atomic_testings/InjectDtoList.tsx index 0bb46a655e..18a4630770 100644 --- a/openbas-front/src/admin/components/atomic_testings/InjectList.tsx +++ b/openbas-front/src/admin/components/atomic_testings/InjectDtoList.tsx @@ -7,16 +7,22 @@ import type { InjectResultDTO, SearchPaginationInput } from '../../../utils/api- import AtomicTestingResult from './atomic_testing/AtomicTestingResult'; import ItemTargets from '../../../components/ItemTargets'; import Empty from '../../../components/Empty'; -import { initSorting, type Page } from '../../../components/common/queryable/Page'; -import PaginationComponent from '../../../components/common/pagination/PaginationComponent'; -import SortHeadersComponent from '../../../components/common/pagination/SortHeadersComponent'; +import { type Page } from '../../../components/common/queryable/Page'; import InjectorContract from '../common/injects/InjectorContract'; import ItemStatus from '../../../components/ItemStatus'; import AtomicTestingPopover from './atomic_testing/AtomicTestingPopover'; import { isNotEmptyField } from '../../../utils/utils'; -import { buildSearchPagination } from '../../../components/common/queryable/QueryableUtils'; +import { QueryableHelpers } from '../../../components/common/queryable/QueryableHelpers'; +import PaginationComponentV2 from '../../../components/common/queryable/pagination/PaginationComponentV2'; +import SortHeadersComponentV2 from '../../../components/common/queryable/sort/SortHeadersComponentV2'; const useStyles = makeStyles(() => ({ + itemHead: { + textTransform: 'uppercase', + }, + item: { + height: 50, + }, bodyItems: { display: 'flex', }, @@ -28,39 +34,26 @@ const useStyles = makeStyles(() => ({ textOverflow: 'ellipsis', paddingRight: 10, }, - itemHead: { - marginBottom: 10, - textTransform: 'uppercase', - cursor: 'pointer', - }, - item: { - height: 50, - }, })); const inlineStyles: Record = { inject_type: { width: '10%', - cursor: 'default', }, inject_title: { width: '20%', }, 'inject_status.tracking_sent_date': { width: '15%', - cursor: 'default', }, inject_status: { width: '10%', - cursor: 'default', }, inject_targets: { width: '20%', - cursor: 'default', }, inject_expectations: { width: '10%', - cursor: 'default', }, inject_updated_at: { width: '15%', @@ -70,11 +63,17 @@ const inlineStyles: Record = { interface Props { fetchInjects: (input: SearchPaginationInput) => Promise<{ data: Page }>; goTo: (injectId: string) => string; + queryableHelpers: QueryableHelpers; + searchPaginationInput: SearchPaginationInput; + availableFilterNames?: string[]; } -const InjectList: FunctionComponent = ({ +const InjectDtoList: FunctionComponent = ({ fetchInjects, goTo, + queryableHelpers, + searchPaginationInput, + availableFilterNames = [], }) => { // Standard hooks const classes = useStyles(); @@ -82,9 +81,6 @@ const InjectList: FunctionComponent = ({ // Filter and sort hook const [injects, setInjects] = useState([]); - const [searchPaginationInput, setSearchPaginationInput] = useState(buildSearchPagination({ - sorts: initSorting('inject_updated_at', 'DESC'), - })); // Headers const headers = useMemo(() => [ @@ -149,10 +145,13 @@ const InjectList: FunctionComponent = ({ return ( <> - = ({ } /> @@ -230,4 +227,4 @@ const InjectList: FunctionComponent = ({ ); }; -export default InjectList; +export default InjectDtoList; diff --git a/openbas-front/src/admin/components/common/filters/InjectorContractSwitchFilter.tsx b/openbas-front/src/admin/components/common/filters/InjectorContractSwitchFilter.tsx new file mode 100644 index 0000000000..bda11c45c0 --- /dev/null +++ b/openbas-front/src/admin/components/common/filters/InjectorContractSwitchFilter.tsx @@ -0,0 +1,76 @@ +import { Switch } from '@mui/material'; +import React, { FunctionComponent, useEffect, useState } from 'react'; +import { useFormatter } from '../../../../components/i18n'; +import { FilterHelpers } from '../../../../components/common/queryable/filter/FilterHelpers'; +import type { FilterGroup } from '../../../../utils/api-types'; + +export const INJECTOR_CONTRACT_INJECTOR_FILTER_KEY = 'injector_contract_injector'; + +export const INJECTOR_CONTRACT_PLAYERS_ONLY = [ + '49229430-b5b5-431f-ba5b-f36f599b0233', // Challenge + '8d932e36-353c-48fa-ba6f-86cb7b02ed19', // Channel + '41b4dd55-5bd1-4614-98cd-9e3770753306', // Email + '6981a39d-e219-4016-a235-cf7747994abc', // Manual + 'e5aefbca-cf8f-4a57-9384-0503a8ffc22f', // SMS +]; + +interface Props { + filterHelpers: FilterHelpers; + filterGroup?: FilterGroup; +} + +const InjectorContractSwitchFilter: FunctionComponent = ({ + filterHelpers, + filterGroup, +}) => { + // Standard hooks + const { t } = useFormatter(); + + const isChecked = () => { + const filter = filterGroup?.filters?.find((f) => f.key === INJECTOR_CONTRACT_INJECTOR_FILTER_KEY); + if (!filter) { + return false; + } + return filter.values?.some((v) => INJECTOR_CONTRACT_PLAYERS_ONLY.includes(v)); + }; + + const [enablePlayerFilter, setEnablePlayerFilter] = useState(isChecked); + + const onChange = (event: React.ChangeEvent) => { + const { checked } = event.target; + setEnablePlayerFilter(checked); + if (checked) { + filterHelpers.handleAddMultipleValueFilter( + INJECTOR_CONTRACT_INJECTOR_FILTER_KEY, + INJECTOR_CONTRACT_PLAYERS_ONLY, + ); + } else { + filterHelpers.handleAddMultipleValueFilter( + INJECTOR_CONTRACT_INJECTOR_FILTER_KEY, + [], + ); + } + }; + + useEffect(() => { + const isFilterChecked = isChecked(); + if (enablePlayerFilter !== isFilterChecked) { + setEnablePlayerFilter(isFilterChecked); + } + }, [filterGroup]); + + return ( + <> + + {t('Targeting Players only')} + + ); +}; + +export default InjectorContractSwitchFilter; diff --git a/openbas-front/src/admin/components/common/injects/CreateInject.tsx b/openbas-front/src/admin/components/common/injects/CreateInject.tsx index 2d173e7a15..a10f356a23 100644 --- a/openbas-front/src/admin/components/common/injects/CreateInject.tsx +++ b/openbas-front/src/admin/components/common/injects/CreateInject.tsx @@ -1,13 +1,13 @@ -import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; -import { Chip, Grid, List, ListItemButton, ListItemIcon, ListItemText, Tooltip } from '@mui/material'; +import React, { CSSProperties, FunctionComponent, useEffect, useMemo, useRef, useState } from 'react'; +import { Chip, Grid, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Tooltip } from '@mui/material'; import { makeStyles } from '@mui/styles'; import { KeyboardArrowRight } from '@mui/icons-material'; import { useFormatter } from '../../../../components/i18n'; import { searchInjectorContracts } from '../../../../actions/InjectorContracts'; import computeAttackPatterns from '../../../../utils/injector_contract/InjectorContractUtils'; -import type { FilterGroup, Inject, InjectorContractOutput } from '../../../../utils/api-types'; +import type { FilterGroup, Inject, InjectorContractOutput, KillChainPhase } from '../../../../utils/api-types'; import { initSorting } from '../../../../components/common/queryable/Page'; -import { emptyFilterGroup } from '../../../../components/common/queryable/filter/FilterUtils'; +import { buildEmptyFilter } from '../../../../components/common/queryable/filter/FilterUtils'; import { useAppDispatch } from '../../../../utils/hooks'; import { useHelper } from '../../../../store'; import type { AttackPatternHelper } from '../../../../actions/attack_patterns/attackpattern-helper'; @@ -24,19 +24,22 @@ import { fetchKillChainPhases } from '../../../../actions/KillChainPhase'; import { isNotEmptyField } from '../../../../utils/utils'; import PaginationComponentV2 from '../../../../components/common/queryable/pagination/PaginationComponentV2'; import useQueryable from '../../../../components/common/queryable/useQueryable'; +import SortHeadersComponentV2 from '../../../../components/common/queryable/sort/SortHeadersComponentV2'; const useStyles = makeStyles(() => ({ - container: { - height: 30, + itemHead: { + textTransform: 'uppercase', + }, + bodyItems: { display: 'flex', - alignItems: 'center', }, - containerItem: { - float: 'left', + bodyItem: { + height: 20, + fontSize: 13, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', - paddingRight: 20, + paddingRight: 10, }, chipInList: { fontSize: 12, @@ -52,17 +55,17 @@ const useStyles = makeStyles(() => ({ }, })); -const inlineStyles = { - killChainPhase: { +const inlineStyles: Record = { + kill_chain_phase: { width: '20%', }, - label: { + injector_contract_labels: { width: '45%', }, - platform: { - width: '12%', + injector_contract_platforms: { + width: '15%', }, - attackPatterns: { + attack_patterns: { width: '20%', }, }; @@ -80,16 +83,6 @@ interface Props { }; } -const atomicFilter: FilterGroup = { - mode: 'and', - filters: [ - { - key: 'injector_contract_atomic_testing', - operator: 'eq', - values: ['true'], - }], -}; - const CreateInject: FunctionComponent = ({ title, onCreateInject, open = false, handleClose, isAtomic = false, presetValues, ...props }) => { // Standard hooks const classes = useStyles(); @@ -108,6 +101,84 @@ const CreateInject: FunctionComponent = ({ title, onCreateInject, open = dispatch(fetchAttackPatterns()); }); + // Headers + const headers = useMemo(() => [ + { + field: 'kill_chain_phase', + label: 'Kill chain phase', + isSortable: false, + value: (_: InjectorContractOutput, killChainPhase: KillChainPhase, __: Record) => (killChainPhase ? killChainPhase.phase_name : t('Unknown')), + }, + { + field: 'injector_contract_labels', + label: 'Label', + isSortable: false, + value: (contract: InjectorContractOutput, _: KillChainPhase, __: Record) => + {tPick(contract.injector_contract_labels)} + , + }, + { + field: 'injector_contract_platforms', + label: 'Platforms', + isSortable: false, + value: (contract: InjectorContractOutput, _: KillChainPhase, __: Record) => contract.injector_contract_platforms?.map( + (platform: string) => , + ), + }, + { + field: 'attack_patterns', + label: 'Attack patterns', + isSortable: false, + value: (contract: InjectorContractOutput, _: KillChainPhase, contractAttackPatterns: Record) => contractAttackPatterns + .map((contractAttackPattern: AttackPatternStore) => ( + + )), + }, + ], []); + + // Filters + const quickFilter: FilterGroup = { + mode: 'and', + filters: [ + buildEmptyFilter('injector_contract_kill_chain_phases', 'contains'), + buildEmptyFilter('injector_contract_injector', 'contains'), + buildEmptyFilter('injector_contract_platforms', 'contains'), + ], + }; + + const addAtomicFilter = (filterGroup: FilterGroup) => { + const filters = filterGroup.filters ?? []; + if (filters.map((f) => f.key).includes('injector_contract_atomic_testing')) { + return filterGroup; + } + + filters.push({ + key: 'injector_contract_atomic_testing', + operator: 'eq', + values: ['true'], + }); + + return { + ...filterGroup, + filters, + }; + }; + + const availableFilterNames = [ + 'injector_contract_attack_patterns', + 'injector_contract_injector', + 'injector_contract_kill_chain_phases', + 'injector_contract_labels', + 'injector_contract_platforms', + 'injector_contract_players', + ]; + // Contracts const [contracts, setContracts] = useState([]); // as we don't know the type of the content of a contract we need to put any here @@ -116,7 +187,7 @@ const CreateInject: FunctionComponent = ({ title, onCreateInject, open = const initSearchPaginationInput = () => { return ({ sorts: initSorting('injector_contract_labels'), - filterGroup: isAtomic ? atomicFilter : emptyFilterGroup, + filterGroup: isAtomic ? addAtomicFilter(quickFilter) : quickFilter, size: 100, page: 0, }); @@ -169,12 +240,29 @@ const CreateInject: FunctionComponent = ({ title, onCreateInject, open = searchPaginationInput={searchPaginationInput} setContent={setContracts} entityPrefix="injector_contract" - availableFilterNames={['injector_contract_attack_patterns', 'injector_contract_platforms', 'injector_contract_injector', 'injector_contract_kill_chain_phases']} + availableFilterNames={availableFilterNames} queryableHelpers={queryableHelpers} disablePagination attackPatterns={attackPatterns} /> +  } + > + + + } + /> + {contracts.map((contract, index) => { const contractAttackPatterns = computeAttackPatterns(contract, attackPatternsMap); // eslint-disable-next-line max-len @@ -196,29 +284,16 @@ const CreateInject: FunctionComponent = ({ title, onCreateInject, open = -
- {resolvedContractKillChainPhase ? resolvedContractKillChainPhase.phase_name : t('Unknown')} -
- -
- {tPick(contract.injector_contract_labels)} +
+ {headers.map((header) => ( +
+ {header.value(contract, resolvedContractKillChainPhase, contractAttackPatterns)}
- -
- {contract.injector_contract_platforms?.map((platform) => )} -
-
- {contractAttackPatterns.map((contractAttackPattern) => ( - - ))} -
+ ))}
} /> diff --git a/openbas-front/src/admin/components/common/injects/InjectPopover.tsx b/openbas-front/src/admin/components/common/injects/InjectPopover.tsx index 8f81079edd..6a02a145d0 100644 --- a/openbas-front/src/admin/components/common/injects/InjectPopover.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectPopover.tsx @@ -7,19 +7,18 @@ import { DialogContent, DialogContentText, IconButton, + Link, Menu, MenuItem, + SnackbarCloseReason, Table, TableBody, TableCell, TableRow, - SnackbarCloseReason, - Link, } from '@mui/material'; import { MoreVert } from '@mui/icons-material'; import { useFormatter } from '../../../../components/i18n'; import Transition from '../../../../components/common/Transition'; -import type { InjectStore } from '../../../../actions/injects/Inject'; import { InjectContext, PermissionsContext } from '../Context'; import type { Inject, InjectStatus, InjectStatusExecution, InjectTestStatus } from '../../../../utils/api-types'; import { duplicateInjectForExercise, duplicateInjectForScenario, tryInject } from '../../../../actions/Inject'; @@ -30,8 +29,20 @@ import { useHelper } from '../../../../store'; import type { ExercisesHelper } from '../../../../actions/exercises/exercise-helper'; import DialogTest from '../../../../components/common/DialogTest'; +type InjectPopoverType = { + inject_id: string, + inject_exercise?: string, + inject_scenario?: string, + inject_status?: InjectStatus, + inject_testable?: boolean, + inject_teams?: string[], + inject_type?: string, + inject_enabled?: boolean, + inject_title?: string, +}; + interface Props { - inject: InjectStore; + inject: InjectPopoverType; setSelectedInjectId: (injectId: Inject['inject_id']) => void; isDisabled: boolean; canBeTested?: boolean; diff --git a/openbas-front/src/admin/components/payloads/Payloads.tsx b/openbas-front/src/admin/components/payloads/Payloads.tsx index 63d2c705ef..fffa271b89 100644 --- a/openbas-front/src/admin/components/payloads/Payloads.tsx +++ b/openbas-front/src/admin/components/payloads/Payloads.tsx @@ -30,7 +30,6 @@ import { buildEmptyFilter } from '../../../components/common/queryable/filter/Fi const useStyles = makeStyles(() => ({ itemHead: { textTransform: 'uppercase', - cursor: 'pointer', }, item: { height: 50, @@ -68,14 +67,12 @@ const useStyles = makeStyles(() => ({ const inlineStyles: Record = { payload_type: { width: '10%', - cursor: 'default', }, payload_name: { width: '20%', }, payload_platforms: { width: '10%', - cursor: 'default', }, payload_description: { width: '10%', @@ -97,10 +94,10 @@ const inlineStyles: Record = { const Payloads = () => { // Standard hooks const classes = useStyles(); + const { t, nsdt } = useFormatter(); const dispatch = useAppDispatch(); const [selectedPayload, setSelectedPayload] = useState(null); - const { t, nsdt } = useFormatter(); const { documentsMap, collectorsMap } = useHelper((helper: DocumentHelper & CollectorHelper) => ({ documentsMap: helper.getDocumentsMap(), collectorsMap: helper.getCollectorsMap(), @@ -132,7 +129,7 @@ const Payloads = () => { { field: 'payload_platforms', label: 'Platforms', - isSortable: true, + isSortable: false, value: (payload: PayloadStore) => payload.payload_platforms?.map( (platform) => , ), @@ -146,7 +143,7 @@ const Payloads = () => { { field: 'payload_tags', label: 'Tags', - isSortable: true, + isSortable: false, value: (payload: PayloadStore) => { { field: 'payload_status', label: 'Status', - isSortable: true, + isSortable: false, value: (payload: PayloadStore) => { }, ], []); + const availableFilterNames = [ + 'payload_attack_patterns', + 'payload_description', + 'payload_name', + 'payload_platforms', + 'payload_source', + 'payload_status', + 'payload_tags', + 'payload_updated_at', + ]; const [payloads, setPayloads] = useState([]); const { queryableHelpers, searchPaginationInput } = useQueryable('payloads', buildSearchPagination({ sorts: initSorting('payload_name'), @@ -218,9 +225,7 @@ const Payloads = () => { searchPaginationInput={searchPaginationInput} setContent={setPayloads} entityPrefix="payload" - availableFilterNames={ - ['payload_attack_patterns', 'payload_description', 'payload_name', 'payload_platforms', 'payload_source', 'payload_status', 'payload_tags', 'payload_updated_at'] - } + availableFilterNames={availableFilterNames} queryableHelpers={queryableHelpers} exportProps={exportProps} /> @@ -229,6 +234,7 @@ const Payloads = () => { classes={{ root: classes.itemHead }} divider={false} style={{ paddingTop: 0 }} + secondaryAction={<> } > { /> } /> -   {payloads.map((payload: PayloadStore) => { const collector = payload.payload_collector ? collectorsMap[payload.payload_collector] : null; diff --git a/openbas-front/src/admin/components/scenarios/Scenarios.tsx b/openbas-front/src/admin/components/scenarios/Scenarios.tsx index 3ad71026a5..153a73b427 100644 --- a/openbas-front/src/admin/components/scenarios/Scenarios.tsx +++ b/openbas-front/src/admin/components/scenarios/Scenarios.tsx @@ -1,15 +1,15 @@ import { makeStyles } from '@mui/styles'; -import { Card, CardActionArea, CardContent, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; +import { List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; import { MovieFilterOutlined } from '@mui/icons-material'; -import React, { CSSProperties, useEffect, useMemo, useState } from 'react'; -import classNames from 'classnames'; +import React, { CSSProperties, useMemo, useState } from 'react'; import { useFormatter } from '../../../components/i18n'; import { useHelper } from '../../../store'; import type { TagHelper, UserHelper } from '../../../actions/helper'; -import { fetchScenarioStatistic, searchScenarios } from '../../../actions/scenarios/scenario-actions'; +import { searchScenarios } from '../../../actions/scenarios/scenario-actions'; import type { ScenarioStore } from '../../../actions/scenarios/Scenario'; import ScenarioCreation from './ScenarioCreation'; import Breadcrumbs from '../../../components/Breadcrumbs'; +import { initSorting } from '../../../components/common/queryable/Page'; import ItemTags from '../../../components/ItemTags'; import ItemSeverity from '../../../components/ItemSeverity'; import PlatformIcon from '../../../components/PlatformIcon'; @@ -22,31 +22,15 @@ import { useAppDispatch } from '../../../utils/hooks'; import useQueryable from '../../../components/common/queryable/useQueryable'; import { buildSearchPagination } from '../../../components/common/queryable/QueryableUtils'; import ScenarioPopover from './scenario/ScenarioPopover'; +import { fetchStatistics } from '../../../actions/Application'; import SortHeadersComponentV2 from '../../../components/common/queryable/sort/SortHeadersComponentV2'; import PaginationComponentV2 from '../../../components/common/queryable/pagination/PaginationComponentV2'; -import type { Theme } from '../../../components/Theme'; -import type { FilterGroup, ScenarioStatistic } from '../../../utils/api-types'; -import { scenarioCategories } from './ScenarioForm'; +import type { FilterGroup } from '../../../utils/api-types'; import { buildEmptyFilter } from '../../../components/common/queryable/filter/FilterUtils'; -import { initSorting } from '../../../components/common/queryable/Page'; -const useStyles = makeStyles((theme: Theme) => ({ - card: { - overflow: 'hidden', - width: 250, - height: 100, - marginRight: 20, - }, - cardSelected: { - border: `1px solid ${theme.palette.secondary.main}`, - }, - area: { - width: '100%', - height: '100%', - }, +const useStyles = makeStyles(() => ({ itemHead: { textTransform: 'uppercase', - cursor: 'pointer', }, item: { height: 50, @@ -79,11 +63,9 @@ const inlineStyles: Record = { }, scenario_platforms: { width: '10%', - cursor: 'default', }, scenario_tags: { width: '18%', - cursor: 'default', }, scenario_updated_at: { width: '10%', @@ -145,10 +127,10 @@ const Scenarios = () => { value: (scenario: ScenarioStore) => { const platforms = scenario.scenario_platforms ?? []; if (platforms.length === 0) { - return ; + return ; } return platforms.map( - (platform: string) => , + (platform: string) => , ); }, }, @@ -168,67 +150,32 @@ const Scenarios = () => { const [scenarios, setScenarios] = useState([]); - // Category filter - const CATEGORY_FILTER_KEY = 'scenario_category'; - const scenarioFilter: FilterGroup = { + // Filters + const availableFilterNames = [ + 'scenario_category', + 'scenario_kill_chain_phases', + 'scenario_name', + 'scenario_platforms', + 'scenario_recurrence', + 'scenario_severity', + 'scenario_tags', + 'scenario_updated_at', + ]; + + const quickFilter: FilterGroup = { mode: 'and', - filters: [buildEmptyFilter(CATEGORY_FILTER_KEY, 'eq')], + filters: [ + buildEmptyFilter('scenario_category', 'contains'), + buildEmptyFilter('scenario_kill_chain_phases', 'contains'), + buildEmptyFilter('scenario_tags', 'contains'), + ], }; + const { queryableHelpers, searchPaginationInput } = useQueryable('scenarios', buildSearchPagination({ sorts: initSorting('scenario_updated_at', 'DESC'), - filterGroup: scenarioFilter, + filterGroup: quickFilter, })); - const handleOnClickCategory = (category?: string) => { - if (!category) { - // Clear filter - queryableHelpers.filterHelpers.handleAddMultipleValueFilter( - CATEGORY_FILTER_KEY, - [], - ); - } else { - queryableHelpers.filterHelpers.handleAddSingleValueFilter( - CATEGORY_FILTER_KEY, - category, - ); - } - }; - const getCategoryValue = () => searchPaginationInput.filterGroup?.filters?.find((f) => f.key === CATEGORY_FILTER_KEY)?.values; - const hasCategory = (category: string) => getCategoryValue()?.includes(category); - const noCategory = () => getCategoryValue()?.length === 0; - - // Statistic - const [statistic, setStatistic] = useState(); - const fetchStatistics = () => { - fetchScenarioStatistic().then((result: { data: ScenarioStatistic }) => setStatistic(result.data)); - }; - useEffect(() => { - fetchStatistics(); - }, []); - - const categoryCard = (category: string, count: number) => ( - handleOnClickCategory(category)} - className={classNames({ [classes.cardSelected]: hasCategory(category) })} - > - - -
- -
-
- {t(scenarioCategories.get(category) ?? category)} -
-
- {count} {t('scenarios')} -
-
-
-
- ); - // Export const exportProps = { exportType: 'scenario', @@ -248,37 +195,12 @@ const Scenarios = () => { return ( <> -
- handleOnClickCategory()} - className={classNames({ [classes.cardSelected]: noCategory() })} - > - - -
- -
-
- {t('All categories')} -
-
- {statistic?.scenarios_global_count ?? '-'} {t('scenarios')} -
-
-
-
- {Object.entries(statistic?.scenarios_attack_scenario_count ?? {}).map(([key, value]) => ( - categoryCard(key, value) - ))} -
diff --git a/openbas-front/src/admin/components/scenarios/scenario/ScenarioStatus.tsx b/openbas-front/src/admin/components/scenarios/scenario/ScenarioStatus.tsx index 6c02c86790..12d24d9553 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/ScenarioStatus.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/ScenarioStatus.tsx @@ -30,6 +30,9 @@ interface Props { variant?: 'list'; } +export const SCENARIO_SCHEDULED_STATUS = 'Scheduled'; +export const SCENARIO_NOT_SCHEDULED_STATUS = 'Not planned'; + const scenarioStatus: FunctionComponent = ({ scenario, variant, @@ -37,13 +40,14 @@ const scenarioStatus: FunctionComponent = ({ // Standard hooks const { t } = useFormatter(); const classes = useStyles(); + const style = variant === 'list' ? classes.chipInList : classes.chip; if (scenario.scenario_recurrence) { return ( ); } @@ -51,7 +55,7 @@ const scenarioStatus: FunctionComponent = ({ ); }; diff --git a/openbas-front/src/admin/components/simulations/ExerciseList.tsx b/openbas-front/src/admin/components/simulations/ExerciseList.tsx index d47c2f5a60..023aec01a5 100644 --- a/openbas-front/src/admin/components/simulations/ExerciseList.tsx +++ b/openbas-front/src/admin/components/simulations/ExerciseList.tsx @@ -17,9 +17,7 @@ import SortHeadersComponentV2 from '../../../components/common/queryable/sort/So const useStyles = makeStyles(() => ({ itemHead: { - paddingLeft: 17, textTransform: 'uppercase', - cursor: 'pointer', }, item: { height: 50, diff --git a/openbas-front/src/admin/components/simulations/Exercises.tsx b/openbas-front/src/admin/components/simulations/Exercises.tsx index 58630230f7..90e2b34fae 100644 --- a/openbas-front/src/admin/components/simulations/Exercises.tsx +++ b/openbas-front/src/admin/components/simulations/Exercises.tsx @@ -15,6 +15,8 @@ import useQueryable from '../../../components/common/queryable/useQueryable'; import PaginationComponentV2 from '../../../components/common/queryable/pagination/PaginationComponentV2'; import ExercisePopover from './simulation/ExercisePopover'; import type { ExerciseStore } from '../../../actions/exercises/Exercise'; +import type { FilterGroup } from '../../../utils/api-types'; +import { buildEmptyFilter } from '../../../components/common/queryable/filter/FilterUtils'; const Exercises = () => { // Standard hooks @@ -26,8 +28,29 @@ const Exercises = () => { })); const [exercises, setExercises] = useState([]); + + // Filters + const availableFilterNames = [ + 'exercise_kill_chain_phases', + 'exercise_name', + 'exercise_scenario', + 'exercise_start_date', + 'exercise_status', + 'exercise_tags', + 'exercise_updated_at', + ]; + + const quickFilter: FilterGroup = { + mode: 'and', + filters: [ + buildEmptyFilter('exercise_kill_chain_phases', 'contains'), + buildEmptyFilter('exercise_scenario', 'contains'), + buildEmptyFilter('exercise_tags', 'contains'), + ], + }; const { queryableHelpers, searchPaginationInput } = useQueryable('simulations', buildSearchPagination({ sorts: initSorting('exercise_updated_at', 'DESC'), + filterGroup: quickFilter, })); // Export @@ -61,7 +84,7 @@ const Exercises = () => { searchPaginationInput={searchPaginationInput} setContent={setExercises} entityPrefix="exercise" - availableFilterNames={['exercise_kill_chain_phases', 'exercise_scenario', 'exercise_tags']} + availableFilterNames={availableFilterNames} queryableHelpers={queryableHelpers} exportProps={exportProps} > diff --git a/openbas-front/src/admin/components/simulations/simulation/ExercisePopover.tsx b/openbas-front/src/admin/components/simulations/simulation/ExercisePopover.tsx index 5096e6638e..4f7e992512 100644 --- a/openbas-front/src/admin/components/simulations/simulation/ExercisePopover.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/ExercisePopover.tsx @@ -21,7 +21,7 @@ import { useFormatter } from '../../../../components/i18n'; import { deleteExercise, duplicateExercise, updateExercise } from '../../../../actions/Exercise'; import { usePermissions } from '../../../../utils/Exercise'; import Transition from '../../../../components/common/Transition'; -import type { Exercise, ExerciseUpdateInput } from '../../../../utils/api-types'; +import type { ExerciseUpdateInput } from '../../../../utils/api-types'; import { useAppDispatch } from '../../../../utils/hooks'; import ButtonPopover from '../../../../components/common/ButtonPopover'; import ExerciseUpdateForm from './ExerciseUpdateForm'; @@ -36,7 +36,7 @@ import type { TagHelper, UserHelper } from '../../../../actions/helper'; export type ExerciseActionPopover = 'Duplicate' | 'Update' | 'Delete' | 'Export'; interface ExercisePopoverProps { - exercise: Exercise; + exercise: ExerciseStore; actions: ExerciseActionPopover[]; onDelete?: (result: string) => void; inList?: boolean; @@ -141,11 +141,12 @@ const ExercisePopover: FunctionComponent = ({ // Form const initialValues: ExerciseUpdateInput = { exercise_name: exercise.exercise_name, - exercise_subtitle: exercise.exercise_subtitle, + exercise_subtitle: exercise.exercise_subtitle ?? '', exercise_description: exercise.exercise_description, exercise_category: exercise.exercise_category ?? 'attack-scenario', exercise_main_focus: exercise.exercise_main_focus ?? 'incident-response', exercise_severity: exercise.exercise_severity ?? 'high', + exercise_tags: exercise.exercise_tags ?? [], }; const initialValuesEmailParameters = { setting_mail_from: exercise.exercise_mail_from, diff --git a/openbas-front/src/admin/components/simulations/simulation/overview/Exercise.tsx b/openbas-front/src/admin/components/simulations/simulation/overview/Exercise.tsx index e2065fa4aa..b11fe6056c 100644 --- a/openbas-front/src/admin/components/simulations/simulation/overview/Exercise.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/overview/Exercise.tsx @@ -21,7 +21,10 @@ import PlatformIcon from '../../../../../components/PlatformIcon'; import { useFormatter } from '../../../../../components/i18n'; import { useHelper } from '../../../../../store'; import type { ExercisesHelper } from '../../../../../actions/exercises/exercise-helper'; -import InjectList from '../../../atomic_testings/InjectList'; +import InjectDtoList from '../../../atomic_testings/InjectDtoList'; +import useQueryable from '../../../../../components/common/queryable/useQueryable'; +import { buildSearchPagination } from '../../../../../components/common/queryable/QueryableUtils'; +import { initSorting } from '../../../../../components/common/queryable/Page'; // Deprecated - https://mui.com/system/styles/basics/ // Do not use it for new code. @@ -75,6 +78,10 @@ const Exercise = () => { ); } const sortByOrder = R.sortWith([R.ascend(R.prop('phase_order'))]); + + const { queryableHelpers, searchPaginationInput } = useQueryable('simulation-injects-results', buildSearchPagination({ + sorts: initSorting('inject_updated_at', 'DESC'), + })); return ( <> { {t('Injects Results')} - searchExerciseInjects(exerciseId, input)} goTo={(injectId) => `/admin/exercises/${exerciseId}/injects/${injectId}`} + queryableHelpers={queryableHelpers} + searchPaginationInput={searchPaginationInput} /> diff --git a/openbas-front/src/components/common/queryable/filter/FilterChip.tsx b/openbas-front/src/components/common/queryable/filter/FilterChip.tsx index a4e85fdaf7..a50b54f81c 100644 --- a/openbas-front/src/components/common/queryable/filter/FilterChip.tsx +++ b/openbas-front/src/components/common/queryable/filter/FilterChip.tsx @@ -1,6 +1,7 @@ import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; import { Chip, Tooltip } from '@mui/material'; import { makeStyles } from '@mui/styles'; +import classNames from 'classnames'; import { FilterHelpers } from './FilterHelpers'; import FilterChipPopover from './FilterChipPopover'; import type { Filter, PropertySchemaDTO } from '../../../../utils/api-types'; @@ -80,19 +81,21 @@ const FilterChip: FunctionComponent = ({ }); }; - const title = () => { + const title = (withStyle: boolean) => { return ( - {t(filter.key)} {convertOperatorToIcon(t, filter.operator)} {toValues(options)} + + {t(filter.key)} {convertOperatorToIcon(t, filter.operator)} {toValues(options)} + ); }; return ( <> = ({ }; const displayOperatorAndFilter = () => { + // Specific field + if (propertySchema.schema_property_name === 'scenario_recurrence') { + return (); + } + const operators = availableOperators(propertySchema); return ( <> diff --git a/openbas-front/src/components/common/queryable/filter/specific/ScenarioStatusFilter.tsx b/openbas-front/src/components/common/queryable/filter/specific/ScenarioStatusFilter.tsx new file mode 100644 index 0000000000..a9e27384c1 --- /dev/null +++ b/openbas-front/src/components/common/queryable/filter/specific/ScenarioStatusFilter.tsx @@ -0,0 +1,68 @@ +import { Autocomplete, MenuItem, Select, TextField } from '@mui/material'; +import React, { FunctionComponent } from 'react'; +import { useFormatter } from '../../../../i18n'; +import { SCENARIO_NOT_SCHEDULED_STATUS, SCENARIO_SCHEDULED_STATUS } from '../../../../../admin/components/scenarios/scenario/ScenarioStatus'; +import type { PropertySchemaDTO } from '../../../../../utils/api-types'; +import { OperatorKeyValues } from '../FilterUtils'; +import { FilterHelpers } from '../FilterHelpers'; +import { Option } from '../../../../../utils/Option'; + +const ScenarioStatusFilter: FunctionComponent<{ propertySchema: PropertySchemaDTO, helpers: FilterHelpers }> = ({ + propertySchema, + helpers, +}) => { + // Standard hooks + const { t } = useFormatter(); + + const operators = ['eq']; + + const options: Option[] = [ + { id: SCENARIO_SCHEDULED_STATUS, label: SCENARIO_SCHEDULED_STATUS }, + { id: SCENARIO_NOT_SCHEDULED_STATUS, label: SCENARIO_NOT_SCHEDULED_STATUS }, + ]; + + const onChange = (newValue: Option | null) => { + if (newValue) { + helpers.handleAddSingleValueFilter(propertySchema.schema_property_name, newValue.id); + } + }; + + return ( + <> + + option.label ?? ''} + isOptionEqualToValue={(option, v) => option.id === v.id} + onChange={(_event, newValue) => { + onChange(newValue); + }} + renderInput={(paramsInput) => ( + + )} + /> + + ); +}; + +export default ScenarioStatusFilter; diff --git a/openbas-front/src/components/common/queryable/filter/useFilterableProperties.ts b/openbas-front/src/components/common/queryable/filter/useFilterableProperties.ts index c59d48483b..c8afd47477 100644 --- a/openbas-front/src/components/common/queryable/filter/useFilterableProperties.ts +++ b/openbas-front/src/components/common/queryable/filter/useFilterableProperties.ts @@ -3,19 +3,12 @@ import { convertJsonClassToJavaClass } from './FilterUtils'; import type { PropertySchemaDTO } from '../../../../utils/api-types'; const useFilterableProperties: (entityPrefix: string, filterNames: string[]) => Promise = (entity_prefix: string, filterNames: string[]) => { + if (filterNames.length === 0) { + return Promise.resolve([]); + } const javaClass = convertJsonClassToJavaClass(entity_prefix); return filterableProperties(javaClass, filterNames).then(((result: { data: PropertySchemaDTO[] }) => { - const propertySchemas: PropertySchemaDTO[] = result.data; - if (filterNames.some((s) => s.endsWith('_kill_chain_phases'))) { - propertySchemas.push({ - schema_property_name: `${entity_prefix}_kill_chain_phases`, - schema_property_type_array: true, - schema_property_values: [], - schema_property_has_dynamic_value: true, - schema_property_type: 'string', - }); - } - return propertySchemas; + return result.data; })); }; diff --git a/openbas-front/src/components/common/queryable/filter/useRetrieveOptions.tsx b/openbas-front/src/components/common/queryable/filter/useRetrieveOptions.tsx index 8337281a2b..2d3b296fc2 100644 --- a/openbas-front/src/components/common/queryable/filter/useRetrieveOptions.tsx +++ b/openbas-front/src/components/common/queryable/filter/useRetrieveOptions.tsx @@ -12,6 +12,7 @@ const useRetrieveOptions = () => { const searchOptions = (filterKey: string, ids: string[]) => { switch (filterKey) { case 'injector_contract_injector': + case 'inject_injector_contract': searchInjectorByIdAsOptions(ids).then((response) => { setOptions(response.data); }); @@ -19,6 +20,7 @@ const useRetrieveOptions = () => { case 'injector_contract_kill_chain_phases': case 'scenario_kill_chain_phases': case 'exercise_kill_chain_phases': + case 'inject_kill_chain_phases': searchKillChainPhasesByIdAsOption(ids).then((response) => { setOptions(response.data); }); @@ -30,6 +32,7 @@ const useRetrieveOptions = () => { break; case 'scenario_tags': case 'exercise_tags': + case 'inject_tags': searchTagByIdAsOption(ids).then((response) => { setOptions(response.data); }); diff --git a/openbas-front/src/components/common/queryable/filter/useSearchOptions.tsx b/openbas-front/src/components/common/queryable/filter/useSearchOptions.tsx index 20040bcbbf..e7dcb25617 100644 --- a/openbas-front/src/components/common/queryable/filter/useSearchOptions.tsx +++ b/openbas-front/src/components/common/queryable/filter/useSearchOptions.tsx @@ -5,13 +5,18 @@ import { searchKillChainPhasesByNameAsOption } from '../../../../actions/kill_ch import { searchTagAsOption } from '../../../../actions/tags/tag-action'; import { searchScenarioAsOption, searchScenarioCategoryAsOption } from '../../../../actions/scenarios/scenario-actions'; import { searchAttackPatternsByNameAsOption } from '../../../../actions/AttackPattern'; +import { useFormatter } from '../../../i18n'; const useSearchOptions = () => { + // Standard hooks + const { t } = useFormatter(); + const [options, setOptions] = useState([]); const searchOptions = (filterKey: string, search: string = '') => { switch (filterKey) { case 'injector_contract_injector': + case 'inject_injector_contract': searchInjectorsByNameAsOption(search).then((response) => { setOptions(response.data); }); @@ -19,6 +24,7 @@ const useSearchOptions = () => { case 'injector_contract_kill_chain_phases': case 'scenario_kill_chain_phases': case 'exercise_kill_chain_phases': + case 'inject_kill_chain_phases': searchKillChainPhasesByNameAsOption(search).then((response) => { setOptions(response.data); }); @@ -30,6 +36,7 @@ const useSearchOptions = () => { break; case 'scenario_tags': case 'exercise_tags': + case 'inject_tags': searchTagAsOption(search).then((response) => { setOptions(response.data); }); @@ -40,8 +47,8 @@ const useSearchOptions = () => { }); break; case 'scenario_category': - searchScenarioCategoryAsOption(search).then((response) => { - setOptions(response.data); + searchScenarioCategoryAsOption(search).then((response: { data: Option[] }) => { + setOptions(response.data.map((d) => ({ id: d.id, label: t(d.label) }))); }); break; default: diff --git a/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx b/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx index 72a56dcee4..f75a49231b 100644 --- a/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx +++ b/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx @@ -13,10 +13,11 @@ import type { AttackPatternStore } from '../../../../actions/attack_patterns/Att import { QueryableHelpers } from '../QueryableHelpers'; import TextSearchComponent from '../textSearch/TextSearchComponent'; import TablePaginationComponent from './TablePaginationComponent'; -import { OptionPropertySchema } from '../filter/FilterAutocomplete'; +import FilterAutocomplete, { OptionPropertySchema } from '../filter/FilterAutocomplete'; import useFilterableProperties from '../filter/useFilterableProperties'; +import FilterChips from '../filter/FilterChips'; import FilterModeChip from '../filter/FilterModeChip'; -import KillChainPhasesFilter from '../../../../admin/components/common/filters/KillChainPhasesFilter'; +import InjectorContractSwitchFilter from '../../../../admin/components/common/filters/InjectorContractSwitchFilter'; const useStyles = makeStyles(() => ({ parameters: { @@ -63,10 +64,13 @@ const PaginationComponentV2 = ({ const classes = useStyles(); const { t } = useFormatter(); - const [_properties, setProperties] = useState([]); - const [_options, setOptions] = useState([]); + const [properties, setProperties] = useState([]); + const [options, setOptions] = useState([]); useEffect(() => { + // Retrieve input from uri + queryableHelpers.uriHelpers.retrieveFromUri(); + if (entityPrefix) { useFilterableProperties(entityPrefix, availableFilterNames).then((propertySchemas: PropertySchemaDTO[]) => { const newOptions = propertySchemas.filter((property) => property.schema_property_name !== MITRE_FILTER_KEY) @@ -80,6 +84,9 @@ const PaginationComponentV2 = ({ }, []); useEffect(() => { + // Modify URI + queryableHelpers.uriHelpers.updateUri(); + // Fetch datas fetch(searchPaginationInput).then((result: { data: Page }) => { const { data } = result; @@ -89,6 +96,7 @@ const PaginationComponentV2 = ({ }, [searchPaginationInput]); // Filters + const [pristine, setPristine] = useState(true); const [openMitreFilter, setOpenMitreFilter] = React.useState(false); const computeAttackPatternNameForFilter = () => { @@ -111,9 +119,13 @@ const PaginationComponentV2 = ({ textSearchHelpers={queryableHelpers.textSearchHelpers} /> )} - {availableFilterNames?.includes(`${entityPrefix}_kill_chain_phases`) && ( - - )} + {queryableHelpers.filterHelpers && availableFilterNames?.includes('injector_contract_attack_patterns') && ( <>
setOpenMitreFilter(true)}> @@ -131,6 +143,11 @@ const PaginationComponentV2 = ({ )} + {availableFilterNames?.includes('injector_contract_players') && ( +
+ +
+ )}
{!disablePagination && ( ({ )} )} + n !== MITRE_FILTER_KEY)} + helpers={queryableHelpers.filterHelpers} + pristine={pristine} + /> ); }; diff --git a/openbas-front/src/components/common/queryable/uri/useUriState.tsx b/openbas-front/src/components/common/queryable/uri/useUriState.tsx index 8a01963cbe..073851aa5a 100644 --- a/openbas-front/src/components/common/queryable/uri/useUriState.tsx +++ b/openbas-front/src/components/common/queryable/uri/useUriState.tsx @@ -7,7 +7,7 @@ import { UriHelpers } from './UriHelpers'; import type { SearchPaginationInput } from '../../../../utils/api-types'; import { buildSearchPagination, SearchPaginationInputSchema } from '../QueryableUtils'; -const useUriState = (initSearchPaginationInput: SearchPaginationInput, onChange: (input: SearchPaginationInput) => void) => { +const useUriState = (localStorageKey: string, initSearchPaginationInput: SearchPaginationInput, onChange: (input: SearchPaginationInput) => void) => { const location = useLocation(); const navigate = useNavigate(); @@ -17,8 +17,8 @@ const useUriState = (initSearchPaginationInput: SearchPaginationInput, onChange: retrieveFromUri: () => { const encodedParams = location.search?.startsWith('?') ? location.search.substring(1) : ''; const params = atob(encodedParams); - const paramsJson = qs.parse(params) as unknown as SearchPaginationInput; - if (!R.isEmpty(paramsJson)) { + const paramsJson = qs.parse(params) as unknown as SearchPaginationInput & { key: string }; + if (!R.isEmpty(paramsJson) && paramsJson.key === localStorageKey) { try { const parse = SearchPaginationInputSchema.parse(paramsJson); setInput(buildSearchPagination(parse)); @@ -31,7 +31,7 @@ const useUriState = (initSearchPaginationInput: SearchPaginationInput, onChange: } }, updateUri: () => { - const params = qs.stringify(initSearchPaginationInput); + const params = qs.stringify({ key: localStorageKey, ...initSearchPaginationInput }); const encodedParams = btoa(params); navigate(`?${encodedParams}`); }, diff --git a/openbas-front/src/components/common/queryable/useQueryable.tsx b/openbas-front/src/components/common/queryable/useQueryable.tsx index dba37d3881..fe442791dd 100644 --- a/openbas-front/src/components/common/queryable/useQueryable.tsx +++ b/openbas-front/src/components/common/queryable/useQueryable.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; import useFiltersState from './filter/useFiltersState'; import type { FilterGroup, SearchPaginationInput, SortField } from '../../../utils/api-types'; import useTextSearchState from './textSearch/useTextSearchState'; @@ -8,10 +8,10 @@ import useSortState from './sort/useSortState'; import useUriState from './uri/useUriState'; import { buildSearchPagination } from './QueryableUtils'; -const useQueryable = (_localStorageKey: string, initSearchPaginationInput: Partial) => { +const useQueryable = (localStorageKey: string, initSearchPaginationInput: Partial) => { const finalSearchPaginationInput: SearchPaginationInput = buildSearchPagination(initSearchPaginationInput); - const [searchPaginationInput, setSearchPaginationInput] = useState(finalSearchPaginationInput); + const [searchPaginationInput, setSearchPaginationInput] = useLocalStorage(localStorageKey, finalSearchPaginationInput); // Text Search const textSearchHelpers = useTextSearchState(searchPaginationInput.textSearch, (textSearch: string, page: number) => setSearchPaginationInput({ @@ -40,7 +40,7 @@ const useQueryable = (_localStorageKey: string, initSearchPaginationInput: Parti })); // Uri - const uriHelpers = useUriState(searchPaginationInput, (input: SearchPaginationInput) => setSearchPaginationInput(input)); + const uriHelpers = useUriState(localStorageKey, searchPaginationInput, (input: SearchPaginationInput) => setSearchPaginationInput(input)); const queryableHelpers: QueryableHelpers = { textSearchHelpers, diff --git a/openbas-front/src/utils/Localization.js b/openbas-front/src/utils/Localization.js index c9b9115756..f7be12b280 100644 --- a/openbas-front/src/utils/Localization.js +++ b/openbas-front/src/utils/Localization.js @@ -854,18 +854,36 @@ const i18n = { endpoint_platform: 'Platforme', endpoint_arch: 'Architecture', endpoint_agent_version: 'Version de l\'agent', + // Inject + inject_kill_chain_phases: 'Phases de kill chain', + inject_injector_contract: 'Injector', + inject_platforms: 'Plateformes', + inject_tags: 'Tags', + inject_title: 'Titre', + inject_type: 'Type', + inject_updated_at: 'Mis à jour à', // Injector Contract injector_contract_kill_chain_phases: 'Phases de kill chain', injector_contract_injector: 'Injector', + injector_contract_labels: 'Label', injector_contract_platforms: 'Platforms', // Scenario scenario_category: 'Catégorie', scenario_kill_chain_phases: 'Phases de kill chain', + scenario_name: 'Nom', + scenario_platforms: 'Plateformes', + scenario_recurrence: 'Récurrence', + scenario_severity: 'Sévérité', scenario_tags: 'Tags', + scenario_updated_at: 'Mis à jour à', // Exercise exercise_kill_chain_phases: 'Phases de kill chain', + exercise_name: 'Nom', exercise_scenario: 'Scenario', + exercise_status: 'Statut', + exercise_start_date: 'Date de début', exercise_tags: 'Tags', + exercise_updated_at: 'Mis à jour à', // Payload payload_attack_patterns: 'Modèles d\'attaque', payload_name: 'Nom', @@ -2583,18 +2601,36 @@ const i18n = { endpoint_platform: 'Platform', endpoint_arch: 'Architecture', endpoint_agent_version: 'Agent version', + // Inject + inject_kill_chain_phases: 'Kill chain phases', + inject_injector_contract: 'Injector', + inject_platforms: 'Platforms', + inject_tags: 'Tags', + inject_title: 'Title', + inject_type: 'Type', + inject_updated_at: 'Updated at', // Injector Contract injector_contract_kill_chain_phases: 'Kill chain phases', injector_contract_injector: 'Injector', + injector_contract_labels: 'Label', injector_contract_platforms: 'Platforms', // Scenario scenario_category: 'Category', scenario_kill_chain_phases: 'Kill chain phases', + scenario_name: 'Name', + scenario_platforms: 'Platforms', + scenario_recurrence: 'Recurrence', + scenario_severity: 'Severity', scenario_tags: 'Tags', + scenario_updated_at: 'Updated at', // Exercise exercise_kill_chain_phases: 'Kill chain phases', + exercise_name: 'Name', exercise_scenario: 'Scenario', + exercise_status: 'Status', + exercise_start_date: 'Start date', exercise_tags: 'Tags', + exercise_updated_at: 'Updated at', // Payload payload_attack_patterns: 'Attack patterns', payload_name: 'Name', diff --git a/openbas-model/src/main/java/io/openbas/annotation/Queryable.java b/openbas-model/src/main/java/io/openbas/annotation/Queryable.java index 915628843a..dd65dadd1d 100644 --- a/openbas-model/src/main/java/io/openbas/annotation/Queryable.java +++ b/openbas-model/src/main/java/io/openbas/annotation/Queryable.java @@ -6,11 +6,13 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.METHOD}) public @interface Queryable { boolean searchable() default false; boolean filterable() default false; boolean dynamicValues() default false; boolean sortable() default false; - String property() default ""; + + String path() default ""; + Class clazz() default String.class; } diff --git a/openbas-model/src/main/java/io/openbas/database/model/BaseInjectStatus.java b/openbas-model/src/main/java/io/openbas/database/model/BaseInjectStatus.java index e1065a13c5..063a5f973c 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/BaseInjectStatus.java +++ b/openbas-model/src/main/java/io/openbas/database/model/BaseInjectStatus.java @@ -70,7 +70,7 @@ public abstract class BaseInjectStatus implements Base { private Integer trackingTotalSuccess; // endregion - @Queryable(searchable = true, property = "title") + @Queryable(searchable = true, path = "inject.title") @OneToOne @JoinColumn(name = "status_inject") @JsonIgnore diff --git a/openbas-model/src/main/java/io/openbas/database/model/Exercise.java b/openbas-model/src/main/java/io/openbas/database/model/Exercise.java index 3db20f5bb2..0f79a31898 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Exercise.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Exercise.java @@ -6,6 +6,7 @@ import io.openbas.annotation.Queryable; import io.openbas.database.audit.ModelBaseListener; import io.openbas.database.model.Endpoint.PLATFORM_TYPE; +import io.openbas.database.model.Scenario.SEVERITY; import io.openbas.helper.*; import jakarta.persistence.*; import jakarta.validation.constraints.Email; @@ -43,7 +44,7 @@ public class Exercise implements Base { @Getter @Column(name = "exercise_name") @JsonProperty("exercise_name") - @Queryable(searchable = true, sortable = true) + @Queryable(filterable = true, searchable = true, sortable = true) @NotBlank private String name; @@ -56,7 +57,7 @@ public class Exercise implements Base { @Column(name = "exercise_status") @JsonProperty("exercise_status") @Enumerated(EnumType.STRING) - @Queryable(sortable = true) + @Queryable(filterable = true, sortable = true) @NotNull private ExerciseStatus status = ExerciseStatus.SCHEDULED; @@ -77,8 +78,9 @@ public class Exercise implements Base { @Getter @Column(name = "exercise_severity") + @Enumerated(EnumType.STRING) @JsonProperty("exercise_severity") - private String severity; + private SEVERITY severity; @Column(name = "exercise_pause_date") @JsonIgnore @@ -86,7 +88,7 @@ public class Exercise implements Base { @Column(name = "exercise_start_date") @JsonProperty("exercise_start_date") - @Queryable(sortable = true) + @Queryable(filterable = true, sortable = true) private Instant start; @Column(name = "exercise_end_date") @@ -160,6 +162,7 @@ public class Exercise implements Base { @Column(name = "exercise_updated_at") @JsonProperty("exercise_updated_at") @NotNull + @Queryable(filterable = true, sortable = true) private Instant updatedAt = now(); // -- RELATION -- @@ -322,6 +325,7 @@ public List getPlatforms() { // -- KILL CHAIN PHASES -- @JsonProperty("exercise_kill_chain_phases") + @Queryable(filterable = true, dynamicValues = true, path = "injects.injectorContract.attackPatterns.killChainPhases.id") public List getKillChainPhases() { return getInjects().stream() .flatMap(inject -> inject.getInjectorContract() diff --git a/openbas-model/src/main/java/io/openbas/database/model/Inject.java b/openbas-model/src/main/java/io/openbas/database/model/Inject.java index 59771f54c5..c7481412f6 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Inject.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Inject.java @@ -53,7 +53,7 @@ public class Inject implements Base, Injection { private String id; @Getter - @Queryable(searchable = true, sortable = true) + @Queryable(filterable = true, searchable = true, sortable = true) @Column(name = "inject_title") @JsonProperty("inject_title") @NotBlank @@ -93,7 +93,7 @@ public class Inject implements Base, Injection { @Getter @Column(name = "inject_updated_at") - @Queryable(sortable = true) + @Queryable(filterable = true, sortable = true) @JsonProperty("inject_updated_at") @NotNull private Instant updatedAt = now(); @@ -134,6 +134,7 @@ public class Inject implements Base, Injection { @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "inject_injector_contract") @JsonProperty("inject_injector_contract") + @Queryable(filterable = true, path = "injectorContract.injector.id") private InjectorContract injectorContract; @Getter @@ -146,7 +147,7 @@ public class Inject implements Base, Injection { // CascadeType.ALL is required here because inject status are embedded @OneToOne(mappedBy = "inject", cascade = CascadeType.ALL, orphanRemoval = true) @JsonProperty("inject_status") - @Queryable(sortable = true, property = "name") + @Queryable(filterable = true, sortable = true) private InjectStatus status; @Getter @@ -156,6 +157,7 @@ public class Inject implements Base, Injection { inverseJoinColumns = @JoinColumn(name = "tag_id")) @JsonSerialize(using = MultiIdSetDeserializer.class) @JsonProperty("inject_tags") + @Queryable(filterable = true, dynamicValues = true) private Set tags = new HashSet<>(); @Getter @@ -342,6 +344,7 @@ public Instant getSentAt() { } @JsonProperty("inject_kill_chain_phases") + @Queryable(filterable = true, dynamicValues = true, path = "injectorContract.attackPatterns.killChainPhases.id") public List getKillChainPhases() { return getInjectorContract() .map(injectorContract -> @@ -363,6 +366,7 @@ public List getAttackPatterns() { } @JsonProperty("inject_type") + @Queryable(filterable = true, path = "injectorContract.labels", clazz = Map.class) private String getType() { return getInjectorContract() .map(InjectorContract::getInjector) @@ -370,6 +374,14 @@ private String getType() { .orElse(null); } + @JsonIgnore + @JsonProperty("inject_platforms") + @Queryable(filterable = true, dynamicValues = true, path = "injectorContract.platforms", clazz = String[].class) + private Optional getPlatforms() { + return getInjectorContract() + .map(InjectorContract::getPlatforms); + } + @JsonIgnore public boolean isAtomicTesting() { return this.exercise == null && this.scenario == null; diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java b/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java index a790e21ef4..dbe5f5139d 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java @@ -89,7 +89,7 @@ public class InjectorContract implements Base { @JoinColumn(name = "injector_id") @JsonSerialize(using = MonoIdDeserializer.class) @JsonProperty("injector_contract_injector") - @Queryable(filterable = true, property = "id", dynamicValues = true) + @Queryable(filterable = true, dynamicValues = true) @NotNull private Injector injector; @@ -100,7 +100,7 @@ public class InjectorContract implements Base { inverseJoinColumns = @JoinColumn(name = "attack_pattern_id")) @JsonSerialize(using = MultiIdListDeserializer.class) @JsonProperty("injector_contract_attack_patterns") - @Queryable(searchable = true, filterable = true, property = "externalId") + @Queryable(searchable = true, filterable = true, path = "attackPatterns.externalId") private List attackPatterns = new ArrayList<>(); @Column(name = "injector_contract_atomic_testing") @@ -118,6 +118,17 @@ private String getInjectorType() { return this.getInjector() != null ? this.getInjector().getType() : null; } + @JsonIgnore + @JsonProperty("injector_contract_kill_chain_phases") + @Queryable(filterable = true, dynamicValues = true, path = "attackPatterns.killChainPhases.id") + public List getKillChainPhases() { + return getAttackPatterns() + .stream() + .flatMap(attackPattern -> attackPattern.getKillChainPhases().stream()) + .distinct() + .toList(); + } + @JsonIgnore @Override public boolean isUserHasAccess(User user) { diff --git a/openbas-model/src/main/java/io/openbas/database/model/Payload.java b/openbas-model/src/main/java/io/openbas/database/model/Payload.java index d1f78718ae..b43e2cba14 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Payload.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Payload.java @@ -63,7 +63,7 @@ public enum PAYLOAD_STATUS { @NotBlank private String name; - @Queryable(filterable = true) + @Queryable(filterable = true, sortable = true) @Column(name = "payload_description") @JsonProperty("payload_description") private String description; @@ -81,7 +81,7 @@ public enum PAYLOAD_STATUS { inverseJoinColumns = @JoinColumn(name = "attack_pattern_id")) @JsonSerialize(using = MultiIdListDeserializer.class) @JsonProperty("payload_attack_patterns") - @Queryable(searchable = true, filterable = true, dynamicValues = true) + @Queryable(filterable = true, searchable = true, dynamicValues = true, path = "attackPatterns.id") private List attackPatterns = new ArrayList<>(); @Setter @@ -111,7 +111,7 @@ public enum PAYLOAD_STATUS { @JsonProperty("payload_external_id") private String externalId; - @Queryable(filterable = true) + @Queryable(filterable = true, sortable = true) @Setter @Column(name = "payload_source") @Enumerated(EnumType.STRING) @@ -135,7 +135,7 @@ public enum PAYLOAD_STATUS { // -- TAG -- - @Queryable(filterable = true, sortable = true, dynamicValues = true) + @Queryable(filterable = true, dynamicValues = true) @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "payloads_tags", joinColumns = @JoinColumn(name = "payload_id"), @@ -151,7 +151,7 @@ public enum PAYLOAD_STATUS { @NotNull private Instant createdAt = now(); - @Queryable(filterable = true) + @Queryable(filterable = true, sortable = true) @Column(name = "payload_updated_at") @JsonProperty("payload_updated_at") @NotNull diff --git a/openbas-model/src/main/java/io/openbas/database/model/Scenario.java b/openbas-model/src/main/java/io/openbas/database/model/Scenario.java index c6c2f916ab..2ac7402a24 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Scenario.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Scenario.java @@ -32,253 +32,279 @@ @Table(name = "scenarios") @EntityListeners(ModelBaseListener.class) @NamedEntityGraphs({ - @NamedEntityGraph( - name = "Scenario.tags-injects", - attributeNodes = { - @NamedAttributeNode("tags"), - @NamedAttributeNode("injects") - } - ) + @NamedEntityGraph( + name = "Scenario.tags-injects", + attributeNodes = { + @NamedAttributeNode("tags"), + @NamedAttributeNode("injects") + } + ) }) public class Scenario implements Base { - @Id - @UuidGenerator - @Column(name = "scenario_id") - @JsonProperty("scenario_id") - @NotBlank - private String id; - - @Column(name = "scenario_name") - @JsonProperty("scenario_name") - @Queryable(searchable = true, filterable = true) - @NotBlank - private String name; - - @Column(name = "scenario_description") - @JsonProperty("scenario_description") - private String description; - - @Column(name = "scenario_subtitle") - @JsonProperty("scenario_subtitle") - private String subtitle; - - @Column(name = "scenario_category") - @JsonProperty("scenario_category") - @Queryable(filterable = true, dynamicValues = true) - private String category; - - @Column(name = "scenario_main_focus") - @JsonProperty("scenario_main_focus") - private String mainFocus; - - @Column(name = "scenario_severity") - @JsonProperty("scenario_severity") - private String severity; - - @Column(name = "scenario_external_reference") - @JsonProperty("scenario_external_reference") - private String externalReference; - - @Column(name = "scenario_external_url") - @JsonProperty("scenario_external_url") - private String externalUrl; - - // -- RECURRENCE -- - - @Column(name = "scenario_recurrence") - @JsonProperty("scenario_recurrence") - private String recurrence; - - @Column(name = "scenario_recurrence_start") - @JsonProperty("scenario_recurrence_start") - private Instant recurrenceStart; - - @Column(name = "scenario_recurrence_end") - @JsonProperty("scenario_recurrence_end") - private Instant recurrenceEnd; - - // -- MESSAGE -- - - @Column(name = "scenario_message_header") - @JsonProperty("scenario_message_header") - private String header = "EXERCISE - EXERCISE - EXERCISE"; - - @Column(name = "scenario_message_footer") - @JsonProperty("scenario_message_footer") - private String footer = "EXERCISE - EXERCISE - EXERCISE"; - - @Column(name = "scenario_mail_from") - @JsonProperty("scenario_mail_from") - @Email - @NotBlank - private String from; - - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "scenario_mails_reply_to", joinColumns = @JoinColumn(name = "scenario_id")) - @Column(name = "scenario_reply_to", nullable = false) - @JsonProperty("scenario_mails_reply_to") - private List replyTos = new ArrayList<>(); - - // -- AUDIT -- - - @Column(name = "scenario_created_at") - @JsonProperty("scenario_created_at") - @NotNull - private Instant createdAt = now(); - - @Column(name = "scenario_updated_at") - @JsonProperty("scenario_updated_at") - @NotNull - private Instant updatedAt = now(); - - // -- RELATION -- - - @OneToMany(mappedBy = "scenario", fetch = FetchType.EAGER) - @JsonIgnore - private List grants = new ArrayList<>(); - - @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY) - @JsonProperty("scenario_injects") - @JsonSerialize(using = MultiIdListDeserializer.class) - @Getter(NONE) - private Set injects = new HashSet<>(); - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "scenarios_teams", - joinColumns = @JoinColumn(name = "scenario_id"), - inverseJoinColumns = @JoinColumn(name = "team_id")) - @JsonSerialize(using = MultiIdListDeserializer.class) - @JsonProperty("scenario_teams") - private List teams = new ArrayList<>(); - @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - @JsonProperty("scenario_teams_users") - @JsonSerialize(using = MultiModelDeserializer.class) - private List teamUsers = new ArrayList<>(); - @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY, cascade = CascadeType.ALL) - @JsonIgnore - private List objectives = new ArrayList<>(); - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "scenarios_tags", - joinColumns = @JoinColumn(name = "scenario_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id")) - @JsonSerialize(using = MultiIdSetDeserializer.class) - @JsonProperty("scenario_tags") - @Queryable(filterable = true, dynamicValues = true) - private Set tags = new HashSet<>(); - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "scenarios_documents", - joinColumns = @JoinColumn(name = "scenario_id"), - inverseJoinColumns = @JoinColumn(name = "document_id")) - @JsonSerialize(using = MultiIdListDeserializer.class) - @JsonProperty("scenario_documents") - private List documents = new ArrayList<>(); - @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY) - @JsonSerialize(using = MultiIdListDeserializer.class) - @JsonProperty("scenario_articles") - private List
articles = new ArrayList<>(); - @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY, cascade = CascadeType.ALL) - @JsonSerialize(using = MultiIdListDeserializer.class) - @JsonProperty("scenario_lessons_categories") - private List lessonsCategories = new ArrayList<>(); - @OneToMany(fetch = FetchType.LAZY) - @JoinTable(name = "scenarios_exercises", - joinColumns = @JoinColumn(name = "scenario_id"), - inverseJoinColumns = @JoinColumn(name = "exercise_id")) - @JsonSerialize(using = MultiIdListDeserializer.class) - @JsonProperty("scenario_exercises") - private List exercises; - @Getter - @Column(name = "scenario_lessons_anonymized") - @JsonProperty("scenario_lessons_anonymized") - private boolean lessonsAnonymized = false; - - // -- LESSONS -- - - public List getInjects() { - return new ArrayList<>(this.injects); - } - - // -- SECURITY -- - - @JsonProperty("scenario_planners") - @JsonSerialize(using = MultiIdListDeserializer.class) - public List getPlanners() { - return getUsersByType(this.getGrants(), PLANNER); - } - - @JsonProperty("scenario_observers") - @JsonSerialize(using = MultiIdListDeserializer.class) - public List getObservers() { - return getUsersByType(this.getGrants(), PLANNER, OBSERVER); - } - - // -- STATISTICS -- - - @JsonProperty("scenario_injects_statistics") - public Map getInjectStatistics() { - return InjectStatisticsHelper.getInjectStatistics(this.getInjects()); - } - - @JsonProperty("scenario_all_users_number") - public long usersAllNumber() { - return getTeams().stream().mapToLong(Team::getUsersNumber).sum(); - } - - @JsonProperty("scenario_users_number") - public long usersNumber() { - return getTeamUsers().stream().map(ScenarioTeamUser::getUser).distinct().count(); - } - - @JsonProperty("scenario_users") - @JsonSerialize(using = MultiIdListDeserializer.class) - public List getUsers() { - return getTeamUsers().stream() - .map(ScenarioTeamUser::getUser) - .distinct() - .toList(); - } - - @JsonProperty("scenario_communications_number") - public long getCommunicationsNumber() { - return getInjects().stream().mapToLong(Inject::getCommunicationsNumber).sum(); - } - - // -- CHANNELS -- - - public List
getArticlesForChannel(Channel channel) { - return this.articles.stream() - .filter(article -> article.getChannel().equals(channel)) - .toList(); - } - - // -- PLATFORMS -- - @JsonProperty("scenario_platforms") - public List getPlatforms() { - return getInjects().stream() - .flatMap(inject -> inject.getInjectorContract() - .map(InjectorContract::getPlatforms) - .stream()) - .flatMap(Arrays::stream) - .filter(Objects::nonNull) - .distinct() - .toList(); - } - - // -- KILL CHAIN PHASES -- - @JsonProperty("scenario_kill_chain_phases") - public List getKillChainPhases() { - return getInjects().stream() - .flatMap(inject -> inject.getInjectorContract() - .map(InjectorContract::getAttackPatterns) - .stream() - .flatMap(Collection::stream) - .flatMap(attackPattern -> attackPattern.getKillChainPhases().stream())) - .distinct() - .toList(); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } + public enum SEVERITY { + @JsonProperty("low") + low, + @JsonProperty("medium") + medium, + @JsonProperty("high") + high, + @JsonProperty("critical") + critical, + } + + @Id + @UuidGenerator + @Column(name = "scenario_id") + @JsonProperty("scenario_id") + @NotBlank + private String id; + + @Column(name = "scenario_name") + @JsonProperty("scenario_name") + @Queryable(filterable = true, searchable = true, sortable = true) + @NotBlank + private String name; + + @Column(name = "scenario_description") + @JsonProperty("scenario_description") + private String description; + + @Column(name = "scenario_subtitle") + @JsonProperty("scenario_subtitle") + private String subtitle; + + @Column(name = "scenario_category") + @JsonProperty("scenario_category") + @Queryable(filterable = true, sortable = true, dynamicValues = true) + private String category; + + @Column(name = "scenario_main_focus") + @JsonProperty("scenario_main_focus") + private String mainFocus; + + @Column(name = "scenario_severity") + @Enumerated(EnumType.STRING) + @JsonProperty("scenario_severity") + @Queryable(filterable = true, sortable = true) + private SEVERITY severity; + + @Column(name = "scenario_external_reference") + @JsonProperty("scenario_external_reference") + private String externalReference; + + @Column(name = "scenario_external_url") + @JsonProperty("scenario_external_url") + private String externalUrl; + + // -- RECURRENCE -- + + @Column(name = "scenario_recurrence") + @JsonProperty("scenario_recurrence") + @Queryable(filterable = true) + private String recurrence; + + @Column(name = "scenario_recurrence_start") + @JsonProperty("scenario_recurrence_start") + private Instant recurrenceStart; + + @Column(name = "scenario_recurrence_end") + @JsonProperty("scenario_recurrence_end") + private Instant recurrenceEnd; + + // -- MESSAGE -- + + @Column(name = "scenario_message_header") + @JsonProperty("scenario_message_header") + private String header = "EXERCISE - EXERCISE - EXERCISE"; + + @Column(name = "scenario_message_footer") + @JsonProperty("scenario_message_footer") + private String footer = "EXERCISE - EXERCISE - EXERCISE"; + + @Column(name = "scenario_mail_from") + @JsonProperty("scenario_mail_from") + @Email + @NotBlank + private String from; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "scenario_mails_reply_to", joinColumns = @JoinColumn(name = "scenario_id")) + @Column(name = "scenario_reply_to", nullable = false) + @JsonProperty("scenario_mails_reply_to") + private List replyTos = new ArrayList<>(); + + // -- AUDIT -- + + @Column(name = "scenario_created_at") + @JsonProperty("scenario_created_at") + @NotNull + private Instant createdAt = now(); + + @Column(name = "scenario_updated_at") + @JsonProperty("scenario_updated_at") + @NotNull + @Queryable(filterable = true, sortable = true) + private Instant updatedAt = now(); + + // -- RELATION -- + + @OneToMany(mappedBy = "scenario", fetch = FetchType.EAGER) + @JsonIgnore + private List grants = new ArrayList<>(); + + @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY) + @JsonProperty("scenario_injects") + @JsonSerialize(using = MultiIdListDeserializer.class) + @Getter(NONE) + private Set injects = new HashSet<>(); + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "scenarios_teams", + joinColumns = @JoinColumn(name = "scenario_id"), + inverseJoinColumns = @JoinColumn(name = "team_id")) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("scenario_teams") + private List teams = new ArrayList<>(); + + @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("scenario_teams_users") + @JsonSerialize(using = MultiModelDeserializer.class) + private List teamUsers = new ArrayList<>(); + + @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JsonIgnore + private List objectives = new ArrayList<>(); + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "scenarios_tags", + joinColumns = @JoinColumn(name = "scenario_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id")) + @JsonSerialize(using = MultiIdSetDeserializer.class) + @JsonProperty("scenario_tags") + @Queryable(filterable = true, dynamicValues = true, path = "tags.id") + private Set tags = new HashSet<>(); + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "scenarios_documents", + joinColumns = @JoinColumn(name = "scenario_id"), + inverseJoinColumns = @JoinColumn(name = "document_id")) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("scenario_documents") + private List documents = new ArrayList<>(); + + @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("scenario_articles") + private List
articles = new ArrayList<>(); + + @OneToMany(mappedBy = "scenario", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("scenario_lessons_categories") + private List lessonsCategories = new ArrayList<>(); + + @OneToMany(fetch = FetchType.LAZY) + @JoinTable(name = "scenarios_exercises", + joinColumns = @JoinColumn(name = "scenario_id"), + inverseJoinColumns = @JoinColumn(name = "exercise_id")) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("scenario_exercises") + private List exercises; + + @Getter + @Column(name = "scenario_lessons_anonymized") + @JsonProperty("scenario_lessons_anonymized") + private boolean lessonsAnonymized = false; + + // -- LESSONS -- + + public List getInjects() { + return new ArrayList<>(this.injects); + } + + // -- SECURITY -- + + @JsonProperty("scenario_planners") + @JsonSerialize(using = MultiIdListDeserializer.class) + public List getPlanners() { + return getUsersByType(this.getGrants(), PLANNER); + } + + @JsonProperty("scenario_observers") + @JsonSerialize(using = MultiIdListDeserializer.class) + public List getObservers() { + return getUsersByType(this.getGrants(), PLANNER, OBSERVER); + } + + // -- STATISTICS -- + + @JsonProperty("scenario_injects_statistics") + public Map getInjectStatistics() { + return InjectStatisticsHelper.getInjectStatistics(this.getInjects()); + } + + @JsonProperty("scenario_all_users_number") + public long usersAllNumber() { + return getTeams().stream().mapToLong(Team::getUsersNumber).sum(); + } + + @JsonProperty("scenario_users_number") + public long usersNumber() { + return getTeamUsers().stream().map(ScenarioTeamUser::getUser).distinct().count(); + } + + @JsonProperty("scenario_users") + @JsonSerialize(using = MultiIdListDeserializer.class) + public List getUsers() { + return getTeamUsers().stream() + .map(ScenarioTeamUser::getUser) + .distinct() + .toList(); + } + + @JsonProperty("scenario_communications_number") + public long getCommunicationsNumber() { + return getInjects().stream().mapToLong(Inject::getCommunicationsNumber).sum(); + } + + // -- CHANNELS -- + + public List
getArticlesForChannel(Channel channel) { + return this.articles.stream() + .filter(article -> article.getChannel().equals(channel)) + .toList(); + } + + // -- PLATFORMS -- + @JsonProperty("scenario_platforms") + @Queryable(filterable = true, path = "injects.injectorContract.platforms", clazz = String[].class) + public List getPlatforms() { + return getInjects().stream() + .flatMap(inject -> inject.getInjectorContract() + .map(InjectorContract::getPlatforms) + .stream()) + .flatMap(Arrays::stream) + .filter(Objects::nonNull) + .distinct() + .toList(); + } + + // -- KILL CHAIN PHASES -- + @JsonProperty("scenario_kill_chain_phases") + @Queryable(filterable = true, dynamicValues = true, path = "injects.injectorContract.attackPatterns.killChainPhases.id") + public List getKillChainPhases() { + return getInjects().stream() + .flatMap(inject -> inject.getInjectorContract() + .map(InjectorContract::getAttackPatterns) + .stream() + .flatMap(Collection::stream) + .flatMap(attackPattern -> attackPattern.getKillChainPhases().stream())) + .distinct() + .toList(); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } diff --git a/openbas-model/src/main/java/io/openbas/database/raw/RawPaginationScenario.java b/openbas-model/src/main/java/io/openbas/database/raw/RawPaginationScenario.java index 7637042d5c..f23b6f6af4 100644 --- a/openbas-model/src/main/java/io/openbas/database/raw/RawPaginationScenario.java +++ b/openbas-model/src/main/java/io/openbas/database/raw/RawPaginationScenario.java @@ -1,5 +1,6 @@ package io.openbas.database.raw; +import io.openbas.database.model.Scenario.SEVERITY; import lombok.Data; import java.time.Instant; @@ -12,7 +13,7 @@ public class RawPaginationScenario { private String scenario_id; private String scenario_name; - private String scenario_severity; + private SEVERITY scenario_severity; private String scenario_category; private String scenario_recurrence; private Instant scenario_updated_at; @@ -22,7 +23,7 @@ public class RawPaginationScenario { public RawPaginationScenario( String id, String name, - String severity, + SEVERITY severity, String category, String recurrence, Instant updatedAt, diff --git a/openbas-model/src/main/java/io/openbas/database/specification/ScenarioSpecification.java b/openbas-model/src/main/java/io/openbas/database/specification/ScenarioSpecification.java index 285e6811a4..c2acb707cb 100644 --- a/openbas-model/src/main/java/io/openbas/database/specification/ScenarioSpecification.java +++ b/openbas-model/src/main/java/io/openbas/database/specification/ScenarioSpecification.java @@ -18,6 +18,10 @@ public static Specification isRecurring() { return (root, query, cb) -> cb.isNotNull(root.get("recurrence")); } + public static Specification noRecurring() { + return (root, query, cb) -> cb.isNull(root.get("recurrence")); + } + public static Specification recurrenceStartDateBefore(@NotNull final Instant startDate) { return (root, query, cb) -> cb.or( cb.isNull(root.get("recurrenceStart")),