From 7259a5d3765cd4a587c259777f78858423e3f937 Mon Sep 17 00:00:00 2001 From: Issel Parra <14161286+isselparra@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:12:04 +0100 Subject: [PATCH] [backend/frontend] display latest simulations results on scenario overview --- .../rest/scenario/ScenarioStatisticApi.java | 37 ++-- .../GlobalScoreBySimulationEndDate.java | 10 ++ .../scenario/response/ScenarioStatistic.java | 15 +- .../response/SimulationsResultsLatest.java | 11 ++ .../service/ScenarioStatisticService.java | 136 +++++++++++++++ .../src/actions/scenarios/scenario-actions.ts | 4 +- .../components/scenarios/ScenariosCard.tsx | 159 ------------------ .../scenarios/scenario/Scenario.tsx | 25 +-- .../ScenarioDistributionByExercise.tsx | 97 +++++++---- openbas-front/src/utils/api-types.d.ts | 21 ++- .../raw/RawFinishedExerciseWithInjects.java | 10 ++ .../repository/ExerciseRepository.java | 22 ++- 12 files changed, 286 insertions(+), 261 deletions(-) create mode 100644 openbas-api/src/main/java/io/openbas/rest/scenario/response/GlobalScoreBySimulationEndDate.java create mode 100644 openbas-api/src/main/java/io/openbas/rest/scenario/response/SimulationsResultsLatest.java create mode 100644 openbas-api/src/main/java/io/openbas/rest/scenario/service/ScenarioStatisticService.java delete mode 100644 openbas-front/src/admin/components/scenarios/ScenariosCard.tsx create mode 100644 openbas-model/src/main/java/io/openbas/database/raw/RawFinishedExerciseWithInjects.java diff --git a/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioStatisticApi.java b/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioStatisticApi.java index 681176caea..bb01aeab3f 100644 --- a/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioStatisticApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioStatisticApi.java @@ -1,46 +1,33 @@ 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.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 findTopCategories() { - List results = this.scenarioRepository.findTopCategories(3); - Map 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; + public ScenarioStatistic getScenarioStatistics(@PathVariable @NotBlank final String scenarioId) { + return scenarioStatisticService.getStatistics(scenarioId); } } diff --git a/openbas-api/src/main/java/io/openbas/rest/scenario/response/GlobalScoreBySimulationEndDate.java b/openbas-api/src/main/java/io/openbas/rest/scenario/response/GlobalScoreBySimulationEndDate.java new file mode 100644 index 0000000000..5e5701e296 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/scenario/response/GlobalScoreBySimulationEndDate.java @@ -0,0 +1,10 @@ +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 + double globalScoreSuccessPercentage) {} diff --git a/openbas-api/src/main/java/io/openbas/rest/scenario/response/ScenarioStatistic.java b/openbas-api/src/main/java/io/openbas/rest/scenario/response/ScenarioStatistic.java index 95bf266932..fb3e647b09 100644 --- a/openbas-api/src/main/java/io/openbas/rest/scenario/response/ScenarioStatistic.java +++ b/openbas-api/src/main/java/io/openbas/rest/scenario/response/ScenarioStatistic.java @@ -1,15 +1,8 @@ package io.openbas.rest.scenario.response; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Map; -import lombok.Data; +import jakarta.validation.constraints.NotNull; -@Data -public class ScenarioStatistic { - - @JsonProperty("scenarios_global_count") - private long scenariosGlobalCount; - - @JsonProperty("scenarios_attack_scenario_count") - private Map scenariosCategoriesCount; -} +public record ScenarioStatistic( + @JsonProperty("simulations_results_latest") @NotNull + SimulationsResultsLatest simulationsResultsLatest) {} diff --git a/openbas-api/src/main/java/io/openbas/rest/scenario/response/SimulationsResultsLatest.java b/openbas-api/src/main/java/io/openbas/rest/scenario/response/SimulationsResultsLatest.java new file mode 100644 index 0000000000..b55bda8288 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/scenario/response/SimulationsResultsLatest.java @@ -0,0 +1,11 @@ +package io.openbas.rest.scenario.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openbas.expectation.ExpectationType; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +public record SimulationsResultsLatest( + @JsonProperty("global_scores_by_expectation_type") @NotNull + Map> globalScoresByExpectationType) {} diff --git a/openbas-api/src/main/java/io/openbas/rest/scenario/service/ScenarioStatisticService.java b/openbas-api/src/main/java/io/openbas/rest/scenario/service/ScenarioStatisticService.java new file mode 100644 index 0000000000..bf526627b5 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/scenario/service/ScenarioStatisticService.java @@ -0,0 +1,136 @@ +package io.openbas.rest.scenario.service; + +import io.openbas.database.raw.RawFinishedExerciseWithInjects; +import io.openbas.database.repository.ExerciseRepository; +import io.openbas.expectation.ExpectationType; +import io.openbas.rest.scenario.response.GlobalScoreBySimulationEndDate; +import io.openbas.rest.scenario.response.ScenarioStatistic; +import io.openbas.rest.scenario.response.SimulationsResultsLatest; +import io.openbas.utils.AtomicTestingUtils.ExpectationResultsByType; +import io.openbas.utils.AtomicTestingUtils.ResultDistribution; +import io.openbas.utils.ResultUtils; +import java.util.*; +import java.util.function.BinaryOperator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ScenarioStatisticService { + + private final ExerciseRepository exerciseRepository; + + private final ResultUtils resultUtils; + + public ScenarioStatistic getStatistics(String scenarioId) { + return getLatestSimulationsStatistics(scenarioId); + } + + private ScenarioStatistic getLatestSimulationsStatistics(String scenarioId) { + List rawFinishedExercises = + exerciseRepository.rawLatestFinishedExercisesWithInjectsByScenarioId(scenarioId); + + Map> + initialGlobalScoresByExpectationType = new HashMap<>(); + initialGlobalScoresByExpectationType.put(ExpectationType.PREVENTION, new ArrayList<>()); + initialGlobalScoresByExpectationType.put(ExpectationType.DETECTION, new ArrayList<>()); + initialGlobalScoresByExpectationType.put(ExpectationType.HUMAN_RESPONSE, new ArrayList<>()); + + Map> globalScoresByExpectationType = + rawFinishedExercises.stream() + .reduce( + initialGlobalScoresByExpectationType, + (scoresByType, rawFinishedExercise) -> + addGlobalScores( + scoresByType, + rawFinishedExercise, + resultUtils.getResultsByTypes(rawFinishedExercise.getInject_ids())), + getMapBinaryOperator()); + + return new ScenarioStatistic(new SimulationsResultsLatest(globalScoresByExpectationType)); + } + + private static Map> addGlobalScores( + Map> globalScoresByExpectationType, + RawFinishedExerciseWithInjects rawFinishedExercise, + List expectationResultsByType) { + + updateGlobalScores( + globalScoresByExpectationType, + rawFinishedExercise, + expectationResultsByType, + ExpectationType.PREVENTION); + + updateGlobalScores( + globalScoresByExpectationType, + rawFinishedExercise, + expectationResultsByType, + ExpectationType.DETECTION); + + updateGlobalScores( + globalScoresByExpectationType, + rawFinishedExercise, + expectationResultsByType, + ExpectationType.HUMAN_RESPONSE); + + return globalScoresByExpectationType; + } + + private static void updateGlobalScores( + Map> globalScoresByExpectationType, + RawFinishedExerciseWithInjects rawFinishedExercise, + List expectationResultsByType, + ExpectationType expectationType) { + List globalScores = + getGlobalScoresBySimulationEndDates( + rawFinishedExercise, expectationResultsByType, expectationType); + updateGlobalScoresByExpectationType( + globalScoresByExpectationType, globalScores, expectationType); + } + + private static void updateGlobalScoresByExpectationType( + Map> globalScoresByType, + List globalScores, + ExpectationType expectationType) { + List previousGlobalScores = + globalScoresByType.getOrDefault(expectationType, new ArrayList<>()); + previousGlobalScores.addAll(globalScores); + globalScoresByType.put(expectationType, previousGlobalScores); + } + + private static List getGlobalScoresBySimulationEndDates( + RawFinishedExerciseWithInjects rawFinishedExercise, + List expectationResultsByType, + ExpectationType expectationType) { + + return expectationResultsByType.stream() + .filter(expectationResultByType -> expectationResultByType.type() == expectationType) + .map( + expectationResultByType -> + new GlobalScoreBySimulationEndDate( + rawFinishedExercise.getExercise_end_date(), + getPercentageOfInjectsOnSuccess(expectationResultByType))) + .toList(); + } + + private static float getPercentageOfInjectsOnSuccess( + ExpectationResultsByType expectationResultByType) { + if (expectationResultByType.distribution().isEmpty()) { + return 0; + } + var totalNumberOfInjects = + expectationResultByType.distribution().stream() + .map(ResultDistribution::value) + .reduce(0, Integer::sum); + var numberOfInjectsOnSuccess = expectationResultByType.distribution().getFirst().value(); + return (float) numberOfInjectsOnSuccess / totalNumberOfInjects; + } + + private static BinaryOperator>> + getMapBinaryOperator() { + return (m1, m2) -> { + m1.putAll(m2); + return m1; + }; + } +} diff --git a/openbas-front/src/actions/scenarios/scenario-actions.ts b/openbas-front/src/actions/scenarios/scenario-actions.ts index e19b97ea36..2069c7d682 100644 --- a/openbas-front/src/actions/scenarios/scenario-actions.ts +++ b/openbas-front/src/actions/scenarios/scenario-actions.ts @@ -140,8 +140,8 @@ export const updateScenarioRecurrence = ( // -- STATISTIC -- -export const fetchScenarioStatistic = () => { - const uri = `${SCENARIO_URI}/statistics`; +export const fetchScenarioStatistic = (scenarioId: Scenario['scenario_id']) => { + const uri = `${SCENARIO_URI}/${scenarioId}/statistics`; return simpleCall(uri); }; diff --git a/openbas-front/src/admin/components/scenarios/ScenariosCard.tsx b/openbas-front/src/admin/components/scenarios/ScenariosCard.tsx deleted file mode 100644 index f8d212fcac..0000000000 --- a/openbas-front/src/admin/components/scenarios/ScenariosCard.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { Card, CardActionArea, CardContent } from '@mui/material'; -import { makeStyles } from '@mui/styles'; -import classNames from 'classnames'; -import { FunctionComponent, useEffect, useState } from 'react'; - -import { fetchScenarioStatistic } from '../../../actions/scenarios/scenario-actions'; -import { FilterHelpers } from '../../../components/common/queryable/filter/FilterHelpers'; -import { useFormatter } from '../../../components/i18n'; -import ItemCategory from '../../../components/ItemCategory'; -import type { Theme } from '../../../components/Theme'; -import type { ScenarioStatistic, SearchPaginationInput } from '../../../utils/api-types'; -import { scenarioCategories } from './constants'; - -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%', - }, -})); - -export const CATEGORY_FILTER_KEY = 'scenario_category'; - -interface ScenarioCardProps { - helpers: FilterHelpers; - searchPaginationInput: SearchPaginationInput; - category: string; - count: number; -} - -const ScenarioCard: FunctionComponent = ({ - helpers, - searchPaginationInput, - category, - count, -}) => { - // Standard hooks - const classes = useStyles(); - const { t } = useFormatter(); - - const handleOnClickCategory = () => { - helpers.handleAddSingleValueFilter( - CATEGORY_FILTER_KEY, - category, - ); - }; - - const hasCategory = searchPaginationInput.filterGroup?.filters?.find(f => f.key === CATEGORY_FILTER_KEY)?.values?.includes(category); - - return ( - - - -
- -
-
- {t(scenarioCategories.get(category) ?? category)} -
-
- {count} - {' '} - {t('scenarios')} -
-
-
-
- ); -}; - -interface ScenariosCardProps { - helpers: FilterHelpers; - searchPaginationInput: SearchPaginationInput; -} - -const ScenariosCard: FunctionComponent = ({ - helpers, - searchPaginationInput, -}) => { - // Standard hooks - const classes = useStyles(); - const { t } = useFormatter(); - - const handleOnClickCategory = () => { - helpers.handleAddMultipleValueFilter( - CATEGORY_FILTER_KEY, - [], - ); - }; - - const noCategory = () => { - const categoryFilter = searchPaginationInput.filterGroup?.filters?.find(f => f.key === CATEGORY_FILTER_KEY); - if (categoryFilter) { - return !categoryFilter.values || categoryFilter.values.length === 0; - } - return false; - }; - - // Statistic - const [statistic, setStatistic] = useState(); - const fetchStatistics = () => { - fetchScenarioStatistic().then((result: { data: ScenarioStatistic }) => setStatistic(result.data)); - }; - useEffect(() => { - fetchStatistics(); - }, []); - - return ( -
- - - -
- -
-
- {t('All categories')} -
-
- {statistic?.scenarios_global_count ?? '-'} - {' '} - {t('scenarios')} -
-
-
-
- {Object.entries(statistic?.scenarios_attack_scenario_count ?? {}).map(([key, value]) => ( - - ))} -
- ); -}; - -export default ScenariosCard; diff --git a/openbas-front/src/admin/components/scenarios/scenario/Scenario.tsx b/openbas-front/src/admin/components/scenarios/scenario/Scenario.tsx index 27f991f1a6..f1dec4091c 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/Scenario.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/Scenario.tsx @@ -9,7 +9,7 @@ import { Link, useParams } from 'react-router-dom'; import type { ExerciseStore } from '../../../../actions/exercises/Exercise'; import type { ExercisesHelper } from '../../../../actions/exercises/exercise-helper'; import type { ScenarioStore } from '../../../../actions/scenarios/Scenario'; -import { fetchScenarioExercises, searchScenarioExercises } from '../../../../actions/scenarios/scenario-actions'; +import { searchScenarioExercises } from '../../../../actions/scenarios/scenario-actions'; import type { ScenariosHelper } from '../../../../actions/scenarios/scenario-helper'; import { initSorting } from '../../../../components/common/queryable/Page'; import PaginationComponentV2 from '../../../../components/common/queryable/pagination/PaginationComponentV2'; @@ -21,15 +21,12 @@ import ItemCategory from '../../../../components/ItemCategory'; import ItemMainFocus from '../../../../components/ItemMainFocus'; import ItemSeverity from '../../../../components/ItemSeverity'; import ItemTags from '../../../../components/ItemTags'; -import Loader from '../../../../components/Loader'; import PlatformIcon from '../../../../components/PlatformIcon'; import type { Theme } from '../../../../components/Theme'; import octiDark from '../../../../static/images/xtm/octi_dark.png'; import octiLight from '../../../../static/images/xtm/octi_light.png'; import { useHelper } from '../../../../store'; import type { KillChainPhase, SearchPaginationInput } from '../../../../utils/api-types'; -import { useAppDispatch } from '../../../../utils/hooks'; -import useDataLoader from '../../../../utils/hooks/useDataLoader'; import { isEmptyField } from '../../../../utils/utils'; import type { EndpointStore } from '../../assets/endpoints/Endpoint'; import ExerciseList from '../../simulations/ExerciseList'; @@ -64,19 +61,12 @@ const Scenario = ({ setOpenInstantiateSimulationAndStart }: { setOpenInstantiate const classes = useStyles(); const theme = useTheme(); const { t } = useFormatter(); - const dispatch = useAppDispatch(); const { scenarioId } = useParams() as { scenarioId: ScenarioStore['scenario_id'] }; // Fetching data - const { scenario, exercises: exercisesFromStore } = useHelper((helper: ScenariosHelper & ExercisesHelper) => ({ + const { scenario } = useHelper((helper: ScenariosHelper & ExercisesHelper) => ({ scenario: helper.getScenario(scenarioId), - exercises: helper.getExercisesMap(), })); - const [loadingScenarioExercises, setLoadingScenarioExercises] = useState(true); - useDataLoader(() => { - setLoadingScenarioExercises(true); - dispatch(fetchScenarioExercises(scenarioId)).finally(() => setLoadingScenarioExercises(false)); - }); - const scenarioExercises = scenario.scenario_exercises?.map((exerciseId: string) => exercisesFromStore[exerciseId]).filter((ex: ExerciseStore) => !!ex); + const scenarioHasExercises = scenario.scenario_exercises?.length > 0; const sortByOrder = R.sortWith([R.ascend(R.prop('phase_order'))]); // Exercises @@ -219,11 +209,10 @@ const Scenario = ({ setOpenInstantiateSimulationAndStart }: { setOpenInstantiate {t('Simulations Results')} - {loadingScenarioExercises && ()} - {!loadingScenarioExercises && ()} + - {(scenarioExercises ?? 0).length > 0 && ( + {scenarioHasExercises && ( {t('Simulations')} @@ -248,7 +237,7 @@ const Scenario = ({ setOpenInstantiateSimulationAndStart }: { setOpenInstantiate )} - {(scenarioExercises ?? 0).length === 0 && !scenario.scenario_recurrence && ( + {!scenarioHasExercises && !scenario.scenario_recurrence && (
{t('This scenario has never run, schedule or run it now!')} @@ -265,7 +254,7 @@ const Scenario = ({ setOpenInstantiateSimulationAndStart }: { setOpenInstantiate
)} - {(scenarioExercises ?? 0).length === 0 && scenario.scenario_recurrence && ( + {!scenarioHasExercises && scenario.scenario_recurrence && (
{t('This scenario is scheduled to run, results will appear soon.')} diff --git a/openbas-front/src/admin/components/scenarios/scenario/ScenarioDistributionByExercise.tsx b/openbas-front/src/admin/components/scenarios/scenario/ScenarioDistributionByExercise.tsx index 0785854f63..4451e60a3e 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/ScenarioDistributionByExercise.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/ScenarioDistributionByExercise.tsx @@ -1,68 +1,90 @@ import { useTheme } from '@mui/styles'; -import { FunctionComponent } from 'react'; +import { FunctionComponent, useEffect, useState } from 'react'; import Chart from 'react-apexcharts'; -import type { ExerciseSimpleStore } from '../../../../actions/exercises/Exercise'; +import { fetchScenarioStatistic } from '../../../../actions/scenarios/scenario-actions'; import Empty from '../../../../components/Empty'; import { useFormatter } from '../../../../components/i18n'; +import Loader from '../../../../components/Loader'; import type { Theme } from '../../../../components/Theme'; +import { GlobalScoreBySimulationEndDate, ScenarioStatistic } from '../../../../utils/api-types'; import { verticalBarsChartOptions } from '../../../../utils/Charts'; interface Props { - exercises: ExerciseSimpleStore[]; + scenarioId: string; +} + +function generateFakeSeriesData(times: string[], percentage: number): GlobalScoreBySimulationEndDate[] { + return times.map(time => ({ + simulation_end_date: time, + global_score_success_percentage: percentage, + })); +} + +const generateFakeData = (): Record => { + const now = new Date(); + const times = Array.from({ length: 5 }, (_, i) => { + const newTime = new Date(now); + newTime.setHours(now.getHours() + i + 1); + return newTime.toISOString(); + }); + const prevention = { PREVENTION: generateFakeSeriesData(times, 0.69) }; + const detection = { DETECTION: generateFakeSeriesData(times, 0.84) }; + const humanResponse = { HUMAN_RESPONSE: generateFakeSeriesData(times, 0.46) }; + return ({ + ...prevention, + ...detection, + ...humanResponse, + }); +}; + +function generateSeriesData(globalScores: GlobalScoreBySimulationEndDate[]) { + return globalScores.map(globalScore => ({ + x: globalScore.simulation_end_date, + y: globalScore.global_score_success_percentage, + })); } const ScenarioDistributionByExercise: FunctionComponent = ({ - exercises = [], + scenarioId, }) => { // Standard hooks const { t, nsdt } = useFormatter(); const theme: Theme = useTheme(); - const generateFakeData = (): ExerciseSimpleStore[] => { - const now = new Date(); - return Array.from(Array(5), (e, i) => { - now.setHours(now.getHours() + 1); - return { - exercise_id: `fake-${i}`, - exercise_name: 'fake', - exercise_start_date: now.toISOString(), - exercise_global_score: [ - { type: 'PREVENTION', distribution: [{ id: 'PARTIAL_ID', value: 0.69, label: t('Unknown') }], avgResult: 'PARTIAL' }, - { type: 'DETECTION', distribution: [{ id: 'PARTIAL_ID', value: 0.84, label: t('Unknown') }], avgResult: 'PARTIAL' }, - { type: 'HUMAN_RESPONSE', distribution: [{ id: 'PARTIAL_ID', value: 0.46, label: t('Unknown') }], avgResult: 'PARTIAL' }, - ], - exercise_targets: [], - exercise_tags: undefined, - }; - }); + + const [loadingScenarioStatistics, setLoadingScenarioStatistics] = useState(true); + const [statistic, setStatistic] = useState(); + const fetchStatistics = () => { + setLoadingScenarioStatistics(true); + fetchScenarioStatistic(scenarioId).then((result: { data: ScenarioStatistic }) => setStatistic(result.data)).finally(() => setLoadingScenarioStatistics(false)); }; - const data = exercises.length > 0 ? exercises : generateFakeData(); + useEffect(() => { + fetchStatistics(); + }, []); + + const preventionData = statistic?.simulations_results_latest.global_scores_by_expectation_type['PREVENTION']; + const globalScoresByExpectationType = preventionData && preventionData.length > 0 ? statistic?.simulations_results_latest.global_scores_by_expectation_type : generateFakeData(); + const isStatisticsDataEmpty = preventionData && preventionData.length === 0; + const series = [ { name: t('Prevention'), - data: data.map(exercise => ({ - x: exercise.exercise_start_date ? new Date(exercise.exercise_start_date) : new Date(), - y: exercise.exercise_global_score?.filter(score => score.type === 'PREVENTION').at(0)?.distribution?.[0]?.value ?? 0, - })), + data: generateSeriesData(globalScoresByExpectationType['PREVENTION']), }, { name: t('Detection'), - data: data.map(exercise => ({ - x: exercise.exercise_start_date ? new Date(exercise.exercise_start_date) : new Date(), - y: exercise.exercise_global_score?.filter(score => score.type === 'DETECTION').at(0)?.distribution?.[0]?.value ?? 0, - })), + data: generateSeriesData(globalScoresByExpectationType['DETECTION']), }, { name: t('Human Response'), - data: data.map(exercise => ({ - x: exercise.exercise_start_date ? new Date(exercise.exercise_start_date) : new Date(), - y: exercise.exercise_global_score?.filter(score => score.type === 'HUMAN_RESPONSE').at(0)?.distribution?.[0]?.value ?? 0, - })), + data: generateSeriesData(globalScoresByExpectationType['HUMAN_RESPONSE']), }, ]; + return ( <> - {data.length > 0 ? ( + {loadingScenarioStatistics && ()} + {(!loadingScenarioStatistics && series[0].data.length > 0) && ( = ({ true, 'dataPoints', true, - exercises.length === 0, + isStatisticsDataEmpty, 1, t('No data to display'), )} @@ -83,7 +105,8 @@ const ScenarioDistributionByExercise: FunctionComponent = ({ width="100%" height={300} /> - ) : ( + )} + {(!loadingScenarioStatistics && series[0].data.length === 0) && ( ; result: string; /** @format double */ score?: number; @@ -1190,8 +1198,8 @@ export interface InjectExpectationSimple { export interface InjectExpectationUpdateInput { collector_id: string; is_success: boolean; + metadata?: Record; result: string; - success?: boolean; } export interface InjectImporter { @@ -2434,6 +2442,7 @@ export interface Payload { payload_type?: string; /** @format date-time */ payload_updated_at: string; + typeEnum?: "COMMAND" | "EXECUTABLE" | "FILE_DROP" | "DNS_RESOLUTION" | "NETWORK_TRAFFIC"; } export interface PayloadArgument { @@ -2892,7 +2901,7 @@ export interface ScenarioInput { scenario_description?: string; scenario_external_reference?: string; scenario_external_url?: string; - scenario_mail_from: string; + scenario_mail_from?: string; scenario_mails_reply_to?: string[]; scenario_main_focus?: string; scenario_message_footer?: string; @@ -2920,9 +2929,7 @@ export interface ScenarioSimple { } export interface ScenarioStatistic { - scenarios_attack_scenario_count?: Record; - /** @format int64 */ - scenarios_global_count?: number; + simulations_results_latest: SimulationsResultsLatest; } export interface ScenarioTeamPlayersEnableInput { @@ -3032,6 +3039,10 @@ export interface SettingsUpdateInput { platform_theme: string; } +export interface SimulationsResultsLatest { + global_scores_by_expectation_type: Record; +} + /** List of sort fields : a field is composed of a property (for instance "label" and an optional direction ("asc" is assumed if no direction is specified) : ("desc", "asc") */ export interface SortField { direction?: string; diff --git a/openbas-model/src/main/java/io/openbas/database/raw/RawFinishedExerciseWithInjects.java b/openbas-model/src/main/java/io/openbas/database/raw/RawFinishedExerciseWithInjects.java new file mode 100644 index 0000000000..2330325c20 --- /dev/null +++ b/openbas-model/src/main/java/io/openbas/database/raw/RawFinishedExerciseWithInjects.java @@ -0,0 +1,10 @@ +package io.openbas.database.raw; + +import java.time.Instant; +import java.util.Set; + +public interface RawFinishedExerciseWithInjects { + Instant getExercise_end_date(); + + Set getInject_ids(); +} diff --git a/openbas-model/src/main/java/io/openbas/database/repository/ExerciseRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/ExerciseRepository.java index b2be2d5836..e1dca85f8e 100644 --- a/openbas-model/src/main/java/io/openbas/database/repository/ExerciseRepository.java +++ b/openbas-model/src/main/java/io/openbas/database/repository/ExerciseRepository.java @@ -1,10 +1,7 @@ package io.openbas.database.repository; import io.openbas.database.model.Exercise; -import io.openbas.database.raw.RawExercise; -import io.openbas.database.raw.RawExerciseSimple; -import io.openbas.database.raw.RawGlobalInjectExpectation; -import io.openbas.database.raw.RawInjectExpectation; +import io.openbas.database.raw.*; import jakarta.validation.constraints.NotNull; import java.time.Instant; import java.util.List; @@ -313,4 +310,21 @@ Iterable rawGrantedInjectExpectationResultsFromDate( @Transactional void removeTeams( @Param("exerciseId") final String exerciseId, @Param("teamIds") final List teamIds); + + @Query( + value = + " SELECT ex.exercise_end_date, " + + " array_agg(distinct ie.inject_id) FILTER ( WHERE ie.inject_id IS NOT NULL ) as inject_ids " + + "FROM exercises ex " + + "LEFT JOIN scenarios_exercises s ON s.exercise_id = ex.exercise_id " + + "LEFT JOIN injects_expectations ie ON ex.exercise_id = ie.exercise_id " + + "WHERE s.scenario_id = :scenarioId " + + "AND ex.exercise_status = 'FINISHED' " + + "AND ex.exercise_end_date IS NOT NULL " + + "GROUP BY ex.exercise_id, ex.exercise_end_date " + + "ORDER BY ex.exercise_end_date DESC " + + "LIMIT 10 ;", + nativeQuery = true) + List rawLatestFinishedExercisesWithInjectsByScenarioId( + @Param("scenarioId") String scenarioId); }