diff --git a/openbas-api/src/main/java/io/openbas/migration/V3_31__Add_Injects_tests_statuses.java b/openbas-api/src/main/java/io/openbas/migration/V3_31__Add_Injects_tests_statuses.java new file mode 100644 index 0000000000..b81c35a23f --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/migration/V3_31__Add_Injects_tests_statuses.java @@ -0,0 +1,38 @@ +package io.openbas.migration; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; +import org.springframework.stereotype.Component; + +import java.sql.Connection; +import java.sql.Statement; + +@Component +public class V3_31__Add_Injects_tests_statuses extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + Connection connection = context.getConnection(); + Statement select = connection.createStatement(); + // Create table + select.execute(""" + CREATE TABLE injects_tests_statuses ( + status_id varchar(255) NOT NULL CONSTRAINT inject_test_status_pkey PRIMARY KEY, + status_name VARCHAR(255) NOT NULL, + status_executions text, + tracking_sent_date timestamp, + tracking_ack_date timestamp, + tracking_end_date timestamp, + tracking_total_execution_time bigint, + tracking_total_count int, + tracking_total_error int, + tracking_total_success int, + status_inject VARCHAR(255) NOT NULL CONSTRAINT inject_test_status_inject_id_fkey REFERENCES injects(inject_id) ON DELETE SET NULL, + status_created_at timestamp not null default now(), + status_updated_at timestamp not null default now() + ); + CREATE INDEX idx_inject_test_inject ON injects_tests_statuses(status_inject); + """); + } + +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java index 2b6f8aa9dc..1a68d2250c 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java @@ -21,6 +21,7 @@ import io.openbas.rest.inject.service.InjectDuplicateService; import io.openbas.service.AtomicTestingService; import io.openbas.service.InjectService; +import io.openbas.service.InjectTestStatusService; import io.openbas.service.ScenarioService; import io.openbas.utils.AtomicTestingMapper; import io.openbas.utils.pagination.SearchPaginationInput; @@ -29,8 +30,6 @@ import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -49,9 +48,6 @@ import java.util.stream.StreamSupport; import static io.openbas.config.SessionHelper.currentUser; - -import io.openbas.execution.Injector; - import static io.openbas.database.model.User.ROLE_ADMIN; import static io.openbas.database.specification.CommunicationSpecification.fromInject; import static io.openbas.helper.DatabaseHelper.resolveOptionalRelation; @@ -77,7 +73,6 @@ public class InjectApi extends RestBehavior { private final Executor executor; private final InjectorContractRepository injectorContractRepository; - private ApplicationContext context; private final CommunicationRepository communicationRepository; private final ExerciseRepository exerciseRepository; private final UserRepository userRepository; @@ -94,11 +89,6 @@ public class InjectApi extends RestBehavior { private final AtomicTestingService atomicTestingService; private final InjectDuplicateService injectDuplicateService; - @Autowired - public void setContext(ApplicationContext context) { - this.context = context; - } - // -- INJECTS -- @GetMapping(INJECT_URI + "/{injectId}") @@ -169,23 +159,6 @@ public Inject tryInject(@PathVariable String injectId) { return atomicTestingService.tryInject(injectId); } - @GetMapping(INJECT_URI + "/test/{injectId}") - public InjectStatus testInject(@PathVariable String injectId) { - Inject inject = injectRepository.findById(injectId).orElseThrow(); - User user = this.userRepository.findById(currentUser().getId()).orElseThrow(); - List userInjectContexts = List.of( - this.executionContextService.executionContext(user, inject, "Direct test") - ); - Injector executor = context.getBean( - inject.getInjectorContract().map(injectorContract -> injectorContract.getInjector().getType()).orElseThrow(), - io.openbas.execution.Injector.class); - ExecutableInject injection = new ExecutableInject(false, true, inject, List.of(), inject.getAssets(), - inject.getAssetGroups(), userInjectContexts); - Execution execution = executor.executeInjection(injection); - return InjectStatus.fromExecutionTest(execution); - - } - @Transactional(rollbackFor = Exception.class) @PutMapping(INJECT_URI + "/{exerciseId}/{injectId}") @PreAuthorize("isExercisePlanner(#exerciseId)") diff --git a/openbas-api/src/main/java/io/openbas/rest/inject_test_status/InjectTestStatusApi.java b/openbas-api/src/main/java/io/openbas/rest/inject_test_status/InjectTestStatusApi.java new file mode 100644 index 0000000000..023a56f998 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject_test_status/InjectTestStatusApi.java @@ -0,0 +1,41 @@ +package io.openbas.rest.inject_test_status; + +import io.openbas.database.model.InjectTestStatus; +import io.openbas.rest.helper.RestBehavior; +import io.openbas.service.InjectTestStatusService; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; +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; + +import java.util.List; + +@RestController +@PreAuthorize("isAdmin()") +@RequiredArgsConstructor +public class InjectTestStatusApi extends RestBehavior { + + private final InjectTestStatusService injectTestStatusService; + + @GetMapping("/api/injects/{injectId}/test") + public InjectTestStatus testInject(@PathVariable @NotBlank String injectId) { + return injectTestStatusService.testInject(injectId); + } + + @GetMapping("/api/exercise/{exerciseId}/injects/test") + public List findAllExerciseInjectTests(@PathVariable @NotBlank String exerciseId) { + return injectTestStatusService.findAllInjectTestsByExerciseId(exerciseId); + } + + @GetMapping("/api/scenario/{scenarioId}/injects/test") + public List findAllScenarioInjectTests(@PathVariable @NotBlank String scenarioId) { + return injectTestStatusService.findAllInjectTestsByScenarioId(scenarioId); + } + + @GetMapping("/api/injects/test/{testId}") + public InjectTestStatus findInjectTestStatus(@PathVariable @NotBlank String testId) { + return injectTestStatusService.findInjectTestStatusById(testId); + } +} diff --git a/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java b/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java new file mode 100644 index 0000000000..7d034558c8 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java @@ -0,0 +1,79 @@ +package io.openbas.service; + +import io.openbas.database.model.*; +import io.openbas.database.repository.InjectRepository; +import io.openbas.database.repository.InjectTestStatusRepository; +import io.openbas.database.repository.UserRepository; +import io.openbas.execution.ExecutableInject; +import io.openbas.execution.ExecutionContext; +import io.openbas.execution.ExecutionContextService; +import io.openbas.execution.Injector; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static io.openbas.config.SessionHelper.currentUser; + +@Service +@Log +@RequiredArgsConstructor +public class InjectTestStatusService { + + private ApplicationContext context; + private final UserRepository userRepository; + private final InjectRepository injectRepository; + private final ExecutionContextService executionContextService; + private final InjectTestStatusRepository injectTestStatusRepository; + + @Autowired + public void setContext(ApplicationContext context) { + this.context = context; + } + + @Transactional + public InjectTestStatus testInject(String injectId) { + Inject inject = injectRepository.findById(injectId).orElseThrow(); + User user = this.userRepository.findById(currentUser().getId()).orElseThrow(); + List userInjectContexts = List.of( + this.executionContextService.executionContext(user, inject, "Direct test") + ); + Injector executor = context.getBean( + inject.getInjectorContract().map(injectorContract -> injectorContract.getInjector().getType()).orElseThrow(), + io.openbas.execution.Injector.class); + ExecutableInject injection = new ExecutableInject(false, true, inject, List.of(), inject.getAssets(), + inject.getAssetGroups(), userInjectContexts); + Execution execution = executor.executeInjection(injection); + + //Save inject test status + Optional injectTestStatus = this.injectTestStatusRepository.findByInject(inject); + InjectTestStatus injectTestStatusToSave = InjectTestStatus.fromExecutionTest(execution); + injectTestStatus.ifPresent(testStatus -> { + injectTestStatusToSave.setId(testStatus.getId()); + injectTestStatusToSave.setTestCreationDate(testStatus.getTestCreationDate()); + }); + injectTestStatusToSave.setInject(inject); + this.injectTestStatusRepository.save(injectTestStatusToSave); + + return injectTestStatusToSave; + } + + public List findAllInjectTestsByExerciseId(String exerciseId) { + return injectTestStatusRepository.findAllExerciseInjectTests(exerciseId); + } + + public List findAllInjectTestsByScenarioId(String scenarioId) { + return injectTestStatusRepository.findAllScenarioInjectTests(scenarioId); + } + + public InjectTestStatus findInjectTestStatusById(String testId) { + return injectTestStatusRepository.findById(testId).orElseThrow(); + } + +} diff --git a/openbas-api/src/test/java/io/openbas/rest/TeamApiTest.java b/openbas-api/src/test/java/io/openbas/rest/TeamApiTest.java index 89bed077c7..311f3f1024 100644 --- a/openbas-api/src/test/java/io/openbas/rest/TeamApiTest.java +++ b/openbas-api/src/test/java/io/openbas/rest/TeamApiTest.java @@ -130,7 +130,7 @@ void retrieveTeamsOnScenarioTest() throws Exception { void addPlayerOnTeamOnScenarioTest() throws Exception { // -- PREPARE -- User user = new User(); - user.setEmail("test@gmail.com"); + user.setEmail("testfiligran@gmail.com"); user = this.userRepository.save(user); USER_ID = user.getId(); ScenarioTeamPlayersEnableInput input = new ScenarioTeamPlayersEnableInput(); diff --git a/openbas-front/src/actions/Inject.js b/openbas-front/src/actions/Inject.js index 91f733fa58..950a10f6a1 100644 --- a/openbas-front/src/actions/Inject.js +++ b/openbas-front/src/actions/Inject.js @@ -14,7 +14,7 @@ export const tryInject = (injectId) => (dispatch) => { }; export const testInject = (injectId) => { - const uri = `/api/injects/test/${injectId}`; + const uri = `/api/injects/${injectId}/test`; return simpleCall(uri); }; diff --git a/openbas-front/src/actions/inject_test/inject-test-actions.ts b/openbas-front/src/actions/inject_test/inject-test-actions.ts new file mode 100644 index 0000000000..c0a6b2b7f7 --- /dev/null +++ b/openbas-front/src/actions/inject_test/inject-test-actions.ts @@ -0,0 +1,17 @@ +import { simpleCall } from '../../utils/Action'; + +// eslint-disable-next-line import/prefer-default-export +export const searchExerciseInjectTests = (exerciseId: string) => { + const uri = `/api/exercise/${exerciseId}/injects/test`; + return simpleCall(uri); +}; + +export const searchScenarioInjectTests = (scenarioId: string) => { + const uri = `/api/scenario/${scenarioId}/injects/test`; + return simpleCall(uri); +}; + +export const fetchInjectTestStatus = (testId: string | undefined) => { + const uri = `/api/injects/test/${testId}`; + return simpleCall(uri); +}; diff --git a/openbas-front/src/admin/components/common/injects/InjectPopover.tsx b/openbas-front/src/admin/components/common/injects/InjectPopover.tsx index 1d73bc5066..c7a432a7a8 100644 --- a/openbas-front/src/admin/components/common/injects/InjectPopover.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectPopover.tsx @@ -1,5 +1,21 @@ -import React, { FunctionComponent, useContext, useState } from 'react'; -import { Alert, Button, Dialog, DialogActions, DialogContent, DialogContentText, IconButton, Menu, MenuItem, Table, TableBody, TableCell, TableRow } from '@mui/material'; +import React, { FunctionComponent, useContext, useEffect, useState } from 'react'; +import { + Alert, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + IconButton, + Menu, + MenuItem, + 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'; @@ -9,13 +25,16 @@ import type { Inject, InjectStatus, InjectStatusExecution, Tag } from '../../../ import { duplicateInjectForExercise, duplicateInjectForScenario, tryInject, testInject } from '../../../../actions/Inject'; import { useAppDispatch } from '../../../../utils/hooks'; import DialogDuplicate from '../../../../components/common/DialogDuplicate'; +import { useHelper } from '../../../../store'; +import type { ExercisesHelper } from '../../../../actions/exercises/exercise-helper'; interface Props { - inject: InjectStore & { inject_testable?: boolean }; // FIXME: Inject object coming from multiple endpoints with different properties + inject: InjectStore; tagsMap: Record; setSelectedInjectId: (injectId: Inject['inject_id']) => void; isDisabled: boolean; canBeTested?: boolean; + exerciseOrScenarioId?: string; } const InjectPopover: FunctionComponent = ({ @@ -23,6 +42,7 @@ const InjectPopover: FunctionComponent = ({ setSelectedInjectId, isDisabled, canBeTested = false, + exerciseOrScenarioId, }) => { // Standard hooks const { t } = useFormatter(); @@ -48,6 +68,8 @@ const InjectPopover: FunctionComponent = ({ const [_injectTestResult, setInjectTestResult] = useState(null); const [anchorEl, setAnchorEl] = useState(null); + const isExercise = useHelper((helper: ExercisesHelper) => helper.getExercisesMap()[exerciseOrScenarioId!] !== undefined); + const handlePopoverOpen = (event: React.MouseEvent) => { event.stopPropagation(); setAnchorEl(event.currentTarget); @@ -114,9 +136,36 @@ const InjectPopover: FunctionComponent = ({ setInjectTestResult(null); }; + const [openDialog, setOpenDialog] = React.useState(false); + const handleCloseDialog = ( + event?: React.SyntheticEvent | Event, + reason?: SnackbarCloseReason, + ) => { + if (reason === 'clickaway') { + return; + } + setOpenDialog(false); + }; + const [detailsLink, setDetailsLink] = React.useState(''); + + useEffect(() => { + if (openDialog) { + setTimeout(() => { + handleCloseDialog(); + setDetailsLink(''); + }, 6000); + } + }, [openDialog]); + const submitTest = () => { testInject(inject.inject_id).then((result: { data: InjectStatus }) => { setInjectTestResult(result.data); + setOpenDialog(true); + if (isExercise) { + setDetailsLink(`/admin/exercises/${exerciseOrScenarioId}/tests/${result.data.status_id}`); + } else { + setDetailsLink(`/admin/scenarios/${exerciseOrScenarioId}/tests/${result.data.status_id}`); + } }); handleCloseTest(); }; @@ -180,6 +229,31 @@ const InjectPopover: FunctionComponent = ({ return ( <> + + + {t('Inject test has been sent, you can view test logs details on ')} {t('its dedicated page.')} + + { ({ + paper: { + position: 'relative', + padding: 20, + overflow: 'hidden', + height: '100%', + }, + header: { + fontWeight: 'bold', + }, + listItem: { + marginBottom: 8, + }, + injectorContract: { + margin: '20px 0 20px 15px', + width: '100%', + border: `1px solid ${theme.palette.divider}`, + borderRadius: 4, + }, + injectorContractHeader: { + backgroundColor: theme.palette.background.default, + }, + injectorContractContent: { + fontSize: 18, + textAlign: 'center', + }, +})); + +interface Props { + open: boolean; + handleClose: () => void; + test: InjectTestStatus | undefined; +} + +const InjectTestDetail: FunctionComponent = ({ + open, + handleClose, + test, +}) => { + const classes = useStyles(); + const { t } = useFormatter(); + + return ( + + + + + {test + ? ( + } + + /> + ) : ( + + {t('No data available')} + + )} + + {truncate(test?.inject_title, 80)} + + + + {t('Execution logs')} + {test ? ( + + + {t('Status')} + + {test.status_name + && + } + + {t('Traces')} + +
+                {test.tracking_sent_date ? (
+                  <>
+                    
+                      {t('Tracking Sent Date')}: {test.tracking_sent_date}
+                    
+                    
+                      {t('Tracking Ack Date')}: {test.tracking_ack_date}
+                    
+                    
+                      {t('Tracking End Date')}: {test.tracking_end_date}
+                    
+                    
+                      {t('Tracking Total Execution')}
+                      {t('Time')}: {test.tracking_total_execution_time} {t('ms')}
+                    
+                    
+                      {t('Tracking Total Count')}: {test.tracking_total_count}
+                    
+                    
+                      {t('Tracking Total Error')}: {test.tracking_total_error}
+                    
+                    
+                      {t('Tracking Total Success')}: {test.tracking_total_success}
+                    
+                  
+                ) : (
+                  
+                    {t('No data available')}
+                  
+                )}
+                {(test.status_traces?.length ?? 0) > 0 && (
+                  <>
+                    
+                      {t('Traces')}:
+                    
+                    
    + {test.status_traces?.map((trace, index) => ( +
  • + {`${trace.execution_status} ${trace.execution_message}`} +
  • + ))} +
+ + )} +
+
+ ) : ( + + {t('No data available')} + + )} +
+
+
+ + ); +}; + +export default InjectTestDetail; diff --git a/openbas-front/src/admin/components/injects/InjectTestList.tsx b/openbas-front/src/admin/components/injects/InjectTestList.tsx new file mode 100644 index 0000000000..be4814fd0f --- /dev/null +++ b/openbas-front/src/admin/components/injects/InjectTestList.tsx @@ -0,0 +1,190 @@ +import { makeStyles } from '@mui/styles'; +import React, { CSSProperties, FunctionComponent, useEffect, useState } from 'react'; +import { List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; +import type { InjectTestStatus, SearchPaginationInput } from '../../../utils/api-types'; +import { useFormatter } from '../../../components/i18n'; +import ItemStatus from '../../../components/ItemStatus'; +import { initSorting } from '../../../components/common/pagination/Page'; +import SortHeadersComponent from '../../../components/common/pagination/SortHeadersComponent'; +import InjectIcon from '../common/injects/InjectIcon'; +import { isNotEmptyField } from '../../../utils/utils'; +import Empty from '../../../components/Empty'; +import InjectTestDetail from './InjectTestDetail'; + +const useStyles = makeStyles(() => ({ + bodyItems: { + display: 'flex', + alignItems: 'center', + }, + bodyItem: { + height: 20, + fontSize: 13, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingRight: 10, + }, + itemHead: { + paddingLeft: 10, + marginBottom: 10, + textTransform: 'uppercase', + cursor: 'pointer', + }, + item: { + paddingLeft: 10, + height: 50, + }, +})); + +const inlineStyles: Record = { + inject_title: { + width: '40%', + cursor: 'default', + }, + tracking_sent_date: { + width: '40%', + }, + status_name: { + width: '20%', + }, +}; + +interface Props { + searchInjectTests: (exerciseOrScenarioId: string) => Promise<{ data: InjectTestStatus[] }>; + searchInjectTest: (testId: string) => Promise<{ data: InjectTestStatus }>; + exerciseOrScenarioId: string; + statusId: string | undefined; +} + +const InjectTestList: FunctionComponent = ({ + searchInjectTests, + searchInjectTest, + exerciseOrScenarioId, + statusId, +}) => { + // Standard hooks + const classes = useStyles(); + const { t, fldt } = useFormatter(); + + const [selectedTest, setSelectedTest] = useState(null); + + // Fetching test + useEffect(() => { + if (statusId !== null && statusId !== undefined) { + searchInjectTest(statusId).then((result: { data: InjectTestStatus }) => { + setSelectedTest(result.data); + }); + } + }, [statusId]); + + // Headers + const headers = [ + { + field: 'inject_title', + label: 'Inject Title', + isSortable: true, + value: (test: InjectTestStatus) => test.inject_title, + }, + { + field: 'tracking_sent_date', + label: 'Test execution time', + isSortable: true, + value: (test: InjectTestStatus) => fldt(test.tracking_sent_date), + }, + { + field: 'status_name', + label: 'Status', + isSortable: true, + value: (test: InjectTestStatus) => { + return (); + }, + }, + ]; + + // Filter and sort hook + const [tests, setTests] = useState([]); + const [searchPaginationInput, setSearchPaginationInput] = useState({ + sorts: initSorting('inject_title'), + }); + + // Fetch tests list + useEffect(() => { + searchInjectTests(exerciseOrScenarioId).then((result: { data: InjectTestStatus[] }) => { + setTests(result.data); + }); + }, []); + + return ( + <> + + + + + } + /> + + {tests?.map((test) => { + return ( + + setSelectedTest(test)} + > + + + + + {headers.map((header) => ( +
+ {header.value(test)} +
+ ))} + + } + /> +
+
+ ); + })} + {!tests ? () : null} +
+ { + selectedTest !== null + && setSelectedTest(null)} test={selectedTest} /> + } + + ); +}; + +export default InjectTestList; diff --git a/openbas-front/src/admin/components/scenarios/scenario/Index.tsx b/openbas-front/src/admin/components/scenarios/scenario/Index.tsx index 1bdb8e249e..9559d7235a 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/Index.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/Index.tsx @@ -25,6 +25,7 @@ import injectContextForScenario from './ScenarioContext'; const Scenario = lazy(() => import('./Scenario')); const ScenarioDefinition = lazy(() => import('./ScenarioDefinition')); const Injects = lazy(() => import('./injects/ScenarioInjects')); +const Tests = lazy(() => import('./tests/ScenarioTests')); // eslint-disable-next-line no-underscore-dangle const _MS_PER_DAY = 1000 * 60 * 60 * 24; @@ -134,6 +135,12 @@ const IndexScenarioComponent: FunctionComponent<{ scenario: ScenarioStore }> = ( value={`/admin/scenarios/${scenario.scenario_id}/injects`} label={t('Injects')} /> +
{!cronExpression && ( @@ -162,6 +169,7 @@ const IndexScenarioComponent: FunctionComponent<{ scenario: ScenarioStore }> = ( + {/* Not found */} } /> diff --git a/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx b/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx index 7848642269..a99616b9c9 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx @@ -174,6 +174,7 @@ const ScenarioInjects: FunctionComponent = () => { { + const { scenarioId, statusId } = useParams() as { scenarioId: ScenarioStore['scenario_id'], statusId: InjectTestStatus['status_id'] }; + + return ( + + ); +}; + +export default ScenarioTests; diff --git a/openbas-front/src/admin/components/simulations/simulation/Index.tsx b/openbas-front/src/admin/components/simulations/simulation/Index.tsx index f3f69096e5..d61cc20e49 100644 --- a/openbas-front/src/admin/components/simulations/simulation/Index.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/Index.tsx @@ -25,6 +25,7 @@ const Comcheck = lazy(() => import('./controls/Comcheck')); const Lessons = lazy(() => import('./lessons/Lessons')); const ExerciseDefinition = lazy(() => import('./ExerciseDefinition')); const Injects = lazy(() => import('./injects/ExerciseInjects')); +const Tests = lazy(() => import('./tests/ExerciseTests')); const TimelineOverview = lazy(() => import('./timeline/TimelineOverview')); const Mails = lazy(() => import('./mails/Mails')); const MailsInject = lazy(() => import('./mails/Inject')); @@ -64,6 +65,8 @@ const IndexComponent: FunctionComponent<{ exercise: ExerciseType }> = ({ tabValue = `/admin/exercises/${exercise.exercise_id}/animation`; } else if (location.pathname.includes(`/admin/exercises/${exercise.exercise_id}/results`)) { tabValue = `/admin/exercises/${exercise.exercise_id}/results`; + } else if (location.pathname.includes(`/admin/exercises/${exercise.exercise_id}/tests`)) { + tabValue = `/admin/exercises/${exercise.exercise_id}/tests`; } return ( @@ -101,6 +104,12 @@ const IndexComponent: FunctionComponent<{ exercise: ExerciseType }> = ({ value={`/admin/exercises/${exercise.exercise_id}/injects`} label={t('Injects')} /> + = ({ + } /> diff --git a/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx b/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx index 8d0e8bedcd..61169ee589 100644 --- a/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx @@ -207,6 +207,7 @@ const ExerciseInjects: FunctionComponent = () => { = () => { selected={false} aria-label="List view mode" > - + @@ -263,7 +264,7 @@ const ExerciseInjects: FunctionComponent = () => { selected={true} aria-label="Distribution view mode" > - + @@ -274,7 +275,7 @@ const ExerciseInjects: FunctionComponent = () => { {t('Distribution of injects by type')} - + @@ -282,7 +283,7 @@ const ExerciseInjects: FunctionComponent = () => { {t('Distribution of injects by team')} - + @@ -290,7 +291,7 @@ const ExerciseInjects: FunctionComponent = () => { {t('Distribution of expectations by inject type')} (%) - + @@ -298,7 +299,7 @@ const ExerciseInjects: FunctionComponent = () => { {t('Distribution of expected total score by inject type')} - + @@ -306,7 +307,7 @@ const ExerciseInjects: FunctionComponent = () => { {t('Distribution of expectations by team')} - + @@ -314,7 +315,7 @@ const ExerciseInjects: FunctionComponent = () => { {t('Distribution of expected total score by team')} - + diff --git a/openbas-front/src/admin/components/simulations/simulation/tests/ExerciseTests.tsx b/openbas-front/src/admin/components/simulations/simulation/tests/ExerciseTests.tsx new file mode 100644 index 0000000000..5412c77e9b --- /dev/null +++ b/openbas-front/src/admin/components/simulations/simulation/tests/ExerciseTests.tsx @@ -0,0 +1,15 @@ +import React, { FunctionComponent } from 'react'; +import { useParams } from 'react-router-dom'; +import type { Exercise, InjectTestStatus } from '../../../../../utils/api-types'; +import { fetchInjectTestStatus, searchExerciseInjectTests } from '../../../../../actions/inject_test/inject-test-actions'; +import InjectTestList from '../../../injects/InjectTestList'; + +const ExerciseTests: FunctionComponent = () => { + const { exerciseId, statusId } = useParams() as { exerciseId: Exercise['exercise_id'], statusId: InjectTestStatus['status_id'] }; + + return ( + + ); +}; + +export default ExerciseTests; diff --git a/openbas-front/src/utils/Localization.js b/openbas-front/src/utils/Localization.js index df1f567a23..5323a56fe9 100644 --- a/openbas-front/src/utils/Localization.js +++ b/openbas-front/src/utils/Localization.js @@ -619,6 +619,7 @@ const i18n = { 'Chaque équipe doit uploader un document', 'Each team should submit a text response': 'Chaque équipe doit soumettre une réponse texte', + 'Inject tests': 'Tests de stimuli', // -- Expectation start -- 'This expectation is handled automatically by the platform and triggered when target reads the articles': 'Cet attendu est géré automatiquement par la plateforme et déclenché lorsque une cible lit les articles', 'This expectation is handled automatically by the platform and triggered when the target completes the challenges': 'Cette attente est gérée automatiquement par la plateforme et est déclenchée lorsque la cible termine les défis', @@ -1241,6 +1242,9 @@ const i18n = { // Platform Banner 'IMAP service is not responding, your injectors may be impacted.': 'Le service IMAP ne réponds pas, vos injecteurs peuvent être impactés.', 'Executor Caldera is not responding, your exercises may be impacted.': 'L\'exécuteur Caldera ne réponds pas, vos exercises peuvent être impactés.', + // Inject test + 'Inject test has been sent, you can view test logs details on ': 'Le test de l\'inject a été envoyé, vous pouvez visualiser les logs de test sur ', + 'its dedicated page.': 'sa page dédiée', }, zh: { 'Email address': 'email地址', diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index de9caca2c7..3d58d3e12b 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -1095,6 +1095,8 @@ export interface Inject { /** @uniqueItems true */ inject_tags?: Tag[]; inject_teams?: Team[]; + inject_test_status?: InjectTestStatus; + inject_testable?: boolean; inject_title: string; inject_type?: string; /** @format date-time */ @@ -1250,6 +1252,7 @@ export interface InjectOutput { /** @uniqueItems true */ inject_tags?: string[]; inject_teams?: string[]; + inject_testable?: boolean; inject_title?: string; inject_type?: string; } @@ -1348,13 +1351,52 @@ export interface InjectTargetWithResult { id: string; name?: string; platformType?: "Linux" | "Windows" | "MacOS" | "Container" | "Service" | "Generic" | "Internal" | "Unknown"; - targetType?: "ASSETS" | "ASSETS_GROUPS" | "PLAYER" | "TEAMS"; + targetType?: "ASSETS" | "ASSETS_GROUPS" | "TEAMS"; } export interface InjectTeamsInput { inject_teams?: string[]; } +export interface InjectTestStatus { + /** @format date-time */ + inject_test_status_created_at?: string; + /** @format date-time */ + inject_test_status_updated_at?: string; + inject_title?: string; + inject_type?: string; + injector_contract?: InjectorContract; + listened?: boolean; + status_id?: string; + status_name: + | "DRAFT" + | "INFO" + | "QUEUING" + | "EXECUTING" + | "PENDING" + | "PARTIAL" + | "ERROR" + | "MAYBE_PARTIAL_PREVENTED" + | "MAYBE_PREVENTED" + | "SUCCESS"; + status_traces?: InjectStatusExecution[]; + /** @format date-time */ + tracking_ack_date?: string; + /** @format date-time */ + tracking_end_date?: string; + /** @format date-time */ + tracking_sent_date?: string; + /** @format int32 */ + tracking_total_count?: number; + /** @format int32 */ + tracking_total_error?: number; + /** @format int64 */ + tracking_total_execution_time?: number; + /** @format int32 */ + tracking_total_success?: number; + updateAttributes?: object; +} + export interface InjectUpdateActivationInput { inject_enabled?: boolean; } 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 new file mode 100644 index 0000000000..0d9f90d3f1 --- /dev/null +++ b/openbas-model/src/main/java/io/openbas/database/model/BaseInjectStatus.java @@ -0,0 +1,105 @@ +package io.openbas.database.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openbas.database.converter.InjectStatusExecutionConverter; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.UuidGenerator; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Setter +@Getter +@MappedSuperclass +public abstract class BaseInjectStatus implements Base { + + + @Id + @Column(name = "status_id") + @GeneratedValue(generator = "UUID") + @UuidGenerator + @JsonProperty("status_id") + private String id; + + @Column(name = "status_name") + @JsonProperty("status_name") + @Enumerated(EnumType.STRING) + @NotNull + private ExecutionStatus name; + + // region dates tracking + @Column(name = "status_executions") + @Convert(converter = InjectStatusExecutionConverter.class) + @JsonProperty("status_traces") + private List traces = new ArrayList<>(); + + @Column(name = "tracking_sent_date") + @JsonProperty("tracking_sent_date") + private Instant trackingSentDate; // To Queue / processing engine + + @Column(name = "tracking_ack_date") + @JsonProperty("tracking_ack_date") + private Instant trackingAckDate; // Ack from remote injector + + @Column(name = "tracking_end_date") + @JsonProperty("tracking_end_date") + private Instant trackingEndDate; // Done task from injector + + @Column(name = "tracking_total_execution_time") + @JsonProperty("tracking_total_execution_time") + private Long trackingTotalExecutionTime; + // endregion + + // region count + @Column(name = "tracking_total_count") + @JsonProperty("tracking_total_count") + private Integer trackingTotalCount; + + @Column(name = "tracking_total_error") + @JsonProperty("tracking_total_error") + private Integer trackingTotalError; + + @Column(name = "tracking_total_success") + @JsonProperty("tracking_total_success") + private Integer trackingTotalSuccess; + // endregion + + @OneToOne + @JoinColumn(name = "status_inject") + @JsonIgnore + protected Inject inject; + + // region transient + public List statusIdentifiers() { + return this.getTraces().stream().flatMap(ex -> ex.getIdentifiers().stream()).toList(); + } + + @Override + public boolean isUserHasAccess(User user) { + return this.inject.isUserHasAccess(user); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !Base.class.isAssignableFrom(o.getClass())) { + return false; + } + Base base = (Base) o; + return id.equals(base.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + +} 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 d97653434a..bf623a6147 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 @@ -32,532 +32,552 @@ @EntityListeners(ModelBaseListener.class) @Log public class Inject implements Base, Injection { - public static final int SPEED_STANDARD = 1; // Standard speed define by the user. - public static final Comparator executionComparator = (o1, o2) -> { - if (o1.getDate().isPresent() && o2.getDate().isPresent()) { - return o1.getDate().get().compareTo(o2.getDate().get()); - } - return o1.getId().compareTo(o2.getId()); - }; - - @Getter - @Id - @Column(name = "inject_id") - @GeneratedValue(generator = "UUID") - @UuidGenerator - @JsonProperty("inject_id") - @NotBlank - private String id; - - @Getter - @Queryable(searchable = true, filterable = true, sortable = true) - @Column(name = "inject_title") - @JsonProperty("inject_title") - @NotBlank - private String title; - - @Getter - @Column(name = "inject_description") - @JsonProperty("inject_description") - private String description; - - @Getter - @Column(name = "inject_country") - @JsonProperty("inject_country") - private String country; - - @Getter - @Column(name = "inject_city") - @JsonProperty("inject_city") - private String city; - - @Getter - @Column(name = "inject_enabled") - @JsonProperty("inject_enabled") - private boolean enabled = true; - - @Getter - @Column(name = "inject_content") - @Convert(converter = ContentConverter.class) - @JsonProperty("inject_content") - private ObjectNode content; - - @Getter - @Column(name = "inject_created_at") - @JsonProperty("inject_created_at") - @NotNull - private Instant createdAt = now(); - - @Getter - @Column(name = "inject_updated_at") - @Queryable(sortable = true) - @JsonProperty("inject_updated_at") - @NotNull - private Instant updatedAt = now(); - - @Getter - @Column(name = "inject_all_teams") - @JsonProperty("inject_all_teams") - private boolean allTeams; - - @Getter - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "inject_exercise") - @JsonSerialize(using = MonoIdDeserializer.class) - @JsonProperty("inject_exercise") - private Exercise exercise; - - @Getter - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "inject_scenario") - @JsonSerialize(using = MonoIdDeserializer.class) - @JsonProperty("inject_scenario") - private Scenario scenario; - - @Getter - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "inject_depends_from_another") - @JsonSerialize(using = MonoIdDeserializer.class) - @JsonProperty("inject_depends_on") - private Inject dependsOn; - - @Getter - @Column(name = "inject_depends_duration") - @JsonProperty("inject_depends_duration") - @NotNull - @Min(value = 0L, message = "The value must be positive") - private Long dependsDuration; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "inject_injector_contract") - @JsonProperty("inject_injector_contract") - private InjectorContract injectorContract; - - @Getter - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "inject_user") - @JsonSerialize(using = MonoIdDeserializer.class) - @JsonProperty("inject_user") - private User user; - - // 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") - private InjectStatus status; - - @Getter - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "injects_tags", - joinColumns = @JoinColumn(name = "inject_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id")) - @JsonSerialize(using = MultiIdSetDeserializer.class) - @JsonProperty("inject_tags") - private Set tags = new HashSet<>(); - - @Getter - @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name = "injects_teams", - joinColumns = @JoinColumn(name = "inject_id"), - inverseJoinColumns = @JoinColumn(name = "team_id")) - @JsonSerialize(using = MultiIdListDeserializer.class) - @JsonProperty("inject_teams") - private List teams = new ArrayList<>(); - - @Getter - @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name = "injects_assets", - joinColumns = @JoinColumn(name = "inject_id"), - inverseJoinColumns = @JoinColumn(name = "asset_id")) - @JsonSerialize(using = MultiIdListDeserializer.class) - @JsonProperty("inject_assets") - private List assets = new ArrayList<>(); - - @Getter - @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name = "injects_asset_groups", - joinColumns = @JoinColumn(name = "inject_id"), - inverseJoinColumns = @JoinColumn(name = "asset_group_id")) - @JsonSerialize(using = MultiIdListDeserializer.class) - @JsonProperty("inject_asset_groups") - private List assetGroups = new ArrayList<>(); - - @Getter - @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name = "injects_payloads", - joinColumns = @JoinColumn(name = "inject_id"), - inverseJoinColumns = @JoinColumn(name = "payload_id")) - @JsonSerialize(using = MultiIdListDeserializer.class) - @JsonProperty("inject_payloads") - private List payloads = new ArrayList<>(); - - // CascadeType.ALL is required here because of complex relationships - @Getter - @OneToMany(mappedBy = "inject", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) - @JsonProperty("inject_documents") - @JsonSerialize(using = MultiModelDeserializer.class) - private List documents = new ArrayList<>(); - - // CascadeType.ALL is required here because communications are embedded - @Getter - @OneToMany(mappedBy = "inject", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) - @JsonProperty("inject_communications") - @JsonSerialize(using = MultiModelDeserializer.class) - private List communications = new ArrayList<>(); - - // CascadeType.ALL is required here because expectations are embedded - @Getter - @OneToMany(mappedBy = "inject", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) - @JsonProperty("inject_expectations") - @JsonSerialize(using = MultiModelDeserializer.class) - private List expectations = new ArrayList<>(); - - @Getter - @Setter - @Transient - private boolean isListened = true; - - // region transient - @Transient - public String getHeader() { - return ofNullable(this.getExercise()).map(Exercise::getHeader).orElse(""); - } - - @Transient - public String getFooter() { - return ofNullable(this.getExercise()).map(Exercise::getFooter).orElse(""); - } - - @JsonIgnore - @Override - public boolean isUserHasAccess(User user) { - return this.getExercise().isUserHasAccess(user); - } - - @JsonIgnore - public void clean() { - this.status = null; - this.communications.clear(); - this.expectations.clear(); - } - - @JsonProperty("inject_users_number") - public long getNumberOfTargetUsers() { - if (this.getExercise() == null) { - return 0L; - } - if (this.isAllTeams()) { - return this.getExercise().usersNumber(); - } - return getTeams().stream() - .map(team -> team.getUsersNumberInExercise(getExercise().getId())) - .reduce(Long::sum).orElse(0L); - } - - @JsonProperty("inject_ready") - public boolean isReady() { - return InjectModelHelper.isReady( - getInjectorContract().orElse(null), - getContent(), - isAllTeams(), - getTeams().stream().map(Team::getId).collect(Collectors.toList()), - getAssets().stream().map(Asset::getId).collect(Collectors.toList()), - getAssetGroups().stream().map(AssetGroup::getId).collect(Collectors.toList()) - ); - } - - @JsonIgnore - public Instant computeInjectDate(Instant source, int speed) { - return InjectModelHelper.computeInjectDate(source, speed, getDependsOn(), getDependsDuration(), getExercise()); - } - - @JsonProperty("inject_date") - public Optional getDate() { - return InjectModelHelper.getDate(getExercise(), getScenario(), getDependsOn(), getDependsDuration()); - } + public static final int SPEED_STANDARD = 1; // Standard speed define by the user. - @JsonIgnore - public Inject getInject() { - return this; + public static final Comparator executionComparator = (o1, o2) -> { + if (o1.getDate().isPresent() && o2.getDate().isPresent()) { + return o1.getDate().get().compareTo(o2.getDate().get()); } - - @JsonIgnore - public boolean isNotExecuted() { - return this.getStatus().isEmpty(); + return o1.getId().compareTo(o2.getId()); + }; + + @Getter + @Id + @Column(name = "inject_id") + @GeneratedValue(generator = "UUID") + @UuidGenerator + @JsonProperty("inject_id") + @NotBlank + private String id; + + @Getter + @Queryable(searchable = true, filterable = true, sortable = true) + @Column(name = "inject_title") + @JsonProperty("inject_title") + @NotBlank + private String title; + + @Getter + @Column(name = "inject_description") + @JsonProperty("inject_description") + private String description; + + @Getter + @Column(name = "inject_country") + @JsonProperty("inject_country") + private String country; + + @Getter + @Column(name = "inject_city") + @JsonProperty("inject_city") + private String city; + + @Getter + @Column(name = "inject_enabled") + @JsonProperty("inject_enabled") + private boolean enabled = true; + + @Getter + @Column(name = "inject_content") + @Convert(converter = ContentConverter.class) + @JsonProperty("inject_content") + private ObjectNode content; + + @Getter + @Column(name = "inject_created_at") + @JsonProperty("inject_created_at") + @NotNull + private Instant createdAt = now(); + + @Getter + @Column(name = "inject_updated_at") + @Queryable(sortable = true) + @JsonProperty("inject_updated_at") + @NotNull + private Instant updatedAt = now(); + + @Getter + @Column(name = "inject_all_teams") + @JsonProperty("inject_all_teams") + private boolean allTeams; + + @Getter + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "inject_exercise") + @JsonSerialize(using = MonoIdDeserializer.class) + @JsonProperty("inject_exercise") + private Exercise exercise; + + @Getter + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "inject_scenario") + @JsonSerialize(using = MonoIdDeserializer.class) + @JsonProperty("inject_scenario") + private Scenario scenario; + + @Getter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "inject_depends_from_another") + @JsonSerialize(using = MonoIdDeserializer.class) + @JsonProperty("inject_depends_on") + private Inject dependsOn; + + @Getter + @Column(name = "inject_depends_duration") + @JsonProperty("inject_depends_duration") + @NotNull + @Min(value = 0L, message = "The value must be positive") + private Long dependsDuration; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "inject_injector_contract") + @JsonProperty("inject_injector_contract") + private InjectorContract injectorContract; + + @Getter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "inject_user") + @JsonSerialize(using = MonoIdDeserializer.class) + @JsonProperty("inject_user") + private User user; + + // 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") + private InjectStatus status; + + // Status after testing emails and sms + @OneToOne(mappedBy = "inject", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("inject_test_status") + @Queryable(sortable = true, property = "name") + private InjectTestStatus testStatus; + + @Getter + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "injects_tags", + joinColumns = @JoinColumn(name = "inject_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id")) + @JsonSerialize(using = MultiIdSetDeserializer.class) + @JsonProperty("inject_tags") + private Set tags = new HashSet<>(); + + @Getter + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "injects_teams", + joinColumns = @JoinColumn(name = "inject_id"), + inverseJoinColumns = @JoinColumn(name = "team_id")) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("inject_teams") + private List teams = new ArrayList<>(); + + @Getter + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "injects_assets", + joinColumns = @JoinColumn(name = "inject_id"), + inverseJoinColumns = @JoinColumn(name = "asset_id")) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("inject_assets") + private List assets = new ArrayList<>(); + + @Getter + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "injects_asset_groups", + joinColumns = @JoinColumn(name = "inject_id"), + inverseJoinColumns = @JoinColumn(name = "asset_group_id")) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("inject_asset_groups") + private List assetGroups = new ArrayList<>(); + + @Getter + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "injects_payloads", + joinColumns = @JoinColumn(name = "inject_id"), + inverseJoinColumns = @JoinColumn(name = "payload_id")) + @JsonSerialize(using = MultiIdListDeserializer.class) + @JsonProperty("inject_payloads") + private List payloads = new ArrayList<>(); + + // CascadeType.ALL is required here because of complex relationships + @Getter + @OneToMany(mappedBy = "inject", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("inject_documents") + @JsonSerialize(using = MultiModelDeserializer.class) + private List documents = new ArrayList<>(); + + // CascadeType.ALL is required here because communications are embedded + @Getter + @OneToMany(mappedBy = "inject", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("inject_communications") + @JsonSerialize(using = MultiModelDeserializer.class) + private List communications = new ArrayList<>(); + + // CascadeType.ALL is required here because expectations are embedded + @Getter + @OneToMany(mappedBy = "inject", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("inject_expectations") + @JsonSerialize(using = MultiModelDeserializer.class) + private List expectations = new ArrayList<>(); + + @Getter + @Setter + @Transient + private boolean isListened = true; + + // region transient + @Transient + public String getHeader() { + return ofNullable(this.getExercise()).map(Exercise::getHeader).orElse(""); + } + + @Transient + public String getFooter() { + return ofNullable(this.getExercise()).map(Exercise::getFooter).orElse(""); + } + + @JsonIgnore + @Override + public boolean isUserHasAccess(User user) { + return this.getExercise().isUserHasAccess(user); + } + + @JsonIgnore + public void clean() { + this.status = null; + this.communications.clear(); + this.expectations.clear(); + } + + @JsonProperty("inject_users_number") + public long getNumberOfTargetUsers() { + if (this.getExercise() == null) { + return 0L; } - - @JsonIgnore - public boolean isPastInject() { - return this.getDate().map(date -> date.isBefore(now())).orElse(false); - } - - @JsonIgnore - public boolean isFutureInject() { - return this.getDate().map(date -> date.isAfter(now())).orElse(false); - } - // endregion - - public Optional getInjectorContract() { - return Optional.ofNullable(this.injectorContract); - } - - public Optional getStatus() { - return ofNullable(this.status); + if (this.isAllTeams()) { + return this.getExercise().usersNumber(); } - - public List getUserExpectationsForArticle(User user, Article article) { - return this.expectations.stream() - .filter(execution -> execution.getType().equals(InjectExpectation.EXPECTATION_TYPE.ARTICLE)) - .filter(execution -> execution.getArticle().equals(article)) - .filter(execution -> execution.getUser() != null) //We include only the expectations from players, because the validation link is always from a player + return getTeams().stream() + .map(team -> team.getUsersNumberInExercise(getExercise().getId())) + .reduce(Long::sum).orElse(0L); + } + + @JsonProperty("inject_ready") + public boolean isReady() { + return InjectModelHelper.isReady( + getInjectorContract().orElse(null), + getContent(), + isAllTeams(), + getTeams().stream().map(Team::getId).collect(Collectors.toList()), + getAssets().stream().map(Asset::getId).collect(Collectors.toList()), + getAssetGroups().stream().map(AssetGroup::getId).collect(Collectors.toList()) + ); + } + + @JsonIgnore + public Instant computeInjectDate(Instant source, int speed) { + return InjectModelHelper.computeInjectDate(source, speed, getDependsOn(), getDependsDuration(), getExercise()); + } + + @JsonProperty("inject_date") + public Optional getDate() { + return InjectModelHelper.getDate(getExercise(), getScenario(), getDependsOn(), getDependsDuration()); + } + + @JsonIgnore + public Inject getInject() { + return this; + } + + @JsonIgnore + public boolean isNotExecuted() { + return this.getStatus().isEmpty(); + } + + @JsonIgnore + public boolean isPastInject() { + return this.getDate().map(date -> date.isBefore(now())).orElse(false); + } + + @JsonIgnore + public boolean isFutureInject() { + return this.getDate().map(date -> date.isAfter(now())).orElse(false); + } + // endregion + + public Optional getInjectorContract() { + return Optional.ofNullable(this.injectorContract); + } + + public Optional getStatus() { + return ofNullable(this.status); + } + + public List getUserExpectationsForArticle(User user, Article article) { + return this.expectations.stream() + .filter(execution -> execution.getType().equals(InjectExpectation.EXPECTATION_TYPE.ARTICLE)) + .filter(execution -> execution.getArticle().equals(article)) + .filter(execution -> execution.getUser() != null) //We include only the expectations from players, because the validation link is always from a player .filter(execution -> execution.getUser().equals(user)) - .toList(); + .toList(); + } + + @JsonIgnore + public DryInject toDryInject(Dryrun run) { + DryInject dryInject = new DryInject(); + dryInject.setRun(run); + dryInject.setInject(this); + dryInject.setDate(computeInjectDate(run.getDate(), run.getSpeed())); + return dryInject; + } + + @JsonProperty("inject_communications_number") + public long getCommunicationsNumber() { + return this.getCommunications().size(); + } + + @JsonProperty("inject_communications_not_ack_number") + public long getCommunicationsNotAckNumber() { + return this.getCommunications().stream().filter(communication -> !communication.getAck()).count(); + } + + @JsonProperty("inject_sent_at") + public Instant getSentAt() { + return InjectModelHelper.getSentAt(this.getStatus()); + } + + @JsonProperty("inject_kill_chain_phases") + public List getKillChainPhases() { + return getInjectorContract() + .map(injectorContract -> + injectorContract.getAttackPatterns().stream() + .flatMap(attackPattern -> attackPattern.getKillChainPhases().stream()) + .distinct() + .collect(Collectors.toList()) + ) + .orElseGet(ArrayList::new); + } + + @JsonProperty("inject_attack_patterns") + public List getAttackPatterns() { + return getInjectorContract() + .map(InjectorContract::getAttackPatterns) + .orElseGet(ArrayList::new); + } + + @JsonProperty("inject_type") + private String getType() { + return getInjectorContract() + .map(InjectorContract::getInjector) + .map(Injector::getType) + .orElse(null); + } + + @JsonIgnore + public boolean isAtomicTesting() { + return this.exercise == null && this.scenario == null; + } + + private static final Set VALID_TYPES = new HashSet<>(); + + static { + VALID_TYPES.add("openbas_email"); + VALID_TYPES.add("openbas_ovh_sms"); + } + + @JsonProperty("inject_testable") + public boolean getInjectTestable() { + return VALID_TYPES.contains(this.getType()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; } - - @JsonIgnore - public DryInject toDryInject(Dryrun run) { - DryInject dryInject = new DryInject(); - dryInject.setRun(run); - dryInject.setInject(this); - dryInject.setDate(computeInjectDate(run.getDate(), run.getSpeed())); - return dryInject; + if (o == null || !Base.class.isAssignableFrom(o.getClass())) { + return false; } - - @JsonProperty("inject_communications_number") - public long getCommunicationsNumber() { - return this.getCommunications().size(); - } - - @JsonProperty("inject_communications_not_ack_number") - public long getCommunicationsNotAckNumber() { - return this.getCommunications().stream().filter(communication -> !communication.getAck()).count(); - } - - @JsonProperty("inject_sent_at") - public Instant getSentAt() { - return InjectModelHelper.getSentAt(this.getStatus()); - } - - @JsonProperty("inject_kill_chain_phases") - public List getKillChainPhases() { - return getInjectorContract() - .map(injectorContract -> - injectorContract.getAttackPatterns().stream() - .flatMap(attackPattern -> attackPattern.getKillChainPhases().stream()) - .distinct() - .collect(Collectors.toList()) - ) - .orElseGet(ArrayList::new); - } - - @JsonProperty("inject_attack_patterns") - public List getAttackPatterns() { - return getInjectorContract() - .map(InjectorContract::getAttackPatterns) - .orElseGet(ArrayList::new); - } - - @JsonProperty("inject_type") - private String getType() { - return getInjectorContract() - .map(InjectorContract::getInjector) - .map(Injector::getType) - .orElse(null); - } - - @JsonIgnore - public boolean isAtomicTesting() { - return this.exercise == null && this.scenario == null; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || !Base.class.isAssignableFrom(o.getClass())) { - return false; - } - Base base = (Base) o; - return id.equals(base.getId()); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - - /** - * Creates an Inject from a Raw Inject - * - * @param rawInject the raw inject to convert - * @param rawTeams the map of the teams containing at least the ones linked to this inject - * @param rawInjectExpectationMap the map of the expectations containing at least the ones linked to this inject - * @param mapOfAssetGroups the map of the asset groups containing at least the ones linked to this inject - * @param mapOfAsset the map of the asset containing at least the ones linked to this inject and the asset groups linked to it - * @return an Inject - */ - public static Inject fromRawInject(RawInject rawInject, - Map rawTeams, - Map rawInjectExpectationMap, - Map mapOfAssetGroups, - Map mapOfAsset) { - // Create the object - Inject inject = new Inject(); - inject.setId(rawInject.getInject_id()); - - // Set a list of expectations - inject.setExpectations(new ArrayList<>()); - for (String expectationId : rawInject.getInject_expectations()) { - RawInjectExpectation rawInjectExpectation = rawInjectExpectationMap.get(expectationId); - if (rawInjectExpectation != null) { - // Create a new expectation - InjectExpectation expectation = new InjectExpectation(); - expectation.setId(rawInjectExpectation.getInject_expectation_id()); - expectation.setType( - InjectExpectation.EXPECTATION_TYPE.valueOf(rawInjectExpectation.getInject_expectation_type())); - expectation.setScore(rawInjectExpectation.getInject_expectation_score()); - expectation.setExpectedScore(rawInjectExpectation.getInject_expectation_expected_score()); + Base base = (Base) o; + return id.equals(base.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + /** + * Creates an Inject from a Raw Inject + * + * @param rawInject the raw inject to convert + * @param rawTeams the map of the teams containing at least the ones linked to this inject + * @param rawInjectExpectationMap the map of the expectations containing at least the ones linked to this inject + * @param mapOfAssetGroups the map of the asset groups containing at least the ones linked to this inject + * @param mapOfAsset the map of the asset containing at least the ones linked to this inject and the + * asset groups linked to it + * @return an Inject + */ + public static Inject fromRawInject(RawInject rawInject, + Map rawTeams, + Map rawInjectExpectationMap, + Map mapOfAssetGroups, + Map mapOfAsset) { + // Create the object + Inject inject = new Inject(); + inject.setId(rawInject.getInject_id()); + + // Set a list of expectations + inject.setExpectations(new ArrayList<>()); + for (String expectationId : rawInject.getInject_expectations()) { + RawInjectExpectation rawInjectExpectation = rawInjectExpectationMap.get(expectationId); + if (rawInjectExpectation != null) { + // Create a new expectation + InjectExpectation expectation = new InjectExpectation(); + expectation.setId(rawInjectExpectation.getInject_expectation_id()); + expectation.setType( + InjectExpectation.EXPECTATION_TYPE.valueOf(rawInjectExpectation.getInject_expectation_type())); + expectation.setScore(rawInjectExpectation.getInject_expectation_score()); + expectation.setExpectedScore(rawInjectExpectation.getInject_expectation_expected_score()); expectation.setExpectationGroup(rawInjectExpectation.getInject_expectation_group()); - // Add the team of the expectation - if (rawInjectExpectation.getTeam_id() != null) { - RawTeam rawTeam = rawTeams.get(rawInjectExpectation.getTeam_id()); - if (rawTeam != null) { - Team team = new Team(); - team.setId(rawInjectExpectation.getTeam_id()); - team.setName(rawTeam.getTeam_name()); - expectation.setTeam(team); - } - } - - // Add the asset group of the expectation - if (rawInjectExpectation.getAsset_group_id() != null) { - RawAssetGroup rawAssetGroup = mapOfAssetGroups.get(rawInjectExpectation.getAsset_group_id()); - if (rawAssetGroup != null) { - AssetGroup assetGroup = new AssetGroup(); - assetGroup.setId(rawAssetGroup.getAsset_group_id()); - assetGroup.setName(rawAssetGroup.getAsset_group_name()); - assetGroup.setAssets(new ArrayList<>()); - - // We add the assets to the asset group - for (String assetId : rawAssetGroup.getAsset_ids()) { - RawAsset rawAsset = mapOfAsset.get(assetId); - if (rawAsset != null) { - if (rawAsset.getAsset_type().equals(ENDPOINT_TYPE)) { - Endpoint endpoint = new Endpoint(rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name(), - Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); - assetGroup.getAssets().add(endpoint); - } else { - Asset asset = new Asset(rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name()); - assetGroup.getAssets().add(asset); - } - } - } - expectation.setAssetGroup(assetGroup); - } - } - - // We add the asset to the expectation - if (rawInjectExpectation.getAsset_id() != null) { - RawAsset rawAsset = mapOfAsset.get(rawInjectExpectation.getAsset_id()); - if (rawAsset != null) { - if (rawAsset.getAsset_type().equals(ENDPOINT_TYPE)) { - Endpoint endpoint = new Endpoint(rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name(), - Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); - expectation.setAsset(endpoint); - } else { - Asset asset = new Asset( - rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name() - ); - expectation.setAsset(asset); - } - } - } - inject.getExpectations().add(expectation); - } - } - - // We add the teams to the inject - ArrayList injectTeams = new ArrayList(); - for (String injectTeamId : rawInject.getInject_teams()) { + // Add the team of the expectation + if (rawInjectExpectation.getTeam_id() != null) { + RawTeam rawTeam = rawTeams.get(rawInjectExpectation.getTeam_id()); + if (rawTeam != null) { Team team = new Team(); - team.setId(rawTeams.get(injectTeamId).getTeam_id()); - team.setName(rawTeams.get(injectTeamId).getTeam_name()); - injectTeams.add(team); + team.setId(rawInjectExpectation.getTeam_id()); + team.setName(rawTeam.getTeam_name()); + expectation.setTeam(team); + } } - inject.setTeams(injectTeams); - - // We add the assets to the inject - ArrayList injectAssets = new ArrayList(); - for (String injectAssetId : rawInject.getInject_assets()) { - RawAsset rawAsset = mapOfAsset.get(injectAssetId); - if (rawAsset == null) { - continue; // Skip to the next iteration + // Add the asset group of the expectation + if (rawInjectExpectation.getAsset_group_id() != null) { + RawAssetGroup rawAssetGroup = mapOfAssetGroups.get(rawInjectExpectation.getAsset_group_id()); + if (rawAssetGroup != null) { + AssetGroup assetGroup = new AssetGroup(); + assetGroup.setId(rawAssetGroup.getAsset_group_id()); + assetGroup.setName(rawAssetGroup.getAsset_group_name()); + assetGroup.setAssets(new ArrayList<>()); + + // We add the assets to the asset group + for (String assetId : rawAssetGroup.getAsset_ids()) { + RawAsset rawAsset = mapOfAsset.get(assetId); + if (rawAsset != null) { + if (rawAsset.getAsset_type().equals(ENDPOINT_TYPE)) { + Endpoint endpoint = new Endpoint(rawAsset.getAsset_id(), + rawAsset.getAsset_type(), + rawAsset.getAsset_name(), + Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); + assetGroup.getAssets().add(endpoint); + } else { + Asset asset = new Asset(rawAsset.getAsset_id(), + rawAsset.getAsset_type(), + rawAsset.getAsset_name()); + assetGroup.getAssets().add(asset); + } + } } + expectation.setAssetGroup(assetGroup); + } + } - if ("Endpoint".equals(rawAsset.getAsset_type())) { - Endpoint endpoint = new Endpoint( - rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name(), - Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform()) - ); - injectAssets.add(endpoint); + // We add the asset to the expectation + if (rawInjectExpectation.getAsset_id() != null) { + RawAsset rawAsset = mapOfAsset.get(rawInjectExpectation.getAsset_id()); + if (rawAsset != null) { + if (rawAsset.getAsset_type().equals(ENDPOINT_TYPE)) { + Endpoint endpoint = new Endpoint(rawAsset.getAsset_id(), + rawAsset.getAsset_type(), + rawAsset.getAsset_name(), + Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); + expectation.setAsset(endpoint); } else { - Asset newAsset = new Asset( - rawAsset.getAsset_id(), - rawAsset.getAsset_type(), - rawAsset.getAsset_name() - ); - injectAssets.add(newAsset); + Asset asset = new Asset( + rawAsset.getAsset_id(), + rawAsset.getAsset_type(), + rawAsset.getAsset_name() + ); + expectation.setAsset(asset); } + } } - inject.setAssets(injectAssets); - - // Add the asset groups to the inject - ArrayList injectAssetGroups = new ArrayList(); - for (String injectAssetGroupId : rawInject.getInject_asset_groups()) { - Optional rawAssetGroup = Optional.ofNullable(mapOfAssetGroups.get(injectAssetGroupId)); - rawAssetGroup.ifPresent(rag -> { - AssetGroup assetGroup = new AssetGroup(); - assetGroup.setName(rag.getAsset_group_name()); - assetGroup.setId(rag.getAsset_group_id()); - - // We add the assets linked to the asset group - assetGroup.setAssets(rag.getAsset_ids().stream() - .map(assetId -> { - RawAsset rawAsset = mapOfAsset.get(assetId); - if (rawAsset == null) { - return null; - } - - if ("Endpoint".equals(rawAsset.getAsset_type())) { - return new Endpoint(rawAsset.getAsset_id(), rawAsset.getAsset_type(), rawAsset.getAsset_name(), - Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); - } else { - return new Asset(rawAsset.getAsset_id(), rawAsset.getAsset_type(), rawAsset.getAsset_name()); - } - }) - .filter(Objects::nonNull) - .toList() - ); - injectAssetGroups.add(assetGroup); - }); - } - - inject.setAssetGroups(injectAssetGroups); + inject.getExpectations().add(expectation); + } + } - return inject; + // We add the teams to the inject + ArrayList injectTeams = new ArrayList(); + for (String injectTeamId : rawInject.getInject_teams()) { + Team team = new Team(); + team.setId(rawTeams.get(injectTeamId).getTeam_id()); + team.setName(rawTeams.get(injectTeamId).getTeam_name()); + injectTeams.add(team); + } + inject.setTeams(injectTeams); + + // We add the assets to the inject + ArrayList injectAssets = new ArrayList(); + for (String injectAssetId : rawInject.getInject_assets()) { + RawAsset rawAsset = mapOfAsset.get(injectAssetId); + + if (rawAsset == null) { + continue; // Skip to the next iteration + } + + if ("Endpoint".equals(rawAsset.getAsset_type())) { + Endpoint endpoint = new Endpoint( + rawAsset.getAsset_id(), + rawAsset.getAsset_type(), + rawAsset.getAsset_name(), + Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform()) + ); + injectAssets.add(endpoint); + } else { + Asset newAsset = new Asset( + rawAsset.getAsset_id(), + rawAsset.getAsset_type(), + rawAsset.getAsset_name() + ); + injectAssets.add(newAsset); + } } + inject.setAssets(injectAssets); + + // Add the asset groups to the inject + ArrayList injectAssetGroups = new ArrayList(); + for (String injectAssetGroupId : rawInject.getInject_asset_groups()) { + Optional rawAssetGroup = Optional.ofNullable(mapOfAssetGroups.get(injectAssetGroupId)); + rawAssetGroup.ifPresent(rag -> { + AssetGroup assetGroup = new AssetGroup(); + assetGroup.setName(rag.getAsset_group_name()); + assetGroup.setId(rag.getAsset_group_id()); + + // We add the assets linked to the asset group + assetGroup.setAssets(rag.getAsset_ids().stream() + .map(assetId -> { + RawAsset rawAsset = mapOfAsset.get(assetId); + if (rawAsset == null) { + return null; + } + + if ("Endpoint".equals(rawAsset.getAsset_type())) { + return new Endpoint(rawAsset.getAsset_id(), rawAsset.getAsset_type(), rawAsset.getAsset_name(), + Endpoint.PLATFORM_TYPE.valueOf(rawAsset.getEndpoint_platform())); + } else { + return new Asset(rawAsset.getAsset_id(), rawAsset.getAsset_type(), rawAsset.getAsset_name()); + } + }) + .filter(Objects::nonNull) + .toList() + ); + injectAssetGroups.add(assetGroup); + }); + } + + inject.setAssetGroups(injectAssetGroups); + + return inject; + } } \ No newline at end of file diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectStatus.java b/openbas-model/src/main/java/io/openbas/database/model/InjectStatus.java index 93cba8003c..4fad7da3dc 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectStatus.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectStatus.java @@ -18,81 +18,12 @@ @Getter @Entity @Table(name = "injects_statuses") -public class InjectStatus implements Base { - - @Id - @Column(name = "status_id") - @GeneratedValue(generator = "UUID") - @UuidGenerator - @JsonProperty("status_id") - private String id; - - @Column(name = "status_name") - @JsonProperty("status_name") - @Enumerated(EnumType.STRING) - private ExecutionStatus name; - - // region dates tracking - @Column(name = "status_executions") - @Convert(converter = InjectStatusExecutionConverter.class) - @JsonProperty("status_traces") - private List traces = new ArrayList<>(); - - @Column(name = "tracking_sent_date") - @JsonProperty("tracking_sent_date") - private Instant trackingSentDate; // To Queue / processing engine - - @Column(name = "tracking_ack_date") - @JsonProperty("tracking_ack_date") - private Instant trackingAckDate; // Ack from remote injector - - @Column(name = "tracking_end_date") - @JsonProperty("tracking_end_date") - private Instant trackingEndDate; // Done task from injector - - @Column(name = "tracking_total_execution_time") - @JsonProperty("tracking_total_execution_time") - private Long trackingTotalExecutionTime; - // endregion - - // region count - @Column(name = "tracking_total_count") - @JsonProperty("tracking_total_count") - private Integer trackingTotalCount; - - @Column(name = "tracking_total_error") - @JsonProperty("tracking_total_error") - private Integer trackingTotalError; - - @Column(name = "tracking_total_success") - @JsonProperty("tracking_total_success") - private Integer trackingTotalSuccess; - // endregion - - @OneToOne - @JoinColumn(name = "status_inject") - @JsonIgnore - private Inject inject; - - // region transient - public List statusIdentifiers() { - return this.getTraces().stream().flatMap(ex -> ex.getIdentifiers().stream()).toList(); - } - // endregion +public class InjectStatus extends BaseInjectStatus { public static InjectStatus fromExecution(Execution execution, Inject executedInject) { InjectStatus injectStatus = executedInject.getStatus().orElse(new InjectStatus()); - injectStatus.setInject(executedInject); - return fromExecution(execution, injectStatus); - } - - public static InjectStatus fromExecutionTest(Execution execution) { - InjectStatus injectStatus = new InjectStatus(); - return fromExecution(execution, injectStatus); - } - - private static InjectStatus fromExecution(Execution execution, InjectStatus injectStatus) { injectStatus.setTrackingSentDate(Instant.now()); + injectStatus.setInject(executedInject); injectStatus.getTraces().addAll(execution.getTraces()); int numberOfElements = execution.getTraces().size(); int numberOfError = (int) execution.getTraces().stream().filter(ex -> ex.getStatus().equals(ExecutionStatus.ERROR)) @@ -109,32 +40,9 @@ private static InjectStatus fromExecution(Execution execution, InjectStatus inje injectStatus.setTrackingEndDate(Instant.now()); injectStatus.setTrackingTotalExecutionTime( Duration.between(injectStatus.getTrackingSentDate(), injectStatus.getTrackingEndDate()).getSeconds()); - return injectStatus; } - @Override - public boolean isUserHasAccess(User user) { - return this.inject.isUserHasAccess(user); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || !Base.class.isAssignableFrom(o.getClass())) { - return false; - } - Base base = (Base) o; - return id.equals(base.getId()); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - // -- UTILS -- public static InjectStatus draftInjectStatus() { diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectTestStatus.java b/openbas-model/src/main/java/io/openbas/database/model/InjectTestStatus.java new file mode 100644 index 0000000000..080d7873f1 --- /dev/null +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectTestStatus.java @@ -0,0 +1,78 @@ +package io.openbas.database.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openbas.database.converter.InjectStatusExecutionConverter; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.annotations.UuidGenerator; + +import java.time.Instant; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Setter +@Getter +@Entity +@Table(name = "injects_tests_statuses") +public class InjectTestStatus extends BaseInjectStatus implements Base { + + @JsonProperty("inject_title") + public String getInjectTitle() { + return inject.getTitle(); + } + + @JsonProperty("injector_contract") + public Optional getInjectContract() { + return inject.getInjectorContract(); + } + + @JsonProperty("inject_type") + private String getType() { + return inject.getInjectorContract() + .map(InjectorContract::getInjector) + .map(Injector::getType) + .orElse(null); + } + + @CreationTimestamp + @Column(name = "status_created_at") + @JsonProperty("inject_test_status_created_at") + private Instant testCreationDate; + + @UpdateTimestamp + @Column(name = "status_updated_at") + @JsonProperty("inject_test_status_updated_at") + private Instant testUpdateDate; + + public static InjectTestStatus fromExecutionTest(Execution execution) { + InjectTestStatus injectTestStatus = new InjectTestStatus(); + injectTestStatus.setTrackingSentDate(Instant.now()); + injectTestStatus.getTraces().addAll(execution.getTraces()); + int numberOfElements = execution.getTraces().size(); + int numberOfError = (int) execution.getTraces().stream().filter(ex -> ex.getStatus().equals(ExecutionStatus.ERROR)) + .count(); + int numberOfSuccess = (int) execution.getTraces().stream() + .filter(ex -> ex.getStatus().equals(ExecutionStatus.SUCCESS)).count(); + injectTestStatus.setTrackingTotalError(numberOfError); + injectTestStatus.setTrackingTotalSuccess(numberOfSuccess); + injectTestStatus.setTrackingTotalCount( + execution.getExpectedCount() != null ? execution.getExpectedCount() : numberOfElements); + ExecutionStatus globalStatus = numberOfSuccess > 0 ? ExecutionStatus.SUCCESS : ExecutionStatus.ERROR; + ExecutionStatus finalStatus = numberOfError > 0 && numberOfSuccess > 0 ? ExecutionStatus.PARTIAL : globalStatus; + injectTestStatus.setName(execution.isAsync() ? ExecutionStatus.PENDING : finalStatus); + injectTestStatus.setTrackingEndDate(Instant.now()); + injectTestStatus.setTrackingTotalExecutionTime( + Duration.between(injectTestStatus.getTrackingSentDate(), injectTestStatus.getTrackingEndDate()).getSeconds()); + return injectTestStatus; + } + + +} diff --git a/openbas-model/src/main/java/io/openbas/database/repository/InjectTestStatusRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/InjectTestStatusRepository.java new file mode 100644 index 0000000000..54e9d36258 --- /dev/null +++ b/openbas-model/src/main/java/io/openbas/database/repository/InjectTestStatusRepository.java @@ -0,0 +1,27 @@ +package io.openbas.database.repository; + +import io.openbas.database.model.Inject; +import io.openbas.database.model.InjectTestStatus; +import jakarta.validation.constraints.NotNull; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface InjectTestStatusRepository extends CrudRepository, + JpaSpecificationExecutor { + + @NotNull + Optional findById(@NotNull String id); + + Optional findByInject(@NotNull Inject inject); + + @Query(value = "select its.* from injects_tests_statuses its inner join injects i on its.status_inject = i.inject_id where i.inject_exercise = :exerciseId", nativeQuery = true) + List findAllExerciseInjectTests(@Param("exerciseId") String exerciseId); + + @Query(value = "select its.* from injects_tests_statuses its inner join injects i on its.status_inject = i.inject_id where i.inject_scenario = :scenarioId", nativeQuery = true) + List findAllScenarioInjectTests(@Param("scenarioId") String scenarioId); +}