Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[backend/frontend] Ability to see 100 simulations in the overview of a scenario in 0.5 seconds #1995

Merged
merged 6 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import io.openbas.rest.exercise.exports.VariableMixin;
import io.openbas.rest.exercise.exports.VariableWithValueMixin;
import io.openbas.rest.exercise.form.*;
import io.openbas.rest.exercise.response.ExercisesGlobalScoresOutput;
import io.openbas.rest.exercise.service.ExerciseService;
import io.openbas.rest.helper.RestBehavior;
import io.openbas.rest.helper.TeamHelper;
Expand Down Expand Up @@ -611,6 +612,14 @@ public List<ExpectationResultsByType> globalResults(@NotBlank @PathVariable Stri
return exerciseService.getGlobalResults(exerciseId);
}

@LogExecutionTime
@PostMapping(EXERCISE_URI + "/global-scores")
@Tracing(name = "Get the global scores for exercises", layer = "api", operation = "POST")
public ExercisesGlobalScoresOutput getExercisesGlobalScores(
@Valid @RequestBody ExercisesGlobalScoresInput input) {
return exerciseService.getExercisesGlobalScores(input);
}

@LogExecutionTime
@GetMapping(EXERCISE_URI + "/{exerciseId}/injects/results-by-attack-patterns")
@PreAuthorize("isExerciseObserver(#exerciseId)")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.openbas.rest.exercise.form;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotNull;
import java.util.List;

public record ExercisesGlobalScoresInput(
@JsonProperty("exercise_ids") @NotNull List<String> exerciseIds) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.openbas.rest.exercise.response;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openbas.utils.AtomicTestingUtils.ExpectationResultsByType;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;

public record ExercisesGlobalScoresOutput(
@JsonProperty("global_scores_by_exercise_ids") @NotNull
Map<String, List<ExpectationResultsByType>> globalScoresByExerciseIds) {}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import io.openbas.rest.atomic_testing.form.TargetSimple;
import io.openbas.rest.exception.ElementNotFoundException;
import io.openbas.rest.exercise.form.ExerciseSimple;
import io.openbas.rest.exercise.form.ExercisesGlobalScoresInput;
import io.openbas.rest.exercise.response.ExercisesGlobalScoresOutput;
import io.openbas.rest.inject.service.InjectDuplicateService;
import io.openbas.service.GrantService;
import io.openbas.service.TeamService;
Expand All @@ -39,6 +41,7 @@
import jakarta.validation.constraints.NotBlank;
import java.time.Instant;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -340,6 +343,41 @@
Specification<Exercise> specificationCount,
Pageable pageable,
Map<String, Join<Base, Base>> joinMap) {
CriteriaBuilderAndExercises result =
getCriteriaBuilderAndExercises(specification, pageable, joinMap);

setComputedAttributes(result.exercises());

return getExerciseSimples(specificationCount, pageable, result);
}

public Page<ExerciseSimple> exercisesWithEmptyGlobalScore(
Specification<Exercise> specification,
Specification<Exercise> specificationCount,
Pageable pageable,
Map<String, Join<Base, Base>> joinMap) {
CriteriaBuilderAndExercises result =
getCriteriaBuilderAndExercises(specification, pageable, joinMap);

Check warning on line 360 in openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java#L359-L360

Added lines #L359 - L360 were not covered by tests

setComputedAttributesWithEmptyGlobalScore(result.exercises());

Check warning on line 362 in openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java#L362

Added line #L362 was not covered by tests

return getExerciseSimples(specificationCount, pageable, result);

Check warning on line 364 in openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java#L364

Added line #L364 was not covered by tests
}

private PageImpl<ExerciseSimple> getExerciseSimples(
Specification<Exercise> specificationCount,
Pageable pageable,
CriteriaBuilderAndExercises result) {
// -- Count Query --
Long total = countQuery(result.cb(), this.entityManager, Exercise.class, specificationCount);

return new PageImpl<>(result.exercises(), pageable, total);
}

private CriteriaBuilderAndExercises getCriteriaBuilderAndExercises(
Specification<Exercise> specification,
Pageable pageable,
Map<String, Join<Base, Base>> joinMap) {
CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();

CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Expand Down Expand Up @@ -368,14 +406,11 @@
// -- EXECUTION --
List<ExerciseSimple> exercises = execution(query);

setComputedAttribute(exercises);

// -- Count Query --
Long total = countQuery(cb, this.entityManager, Exercise.class, specificationCount);

return new PageImpl<>(exercises, pageable, total);
return new CriteriaBuilderAndExercises(cb, exercises);
}

private record CriteriaBuilderAndExercises(CriteriaBuilder cb, List<ExerciseSimple> exercises) {}

// -- SELECT --
private void select(
CriteriaBuilder cb,
Expand Down Expand Up @@ -431,67 +466,120 @@
}

// -- COMPUTED ATTRIBUTES --
private void setComputedAttribute(List<ExerciseSimple> exercises) {
// -- MAP TO GENERATE TARGETSIMPLEs
Set<String> exerciseIds =
exercises.stream().map(ExerciseSimple::getId).collect(Collectors.toSet());

if (!exerciseIds.isEmpty()) {
Map<String, List<Object[]>> teamMap =
ofNullable(teamRepository.teamsByExerciseIds(exerciseIds)).orElse(emptyList()).stream()
.filter(row -> 0 < row.length && row[0] != null)
.collect(Collectors.groupingBy(row -> (String) row[0]));

Map<String, List<Object[]>> assetMap =
ofNullable(assetRepository.assetsByExerciseIds(exerciseIds)).orElse(emptyList()).stream()
.filter(row -> 0 < row.length && row[0] != null)
.collect(Collectors.groupingBy(row -> (String) row[0]));

Map<String, List<Object[]>> assetGroupMap =
ofNullable(assetGroupRepository.assetGroupsByExerciseIds(exerciseIds))
.orElse(emptyList())
.stream()
.filter(row -> 0 < row.length && row[0] != null)
.collect(Collectors.groupingBy(row -> (String) row[0]));

Map<String, List<RawInjectExpectation>> expectationMap =
ofNullable(injectExpectationRepository.rawForComputeGlobalByExerciseIds(exerciseIds))
.orElse(emptyList())
.stream()
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(RawInjectExpectation::getExercise_id));

for (ExerciseSimple exercise : exercises) {
if (exercise.getId() != null) {
// -- GLOBAL SCORE ---
exercise.setExpectationResultByTypes(
AtomicTestingUtils.getExpectationResultByTypesFromRaw(
expectationMap.getOrDefault(exercise.getId(), emptyList())));

// -- TARGETS --
List<TargetSimple> allTargets =
Stream.concat(
injectMapper
.toTargetSimple(
teamMap.getOrDefault(exercise.getId(), emptyList()), TargetType.TEAMS)
.stream(),
Stream.concat(
injectMapper
.toTargetSimple(
assetMap.getOrDefault(exercise.getId(), emptyList()),
TargetType.ASSETS)
.stream(),
injectMapper
.toTargetSimple(
assetGroupMap.getOrDefault(exercise.getId(), emptyList()),
TargetType.ASSETS_GROUPS)
.stream()))
.toList();

exercise.getTargets().addAll(allTargets);
}
}
private void setComputedAttributes(List<ExerciseSimple> originalExercises) {
List<ExerciseSimple> exercises = getExercisesWithId(originalExercises);
if (exercises.isEmpty()) {
return;
}

Set<String> exerciseIds = getExerciseIds(exercises);
MappingsByExerciseIds mappingsByExerciseIds = getResultsByExerciseIds(exerciseIds);

Map<String, List<RawInjectExpectation>> expectationsByExerciseIds =
getExpectationsByExerciseId(exerciseIds);

for (ExerciseSimple exercise : exercises) {
setGlobalScore(exercise, expectationsByExerciseIds);

setTargets(exercise, mappingsByExerciseIds);
}
}

private void setComputedAttributesWithEmptyGlobalScore(List<ExerciseSimple> originalExercises) {
List<ExerciseSimple> exercises = getExercisesWithId(originalExercises);

Check warning on line 489 in openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java#L489

Added line #L489 was not covered by tests
if (exercises.isEmpty()) {
return;

Check warning on line 491 in openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java#L491

Added line #L491 was not covered by tests
}

MappingsByExerciseIds mappingsByExerciseIds =
getResultsByExerciseIds(getExerciseIds(exercises));

Check warning on line 495 in openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java#L494-L495

Added lines #L494 - L495 were not covered by tests

for (ExerciseSimple exercise : exercises) {
exercise.setExpectationResultByTypes(new ArrayList<>());

Check warning on line 498 in openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java#L498

Added line #L498 was not covered by tests

setTargets(exercise, mappingsByExerciseIds);
}
}

Check warning on line 502 in openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java#L500-L502

Added lines #L500 - L502 were not covered by tests

private static List<ExerciseSimple> getExercisesWithId(List<ExerciseSimple> exercises) {
return exercises.stream().filter(exercise -> exercise.getId() != null).toList();
}

private static Set<String> getExerciseIds(List<ExerciseSimple> exercises) {
return exercises.stream().map(ExerciseSimple::getId).collect(Collectors.toSet());
}

private MappingsByExerciseIds getResultsByExerciseIds(Set<String> exerciseIds) {
Map<String, List<Object[]>> teamsByExerciseIds =
getTeamsOrAssetsOrAssetGroupsByExerciseIds(teamRepository.teamsByExerciseIds(exerciseIds));

Map<String, List<Object[]>> assetsByExerciseIds =
getTeamsOrAssetsOrAssetGroupsByExerciseIds(
assetRepository.assetsByExerciseIds(exerciseIds));

Map<String, List<Object[]>> assetGroupByExerciseIds =
getTeamsOrAssetsOrAssetGroupsByExerciseIds(
assetGroupRepository.assetGroupsByExerciseIds(exerciseIds));

return new MappingsByExerciseIds(
teamsByExerciseIds, assetsByExerciseIds, assetGroupByExerciseIds);
}

private Map<String, List<Object[]>> getTeamsOrAssetsOrAssetGroupsByExerciseIds(
List<Object[]> rawTeamsOrAssetsOrAssetGroups) {
return ofNullable(rawTeamsOrAssetsOrAssetGroups).orElse(emptyList()).stream()
.filter(
rawTeamOrAssetOrAssetGroup ->
0 < rawTeamOrAssetOrAssetGroup.length && rawTeamOrAssetOrAssetGroup[0] != null)
.collect(
Collectors.groupingBy(
rawTeamOrAssetOrAssetGroup -> (String) rawTeamOrAssetOrAssetGroup[0]));

Check warning on line 536 in openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java#L536

Added line #L536 was not covered by tests
}

private record MappingsByExerciseIds(
Map<String, List<Object[]>> teamsByExerciseIds,
Map<String, List<Object[]>> assetsByExerciseIds,
Map<String, List<Object[]>> assetGroupsByExerciseIds) {}

private Map<String, List<RawInjectExpectation>> getExpectationsByExerciseId(
Set<String> exerciseIds) {
return ofNullable(injectExpectationRepository.rawForComputeGlobalByExerciseIds(exerciseIds))
.orElse(emptyList())
.stream()
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(RawInjectExpectation::getExercise_id));
}

private static void setGlobalScore(
ExerciseSimple exercise, Map<String, List<RawInjectExpectation>> expectationsByExerciseIds) {
exercise.setExpectationResultByTypes(
AtomicTestingUtils.getExpectationResultByTypesFromRaw(
expectationsByExerciseIds.getOrDefault(exercise.getId(), emptyList())));
}

private void setTargets(ExerciseSimple exercise, MappingsByExerciseIds mappingsByExerciseIds) {
List<TargetSimple> allTargets =
Stream.of(
getTargets(exercise, mappingsByExerciseIds.teamsByExerciseIds, TargetType.TEAMS)
.stream(),
getTargets(exercise, mappingsByExerciseIds.assetsByExerciseIds, TargetType.ASSETS)
.stream(),
getTargets(
exercise,
mappingsByExerciseIds.assetGroupsByExerciseIds,
TargetType.ASSETS_GROUPS)
.stream())
.flatMap(Function.identity())
.toList();
exercise.getTargets().addAll(allTargets);
}

private List<TargetSimple> getTargets(
ExerciseSimple exercise,
Map<String, List<Object[]>> targetsByExerciseIds,
TargetType targetType) {
return injectMapper.toTargetSimple(
targetsByExerciseIds.getOrDefault(exercise.getId(), emptyList()), targetType);
}

// -- SCENARIO EXERCISES --
Expand All @@ -505,6 +593,13 @@
return resultUtils.getResultsByTypes(exerciseRepository.findInjectsByExercise(exerciseId));
}

public ExercisesGlobalScoresOutput getExercisesGlobalScores(ExercisesGlobalScoresInput input) {
Map<String, List<ExpectationResultsByType>> globalScoresByExerciseIds =
input.exerciseIds().stream()
.collect(Collectors.toMap(Function.identity(), this::getGlobalResults));
return new ExercisesGlobalScoresOutput(globalScoresByExerciseIds);
}

// -- TEAMS --
@Transactional(rollbackFor = Exception.class)
public Iterable<Team> removeTeams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
(Specification<Exercise> specification,
Specification<Exercise> specificationCount,
Pageable pageable) ->
this.exerciseService.exercises(
this.exerciseService.exercisesWithEmptyGlobalScore(

Check warning on line 55 in openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioExerciseApi.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioExerciseApi.java#L55

Added line #L55 was not covered by tests
fromScenario(scenarioId).and(specification),
fromScenario(scenarioId).and(specificationCount),
pageable,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,35 @@
package io.openbas.rest.scenario;

import static io.openbas.database.model.User.ROLE_USER;
import static io.openbas.rest.scenario.ScenarioApi.SCENARIO_URI;
import static org.springframework.util.StringUtils.hasText;

import io.openbas.database.repository.ScenarioRepository;
import io.openbas.rest.helper.RestBehavior;
import io.openbas.rest.scenario.response.ScenarioStatistic;
import io.openbas.rest.scenario.service.ScenarioStatisticService;
import io.openbas.telemetry.Tracing;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.transaction.Transactional;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Secured(ROLE_USER)
@RequiredArgsConstructor
public class ScenarioStatisticApi extends RestBehavior {

private final ScenarioRepository scenarioRepository;
private final ScenarioStatisticService scenarioStatisticService;

@GetMapping(SCENARIO_URI + "/statistics")
@GetMapping(SCENARIO_URI + "/{scenarioId}/statistics")
@PreAuthorize("isScenarioObserver(#scenarioId)")
@Transactional(rollbackOn = Exception.class)
@Operation(summary = "Retrieve scenario statistics")
public ScenarioStatistic scenarioStatistic() {
ScenarioStatistic statistic = new ScenarioStatistic();
statistic.setScenariosGlobalCount(this.scenarioRepository.count());
statistic.setScenariosCategoriesCount(findTopCategories());
return statistic;
}

private Map<String, Long> findTopCategories() {
List<Object[]> results = this.scenarioRepository.findTopCategories(3);
Map<String, Long> categoryCount = new LinkedHashMap<>();
for (Object[] result : results) {
String category = (String) result[0];
if (hasText(category)) {
Long count = (Long) result[1];
categoryCount.put(category, count);
}
}
return categoryCount;
@Tracing(name = "Get scenario statistics", layer = "api", operation = "GET")
public ScenarioStatistic getScenarioStatistics(@PathVariable @NotBlank final String scenarioId) {
return scenarioStatisticService.getStatistics(scenarioId);

Check warning on line 33 in openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioStatisticApi.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioStatisticApi.java#L33

Added line #L33 was not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.openbas.rest.scenario.response;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;

public record GlobalScoreBySimulationEndDate(
@JsonProperty("simulation_end_date") @NotNull Instant simulationEndDate,
@JsonProperty("global_score_success_percentage") @NotNull float globalScoreSuccessPercentage) {}
Loading
Loading