From 8d4b644649c543f42c8a71225f69b4454f9a4217 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Mon, 5 Aug 2024 10:46:22 +0200 Subject: [PATCH 01/13] [backend/frontend] Fix on player not properly displayed (#1312) --- openbas-front/src/utils/api-types.d.ts | 4 ++++ .../java/io/openbas/database/raw/RawPaginationPlayer.java | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 01aff87e0a..1673a75c4e 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -2513,11 +2513,15 @@ export interface RawPaginationImportMapper { } export interface RawPaginationPlayer { + user_phone2?: string; + user_country?: string; user_email?: string; user_firstname?: string; user_id?: string; user_lastname?: string; user_organization?: string; + user_pgp_key?: string; + user_phone?: string; user_tags?: string[]; } diff --git a/openbas-model/src/main/java/io/openbas/database/raw/RawPaginationPlayer.java b/openbas-model/src/main/java/io/openbas/database/raw/RawPaginationPlayer.java index bbd3598269..c4867bbc8a 100644 --- a/openbas-model/src/main/java/io/openbas/database/raw/RawPaginationPlayer.java +++ b/openbas-model/src/main/java/io/openbas/database/raw/RawPaginationPlayer.java @@ -17,6 +17,10 @@ public class RawPaginationPlayer { String user_firstname; String user_lastname; String user_organization; + String user_phone; + String user_phone2; + String user_country; + String user_pgp_key; List user_tags; public RawPaginationPlayer(final User user) { @@ -24,6 +28,10 @@ public RawPaginationPlayer(final User user) { this.user_email = user.getEmail(); this.user_firstname = user.getFirstname(); this.user_lastname = user.getLastname(); + this.user_phone = user.getPhone(); + this.user_phone2 = user.getPhone2(); + this.user_country = user.getCountry(); + this.user_pgp_key = user.getPgpKey(); this.user_organization = ofNullable(user.getOrganization()).map(Organization::getId).orElse(null); this.user_tags = user.getTags().stream().map(Tag::getId).toList(); } From b99d91883806b4b24b01958a2991cb5a8a69a10a Mon Sep 17 00:00:00 2001 From: Johanah LEKEU <49673066+johanah29@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:27:07 +0200 Subject: [PATCH 02/13] [Frontend]Drawer effect is not correct when opening an inject from the list (#1138) --- .../common/injects/UpdateInject.tsx | 24 ++++---- .../common/injects/UpdateInjectDetails.js | 57 ++++++++++--------- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/openbas-front/src/admin/components/common/injects/UpdateInject.tsx b/openbas-front/src/admin/components/common/injects/UpdateInject.tsx index 468d46d291..c9b100bf95 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInject.tsx +++ b/openbas-front/src/admin/components/common/injects/UpdateInject.tsx @@ -47,19 +47,17 @@ const UpdateInject: React.FC = ({ open, handleClose, onUpdateInject, inje }} disableEnforceFocus > - {inject && injectorContract && ( - - )} + ); }; diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js index 282e13e96e..fac7aa68d4 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js @@ -223,7 +223,7 @@ const UpdateInjectDetails = ({ const duration = splitDuration(inject?.inject_depends_duration || 0); const initialValues = { ...inject, - ...inject.inject_content, + ...inject?.inject_content, inject_tags: tagOptions(inject?.inject_tags, tagsMap), inject_depends_duration_days: duration.days, inject_depends_duration_hours: duration.hours, @@ -240,8 +240,8 @@ const UpdateInjectDetails = ({ 'attachments', 'expectations', ]; - if (isEmptyField(inject.inject_content)) { - contractContent.fields + if (isEmptyField(inject?.inject_content)) { + contractContent?.fields .filter((f) => !builtInFields.includes(f.key)) .forEach((field) => { if (!initialValues[field.key]) { @@ -254,7 +254,7 @@ const UpdateInjectDetails = ({ }); } // Specific processing for some fields - contractContent.fields + contractContent?.fields .filter((f) => !builtInFields.includes(f.key)) .forEach((field) => { if ( @@ -318,10 +318,11 @@ const UpdateInjectDetails = ({ : } - title={contractContent.contract_attack_patterns_external_ids.join(', ')} + avatar={contractContent ? + : } + title={contractContent?.contract_attack_patterns_external_ids.join(', ')} action={
- {inject.inject_injector_contract?.injector_contract_platforms?.map( + {inject?.inject_injector_contract?.injector_contract_platforms?.map( (platform) => , )}
} @@ -349,27 +350,27 @@ const UpdateInjectDetails = ({ {contractContent && (
{openDetails && ( - + )}
{openDetails ? : } From 19532e495d42bd5ffacbe5f03f431d0dc6e563fb Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 7 Aug 2024 11:54:12 +0200 Subject: [PATCH 03/13] [frontend] Fix on email adresses (#1314) --- .../admin/components/common/simulate/EmailParametersForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openbas-front/src/admin/components/common/simulate/EmailParametersForm.tsx b/openbas-front/src/admin/components/common/simulate/EmailParametersForm.tsx index e4ffaad13b..74b0fbb507 100644 --- a/openbas-front/src/admin/components/common/simulate/EmailParametersForm.tsx +++ b/openbas-front/src/admin/components/common/simulate/EmailParametersForm.tsx @@ -103,7 +103,7 @@ const EmailParametersForm: React.FC = ({ value={field.value} onChange={() => { if (undefined !== field.value && inputValue !== '' && !field.value.includes(inputValue)) { - field.onChange([...(field.value || []), inputValue]); + field.onChange([...(field.value || []), inputValue.trim()]); } }} onBlur={field.onBlur} @@ -133,7 +133,7 @@ const EmailParametersForm: React.FC = ({ label={t('Reply to')} style={{ marginTop: 20 }} error={!!fieldState.error} - helperText={fieldState.error?.message} + helperText={errors.setting_mails_reply_to?.find ? errors.setting_mails_reply_to?.find((value) => value != null)?.message ?? '' : ''} /> )} /> From 5aa437556b784226aa4b5dac88354aadaf697227 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 7 Aug 2024 18:27:53 +0200 Subject: [PATCH 04/13] [backend/frontend] Allow user to configure a launch time during import if needed (#1249) --- .../rest/scenario/ScenarioImportApi.java | 4 ++ .../scenario/form/InjectsImportInput.java | 5 ++ .../src/actions/scenarios/scenario-actions.ts | 8 +++ .../src/admin/components/common/Context.ts | 5 ++ .../injects/ImportUploaderInjectFromXls.tsx | 1 + .../ImportUploaderInjectFromXlsInjects.tsx | 64 ++++++++++++++++++- .../scenarios/scenario/ScenarioContext.ts | 5 +- openbas-front/src/utils/api-types.d.ts | 2 + 8 files changed, 90 insertions(+), 4 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioImportApi.java b/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioImportApi.java index 3288c6f2ae..21318286fc 100644 --- a/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioImportApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioImportApi.java @@ -61,6 +61,10 @@ public ImportTestSummary validateImportXLSFile(@PathVariable @NotBlank final Str @Valid @RequestBody final InjectsImportInput input) { Scenario scenario = scenarioService.scenario(scenarioId); + if(input.getLaunchDate() != null) { + scenario.setRecurrenceStart(input.getLaunchDate().toInstant()); + } + // Getting the mapper to use ImportMapper importMapper = importMapperRepository .findById(UUID.fromString(input.getImportMapperId())) diff --git a/openbas-api/src/main/java/io/openbas/rest/scenario/form/InjectsImportInput.java b/openbas-api/src/main/java/io/openbas/rest/scenario/form/InjectsImportInput.java index 21d5af24cb..9376a10be1 100644 --- a/openbas-api/src/main/java/io/openbas/rest/scenario/form/InjectsImportInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/scenario/form/InjectsImportInput.java @@ -5,6 +5,8 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.util.Date; + import static io.openbas.config.AppConfig.MANDATORY_MESSAGE; @Data @@ -21,4 +23,7 @@ public class InjectsImportInput { @NotNull(message = MANDATORY_MESSAGE) @JsonProperty("timezone_offset") private Integer timezoneOffset; + + @JsonProperty("launch_date") + private Date launchDate = null; } diff --git a/openbas-front/src/actions/scenarios/scenario-actions.ts b/openbas-front/src/actions/scenarios/scenario-actions.ts index c3dc95433a..89477f595f 100644 --- a/openbas-front/src/actions/scenarios/scenario-actions.ts +++ b/openbas-front/src/actions/scenarios/scenario-actions.ts @@ -167,3 +167,11 @@ export const importXls = (scenarioId: Scenario['scenario_id'], importId: string, return response; }); }; + +export const dryImportXls = (scenarioId: Scenario['scenario_id'], importId: string, input: InjectsImportInput) => { + const uri = `${SCENARIO_URI}/${scenarioId}/xls/${importId}/dry`; + return simplePostCall(uri, input) + .then((response) => { + return response; + }); +}; diff --git a/openbas-front/src/admin/components/common/Context.ts b/openbas-front/src/admin/components/common/Context.ts index 19016847f4..52ad7e4ab8 100644 --- a/openbas-front/src/admin/components/common/Context.ts +++ b/openbas-front/src/admin/components/common/Context.ts @@ -65,6 +65,7 @@ export type InjectContextType = { onInjectDone?: (injectId: Inject['inject_id']) => void, onDeleteInject: (injectId: Inject['inject_id']) => void, onImportInjectFromXls?: (importId: string, input: InjectsImportInput) => Promise + onDryImportInjectFromXls?: (importId: string, input: InjectsImportInput) => Promise }; export type AtomicTestingContextType = { @@ -144,6 +145,10 @@ export const InjectContext = createContext({ return new Promise(() => { }); }, + onDryImportInjectFromXls(_importId: string, _input: InjectsImportInput): Promise { + return new Promise(() => { + }); + }, }); export const AtomicTestingContext = createContext({ diff --git a/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXls.tsx b/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXls.tsx index 67d0fc5a71..7493146ab9 100644 --- a/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXls.tsx +++ b/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXls.tsx @@ -80,6 +80,7 @@ const ImportUploaderInjectFromXls = () => { && } diff --git a/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXlsInjects.tsx b/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXlsInjects.tsx index 1ebb648ae2..f2210c4dd7 100644 --- a/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXlsInjects.tsx +++ b/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXlsInjects.tsx @@ -1,16 +1,18 @@ import { Autocomplete as MuiAutocomplete, Box, Button, MenuItem, TextField } from '@mui/material'; import { TableViewOutlined } from '@mui/icons-material'; -import React, { FunctionComponent, SyntheticEvent, useEffect, useState } from 'react'; +import React, { FunctionComponent, SyntheticEvent, useContext, useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import moment from 'moment-timezone'; import { makeStyles } from '@mui/styles'; +import { DateTimePicker } from '@mui/x-date-pickers'; import { zodImplement } from '../../../../utils/Zod'; import { useFormatter } from '../../../../components/i18n'; -import type { ImportMapper, InjectsImportInput } from '../../../../utils/api-types'; +import type { ImportMapper, InjectsImportInput, ImportMessage, ImportTestSummary } from '../../../../utils/api-types'; import { searchMappers } from '../../../../actions/mapper/mapper-actions'; import type { Page } from '../../../../components/common/pagination/Page'; +import { InjectContext } from '../Context'; const useStyles = makeStyles(() => ({ container: { @@ -38,17 +40,20 @@ const useStyles = makeStyles(() => ({ interface FormProps { sheetName: string; importMapperId: string; + startDate?: string; timezone: string; } interface Props { sheets: string[]; + importId: string; handleClose: () => void; handleSubmit: (input: InjectsImportInput) => void; } const ImportUploaderInjectFromXlsInjects: FunctionComponent = ({ sheets, + importId, handleClose, handleSubmit, }) => { @@ -59,12 +64,17 @@ const ImportUploaderInjectFromXlsInjects: FunctionComponent = ({ // TimeZone const timezones = moment.tz.names(); + // Launch Date + const [needLaunchDate, setNeedLaunchDate] = useState(false); + const injectContext = useContext(InjectContext); + // Form const { register, control, handleSubmit: handleSubmitForm, formState: { errors, isDirty, isSubmitting }, + getValues, } = useForm({ mode: 'onTouched', resolver: zodResolver( @@ -72,6 +82,10 @@ const ImportUploaderInjectFromXlsInjects: FunctionComponent = ({ sheetName: z.string().min(1, { message: t('Should not be empty') }), importMapperId: z.string().min(1, { message: t('Should not be empty') }), timezone: z.string().min(1, { message: t('Should not be empty') }), + startDate: z.string().optional(), + }).refine((data) => !needLaunchDate || (needLaunchDate && data.startDate !== undefined), { + message: t('Should not be empty'), + path: ['startDate'], }), ), defaultValues: { @@ -99,6 +113,7 @@ const ImportUploaderInjectFromXlsInjects: FunctionComponent = ({ import_mapper_id: values.importMapperId, sheet_name: values.sheetName, timezone_offset: moment.tz(values.timezone).utcOffset(), + launch_date: values.startDate, }; handleSubmit(input); }; @@ -109,8 +124,26 @@ const ImportUploaderInjectFromXlsInjects: FunctionComponent = ({ handleSubmitForm(onSubmitImportInjects)(e); }; + const checkNeedLaunchDate = () => { + const formValues = getValues(); + if (formValues.importMapperId && formValues.sheetName && formValues.timezone) { + setNeedLaunchDate(false); + const input: InjectsImportInput = { + import_mapper_id: formValues.importMapperId, + sheet_name: formValues.sheetName, + timezone_offset: moment.tz(formValues.timezone).utcOffset(), + }; + injectContext.onDryImportInjectFromXls?.(importId, input).then((value: ImportTestSummary) => { + const criticalMessages = value.import_message?.filter((importMessage: ImportMessage) => importMessage.message_level === 'CRITICAL'); + if (criticalMessages && criticalMessages?.filter((message) => { return message.message_code === 'ABSOLUTE_TIME_WITHOUT_START_DATE'; }).length > 0) { + setNeedLaunchDate(true); + } + }); + } + }; + return ( -
+
= ({ options={sheets} onChange={(_, v) => { onChange(v); + checkNeedLaunchDate(); }} renderInput={(params) => ( = ({ options={mapperOptions} onChange={(_, v) => { onChange(v?.id); + checkNeedLaunchDate(); }} renderOption={(props, option) => ( @@ -178,6 +213,29 @@ const ImportUploaderInjectFromXlsInjects: FunctionComponent = ({ /> )} /> + {needLaunchDate + && ( + { + return (startDate ? field.onChange(new Date(startDate).toISOString()) : field.onChange('')); + }} + slotProps={{ + textField: { + fullWidth: true, + error: !!fieldState.error, + helperText: fieldState.error && fieldState.error?.message, + }, + }} + label={t('Start date')} + /> + )} + />} { const dispatch = useAppDispatch(); @@ -39,6 +39,9 @@ const injectContextForScenario = (scenario: ScenarioStore) => { resolve(response.data); })); }, + async onDryImportInjectFromXls(importId: string, input: InjectsImportInput): Promise { + return dryImportXls(scenario.scenario_id, importId, input).then((result) => result.data); + }, onBulkDeleteInjects(injectIds: string[]): void { return dispatch(bulkDeleteInjectsForScenario(scenario.scenario_id, injectIds)); }, diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 1673a75c4e..5d030f19e7 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -1493,6 +1493,8 @@ export interface InjectorUpdateInput { export interface InjectsImportInput { import_mapper_id: string; + /** @format date-time */ + launch_date?: string; sheet_name: string; /** @format int32 */ timezone_offset: number; From b492a1895d52cee7f8ce68b8de8bd53b977583f8 Mon Sep 17 00:00:00 2001 From: Stephanya Casanova Date: Thu, 8 Aug 2024 11:17:39 +0200 Subject: [PATCH 05/13] [backend] Fix expectations are not properly set (#1307) --- .../java/io/openbas/injectors/caldera/CalderaContract.java | 4 ++-- .../main/java/io/openbas/rest/inject/output/InjectOutput.java | 2 +- .../main/java/io/openbas/service/AtomicTestingService.java | 4 ++-- .../src/main/java/io/openbas/integrations/PayloadService.java | 4 ++-- .../src/admin/components/common/injects/InjectDefinition.js | 2 +- .../src/admin/components/common/injects/InjectPopover.tsx | 2 +- .../admin/components/common/injects/UpdateInjectDetails.js | 1 + .../common/injects/expectations/ExpectationFormCreate.tsx | 3 +-- .../common/injects/expectations/ExpectationFormUtils.ts | 2 +- .../common/injects/expectations/InjectExpectations.tsx | 1 + .../components/simulations/simulation/injects/QuickInject.js | 2 +- 11 files changed, 14 insertions(+), 13 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java index f1418aa9ad..a25e60bf09 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java +++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java @@ -87,12 +87,12 @@ private ContractExpectations expectations() { Expectation preventionExpectation = new Expectation(); preventionExpectation.setType(PREVENTION); preventionExpectation.setName("Expect inject to be prevented"); - preventionExpectation.setScore(0); + preventionExpectation.setScore(100); // Detection Expectation detectionExpectation = new Expectation(); detectionExpectation.setType(DETECTION); detectionExpectation.setName("Expect inject to be detected"); - detectionExpectation.setScore(0); + detectionExpectation.setScore(100); return expectationsField("expectations", "Expectations", List.of(preventionExpectation, detectionExpectation)); } diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java index 10527f90c8..69deae644c 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java @@ -63,7 +63,7 @@ public class InjectOutput { @JsonProperty("inject_testable") public boolean canBeTested() { - return this.getInjectType().equals(EmailContract.TYPE) || this.getInjectType().equals(OvhSmsContract.TYPE); + return EmailContract.TYPE.equals(this.getInjectType()) || OvhSmsContract.TYPE.equals(this.getInjectType()); } public InjectOutput( diff --git a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java index 99f22edb1d..e5b5d9593c 100644 --- a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java +++ b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java @@ -79,8 +79,8 @@ public class AtomicTestingService { private EntityManager entityManager; public InjectResultDTO findById(String injectId) { - Optional statusById = injectRepository.findWithStatusById(injectId); - return statusById + Optional inject = injectRepository.findWithStatusById(injectId); + return inject .map(AtomicTestingMapper::toDtoWithTargetResults) .orElseThrow(ElementNotFoundException::new); } diff --git a/openbas-framework/src/main/java/io/openbas/integrations/PayloadService.java b/openbas-framework/src/main/java/io/openbas/integrations/PayloadService.java index 303766828b..07dc339cdd 100644 --- a/openbas-framework/src/main/java/io/openbas/integrations/PayloadService.java +++ b/openbas-framework/src/main/java/io/openbas/integrations/PayloadService.java @@ -130,12 +130,12 @@ private ContractExpectations expectations() { Expectation preventionExpectation = new Expectation(); preventionExpectation.setType(PREVENTION); preventionExpectation.setName("Expect inject to be prevented"); - preventionExpectation.setScore(0); + preventionExpectation.setScore(100); // Detection Expectation detectionExpectation = new Expectation(); detectionExpectation.setType(DETECTION); detectionExpectation.setName("Expect inject to be detected"); - detectionExpectation.setScore(0); + detectionExpectation.setScore(100); return expectationsField("expectations", "Expectations", List.of(preventionExpectation, detectionExpectation)); } diff --git a/openbas-front/src/admin/components/common/injects/InjectDefinition.js b/openbas-front/src/admin/components/common/injects/InjectDefinition.js index cb7b337277..5a340d7118 100644 --- a/openbas-front/src/admin/components/common/injects/InjectDefinition.js +++ b/openbas-front/src/admin/components/common/injects/InjectDefinition.js @@ -1443,7 +1443,7 @@ class InjectDefinition extends Component { {hasExpectations && ( 0) ? expectations : predefinedExpectations} handleExpectations={this.handleExpectations.bind(this)} /> )} diff --git a/openbas-front/src/admin/components/common/injects/InjectPopover.tsx b/openbas-front/src/admin/components/common/injects/InjectPopover.tsx index d34fa61d5b..aca4dd595a 100644 --- a/openbas-front/src/admin/components/common/injects/InjectPopover.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectPopover.tsx @@ -194,7 +194,7 @@ const InjectPopover: FunctionComponent = ({ open={Boolean(anchorEl)} onClose={handlePopoverClose} > - + {t('Duplicate')} { if (openDetails) { // eslint-disable-next-line no-param-reassign diff --git a/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx b/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx index 4e19a055a0..00507fa8b5 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx +++ b/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx @@ -42,7 +42,7 @@ const ExpectationFormCreate: FunctionComponent = ({ expectation_type: predefinedExpectation.expectation_type ?? '', expectation_name: predefinedExpectation.expectation_name ?? '', expectation_description: predefinedExpectation.expectation_description ?? '', - expectation_score: predefinedExpectation.expectation_score ?? 100, + expectation_score: predefinedExpectation.expectation_score > 0 ? predefinedExpectation.expectation_score : 100, expectation_expectation_group: predefinedExpectation.expectation_expectation_group ?? false, }; } @@ -56,7 +56,6 @@ const ExpectationFormCreate: FunctionComponent = ({ }; const predefinedTypes = predefinedExpectations.map((e) => e.expectation_type); - const initialValues = computeValuesFromType(predefinedTypes[0]); const { diff --git a/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormUtils.ts b/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormUtils.ts index ba6b3f5473..c826fb83aa 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormUtils.ts +++ b/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormUtils.ts @@ -17,7 +17,7 @@ export const formProps = (initialValues: ExpectationInput, t: (key: string) => s expectation_type: z.string(), expectation_name: z.string().min(1, { message: t('Should not be empty') }), expectation_description: z.string().optional(), - expectation_score: z.coerce.number(), + expectation_score: z.coerce.number().min(1, 'Score must be greater than 0'), expectation_expectation_group: z.coerce.boolean(), })), defaultValues: initialValues, diff --git a/openbas-front/src/admin/components/common/injects/expectations/InjectExpectations.tsx b/openbas-front/src/admin/components/common/injects/expectations/InjectExpectations.tsx index a23947566b..5d449b3044 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/InjectExpectations.tsx +++ b/openbas-front/src/admin/components/common/injects/expectations/InjectExpectations.tsx @@ -41,6 +41,7 @@ const InjectExpectations: FunctionComponent = ({ const [expectations, setExpectations] = useState(expectationDatas ?? []); + // Filter predefinedExpectations already included into expectations const predefinedExpectations = predefinedExpectationDatas .filter((pe) => !expectations.map((e) => e.expectation_type).includes(pe.expectation_type)); diff --git a/openbas-front/src/admin/components/simulations/simulation/injects/QuickInject.js b/openbas-front/src/admin/components/simulations/simulation/injects/QuickInject.js index 58befaada5..f6dde338b9 100644 --- a/openbas-front/src/admin/components/simulations/simulation/injects/QuickInject.js +++ b/openbas-front/src/admin/components/simulations/simulation/injects/QuickInject.js @@ -1250,7 +1250,7 @@ class QuickInject extends Component { {hasExpectations && 0) ? expectations : predefinedExpectations} handleExpectations={this.handleExpectations.bind(this)} /> } From bf9eb702bd8b651f3b3afab1aefe850705933b75 Mon Sep 17 00:00:00 2001 From: Stephanya Casanova Date: Thu, 8 Aug 2024 14:31:58 +0200 Subject: [PATCH 06/13] [frontend] Add translations (#1231) --- .../atomic_testing/TargetResultsDetail.tsx | 4 +- .../common/injects/InjectPopover.tsx | 6 +- openbas-front/src/utils/Localization.js | 84 +++++++++++-------- 3 files changed, 55 insertions(+), 39 deletions(-) diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/TargetResultsDetail.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/TargetResultsDetail.tsx index 28e2419f4c..a54d9e570b 100644 --- a/openbas-front/src/admin/components/atomic_testings/atomic_testing/TargetResultsDetail.tsx +++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/TargetResultsDetail.tsx @@ -109,7 +109,7 @@ const TargetResultsDetailFlow: FunctionComponent = ({ const [targetResults, setTargetResults] = useState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); - const initialSteps = [{ label: 'Attack started', type: '', key: 'attack-started' }, { label: 'Attack ended', type: '', key: 'attack-ended' }]; + const initialSteps = [{ label: t('Attack started'), type: '', key: 'attack-started' }, { label: t('Attack ended'), type: '', key: 'attack-ended' }]; const sortOrder = ['PREVENTION', 'DETECTION', 'MANUAL']; // Flow const layoutOptions: LayoutOptions = { @@ -190,7 +190,7 @@ const TargetResultsDetailFlow: FunctionComponent = ({ useEffect(() => { if (target) { setInitialized(false); - const steps = [...computeInitialSteps(initialSteps), ...[{ label: 'Unknown result', type: '', status: 'PENDING' }]]; + const steps = [...computeInitialSteps(initialSteps), ...[{ label: t('Unknown result'), type: '', status: 'PENDING' }]]; setNodes(steps.map((step: Steptarget, index) => ({ id: `result-${index}`, type: 'result', diff --git a/openbas-front/src/admin/components/common/injects/InjectPopover.tsx b/openbas-front/src/admin/components/common/injects/InjectPopover.tsx index aca4dd595a..1d73bc5066 100644 --- a/openbas-front/src/admin/components/common/injects/InjectPopover.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectPopover.tsx @@ -289,7 +289,7 @@ const InjectPopover: FunctionComponent = ({ > - {t(`Do you want to delete this inject: ${inject.inject_title}?`)} + {`${t('Do you want to delete this inject:')} ${inject.inject_title} ?`} @@ -332,7 +332,7 @@ const InjectPopover: FunctionComponent = ({ > -

{t(`Do you want to test this inject: ${inject.inject_title}?`)}

+

{`${t('Do you want to test this inject:')} ${inject.inject_title} ?`}

@@ -372,7 +372,7 @@ const InjectPopover: FunctionComponent = ({ > - {t(`Do you want to disable this inject: ${inject.inject_title}?`)} + {`${t('Do you want to disable this inject:')} ${inject.inject_title} ?`} diff --git a/openbas-front/src/utils/Localization.js b/openbas-front/src/utils/Localization.js index 27e25d8e59..a0bc3a0153 100644 --- a/openbas-front/src/utils/Localization.js +++ b/openbas-front/src/utils/Localization.js @@ -75,6 +75,7 @@ const i18n = { Name: 'Nom', Subtitle: 'Sous-titre', 'Please note that if you change the “Reply to” address, the email interaction functionality in the platform will be disabled.': 'Veuillez noter que si vous modifiez l\'adresse « Répondre à », la fonctionnalité d\'interaction par mail dans la plateforme sera désactivée.', + 'If you remove the default email address, the email reception for this simulation / scenario will be disabled.': 'Si vous supprimez l\'adresse e-mail par défaut, la réception des e-mails pour cette simulation / ce scénario sera désactivée.', 'Start date': 'Date de début', 'End date': 'Date de fin', To: 'A', @@ -179,6 +180,7 @@ const i18n = { 'Do you want to duplicate this atomic testing:': 'Voulez-vous dupliquer cet atomic testing : ', 'Do you want to delete this atomic testing:': 'Voulez-vous supprimer cet atomic testing : ', 'Do you want to duplicate this inject:': 'Voulez-vous dupliquer ce stimuli : ', + 'Do you want to delete this inject:': 'Voulez-vous supprimer ce stimuli : ', Delete: 'Supprimer', Edition: 'Edition', Confirmation: 'Confirmation', @@ -313,10 +315,12 @@ const i18n = { 'External ID': 'ID externe', 'Update the attack pattern': "Modifier le motif d'attaque", 'Create a new attack pattern': "Créer un nouveau motif d'attaque", + 'Do you want to delete this attack pattern?': 'Voulez-vous supprimer ce modèle d\'attaque ?', 'Create a new kill chain phase': 'Créer une nouvelle phase de kill chain', 'Phase name': 'Nom de la phase', 'Kill chain name': 'Nom de la kill chain', 'Update the kill chain phase': 'Modifier la phase de la kill chain', + 'Do you want to delete this kill chain phase?': 'Voulez-vous supprimer cette kill chain ?', External: 'Externe', openbas_email: 'Email', openbas_ovh_sms: 'SMS (OVH)', @@ -387,6 +391,8 @@ const i18n = { 'Le stimuli sera uniquement envoyé à vous même.', Try: 'Tester', Disable: 'Désactiver', + 'Do you want to disable this inject:': 'Voulez-vous désactiver ce stimuli : ', + 'Do you want to test this inject:': 'Voulez-vous tester ce stimuli : ', Enable: 'Activer', 'Mark as done': 'Marquer comme fait', 'Do you want to mark this inject as done?': @@ -543,6 +549,8 @@ const i18n = { 'Souhaitez-vous supprimer cette pression médiatique ?', 'Do you want to delete this challenge?': 'Souhaitez-vous supprimer ce challenge ?', + 'Used challenges (in injects)': 'Défis utilisés (dans les stimulis)', + 'No challenge are used in the injects of this simulation.': 'Aucun défi n\'est utilisé dans les stimulis de cette simulation.', 'Create a new media pressure': 'Créer une nouvelle pression médiatique', 'Collaborative lessons learned': "Retour d'expérience collaboratif", Answers: 'Réponses', @@ -599,6 +607,7 @@ const i18n = { 'No category': 'Aucune catégorie', 'No challenge in this simulation yet.': 'Encore aucun challenge dans cette simulation.', + 'No media pressure article available in this simulation yet.': 'Aucun article sur la pression médiatique disponible dans cette simulation pour l\'instant', 'Distribution of expectations by team': 'Distribution des attendus par équipe', 'Distribution of expectations by inject type': @@ -773,6 +782,7 @@ const i18n = { 'Please provide one MAC address per line.': 'Veuillez fournir une adresse MAC par ligne.', 'Invalid MAC address': 'Mac adresse non valide', Platform: 'Plateforme', + 'Platform(s)': 'Plateforme(s)', 'Collected by': 'Collecté par', 'Targeted asets': 'Actifs ciblés', 'Add assets': 'Ajouter des actifs', @@ -812,6 +822,7 @@ const i18n = { phone_number_tooltip: 'Le numéro de téléphone devrait commencer par un signe plus ( + )\n' + 'Il peut contenir des espaces ou des tirets ( – ) ou des parenthèses.\n', 'Instantiate a simulation': 'Créer une nouvelle simulation', + 'Do you want to delete the asset group ?': 'Voulez-vous supprimer le groupe d\'actifs ?', // -- FILTERS -- 'Add filter': 'Ajout d\'un filtre', 'Clear filters': 'Supprimer les filtres', @@ -868,6 +879,7 @@ const i18n = { 'Validation Failed': "Échec de l'attaque", 'Attack Unblocked': 'Attaque débloquée', 'Waiting Response': 'En attente', + 'Unknown result': 'Résultat inconnu', 'Inject Execution Status': "Statut d'exécution injecté", 'Global score': 'Score global', 'Execution date': 'Date d\'exécution', @@ -920,6 +932,7 @@ const i18n = { Agents: 'Agents', 'Learn more information about how to setup simulation agents': 'En savoir plus sur la configuration des agents de simulation', 'in the documentation': 'dans la documentation', + 'Here, you can download and install simulation agents available in your executors. Depending on the integrations you have enabled, some of them may be unavailable.': 'Vous pouvez ici télécharger et installer les agents de simulation disponibles pour vos exécuteurs. Selon les intégrations que vous avez activées, certains agents peuvent ne pas être disponibles.', 'Installation path': "Chemin d'installation", Architecture: 'Architecture', AMD64: 'AMD64', @@ -989,6 +1002,7 @@ const i18n = { 'Windows Powershell': 'Windows Powershell', 'Update the payload': 'Mettre à jour la charge utile', Payloads: 'Charges utiles', + 'Do you want to duplicate this payload?': 'Voulez-vous dupliquer cette charge utile ?', 'Number of simulations': 'Nombre de simulations', 'Performance Overview': "Vue d'ensemble des performances", 'Top simulation categories': 'Principales catégories de simulation', @@ -1008,6 +1022,7 @@ const i18n = { 'Supporting adding new contracts': "Supporte l'ajout de nouveaux contrats", 'Create a new mitigation': 'Créer une nouvelle atténuation', 'Update the mitigation': "Mettre à jour l'atténuation", + 'Do you want to delete this mitigation?': 'Voulez-vous supprimer cette atténuation ?', People: 'Personnes', 'Install simulation agents': 'Installer des agents de simulation', 'Modify the scheduling': 'Modifier la planification', @@ -1165,46 +1180,47 @@ const i18n = { "Souhaitez-vous supprimer cette question de retour d'expérience ?", x86_64: 'x86_64', arm64: 'arm64', - 'Invalid IP addresses': 'Invalid IP addresses', - 'Invalid MAC addresses': 'Invalid MAC addresses', - 'Create a new security platform': 'Create a new security platform', + 'Invalid IP addresses': 'Adresses IP invalides', + 'Invalid MAC addresses': 'Adresses MAC invalides', + 'Create a new security platform': 'Créer une nouvelle plateforme de sécurité', EDR: 'EDR', XDR: 'XDR', SIEM: 'SIEM', SOAR: 'SOAR', NDR: 'NDR', ISPM: 'ISPM', - 'Logo light': 'Logo light', - 'Logo dark': 'Logo dark', - 'Update the security platform': 'Update the security platform', - 'Security Platforms': 'Security Platforms', - 'Content should not be empty': 'Content should not be empty', - Timezone: 'Timezone', - Log: 'Log', - 'Your file should be a XLS': 'Your file should be a XLS', - 'Launch import': 'Launch import', - 'Show timeline': 'Show timeline', - 'Selected payload': 'Selected payload', - Prevention: 'Prevention', - 'Start date should be at least today': 'Start date should be at least today', - 'Update the xls mapper': 'Update the xls mapper', - 'Data ingestion': 'Data ingestion', - 'XLS mappers': 'XLS mappers', - 'Rule attributes columns': 'Rule attributes columns', - 'Testing XLS mapper': 'Testing XLS mapper', - Policies: 'Policies', - 'Login messages': 'Login messages', - 'Export this list': 'Export this list', - 'Should be a valid XLS file': 'Should be a valid XLS file', - 'This file is not in the specified format': 'This file is not in the specified format', - 'This file is too large': 'This file is too large', - 'Associated file': 'Associated file', - 'Select your file': 'Select your file', - 'Add document': 'Add document', - 'I have read and comply with the above statement': 'I have read and comply with the above statement', - 'Date should be at least today': 'Date should be at least today', - 'Security platform': 'Security platform', - 'Security platforms': 'Security platforms', + 'Logo light': 'Logo clair', + 'Logo dark': 'Logo sombre', + 'Update the security platform': 'Mettre à jour la plateforme de sécurité', + 'Security Platforms': 'Plateformes de sécurité', + 'Content should not be empty': 'Le contenu ne doit pas être vide', + Timezone: 'Fuseau horaire', + Log: 'Journal', + 'Your file should be a XLS': 'Votre fichier doit être un XLS', + 'Launch import': "Lancer l'importation", + 'Show timeline': 'Afficher la chronologie', + 'Selected payload': 'Charge utile sélectionnée', + Prevention: 'Prévention', + 'Start date should be at least today': "La date de début doit être au moins aujourd'hui", + 'Update the xls mapper': 'Mettre à jour le mappeur xls', + 'Data ingestion': 'Ingestion de données', + 'XLS mappers': 'Mappeurs XLS', + 'Rule attributes columns': 'Colonnes des attributs de la règle', + 'Testing XLS mapper': 'Test du mappeur XLS', + Policies: 'Politiques', + 'Login messages': 'Messages de connexion', + 'Export this list': 'Exporter cette liste', + 'Should be a valid XLS file': 'Doit être un fichier XLS valide', + 'This file is not in the specified format': "Ce fichier n'est pas dans le format spécifié", + 'This file is too large': 'Ce fichier est trop volumineux', + 'Associated file': 'Fichier associé', + 'Select your file': 'Sélectionnez votre fichier', + 'Add document': 'Ajouter un document', + 'I have read and comply with the above statement': "J'ai lu et j'accepte la déclaration ci-dessus", + 'Date should be at least today': "La date doit être au moins aujourd'hui", + 'Security platform': 'Plateforme de sécurité', + 'Security platforms': 'Plateformes de sécurité', + 'Do you want to delete the security platform?': 'Voulez-vous supprimer cette plateforme de sécurité ?', // 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.', From 706e24851cf2ef1b6c29ab03ce5a4b54df38e356 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Fri, 9 Aug 2024 10:09:23 +0200 Subject: [PATCH 07/13] [frontend/backend]Adding the ability to import/export mappers (#1263) Co-authored-by: Romuald Lemesle --- .../rest/exception/ImportException.java | 15 ++++++ .../io/openbas/rest/helper/RestBehavior.java | 16 +++++-- .../io/openbas/rest/mapper/MapperApi.java | 44 +++++++++++++++++ .../mapper/export/MapperExportMixins.java | 36 ++++++++++++++ .../rest/mapper/form/ExportMapperInput.java | 18 +++++++ .../mapper/form/ImportMapperAddInput.java | 6 +-- .../mapper/form/ImportMapperUpdateInput.java | 6 +-- .../mapper/form/InjectImporterAddInput.java | 2 +- .../form/InjectImporterUpdateInput.java | 2 +- .../io/openbas/service/MapperService.java | 30 +++++++++++- .../service/MapperServiceExportTest.java | 47 +++++++++++++++++++ .../io/openbas/service/MapperServiceTest.java | 2 +- .../src/actions/mapper/mapper-actions.ts | 21 ++++++++- openbas-front/src/actions/mapper/mapper.ts | 4 +- .../data_ingestion/ImportUploaderMapper.tsx | 26 ++++++++++ .../data_ingestion/XlsMapperPopover.tsx | 19 +++++++- .../settings/data_ingestion/XlsMappers.tsx | 9 ++-- .../data_ingestion/xls_mapper/MapperForm.tsx | 28 +++++------ .../xls_mapper/RulesContractContent.tsx | 40 ++++++++-------- .../xls_mapper/XlsMapperUpdate.tsx | 10 ++-- openbas-front/src/utils/api-types.d.ts | 25 +++++----- .../openbas/database/model/ImportMapper.java | 2 +- .../database/model/InjectImporter.java | 2 +- 23 files changed, 338 insertions(+), 72 deletions(-) create mode 100644 openbas-api/src/main/java/io/openbas/rest/exception/ImportException.java create mode 100644 openbas-api/src/main/java/io/openbas/rest/mapper/export/MapperExportMixins.java create mode 100644 openbas-api/src/main/java/io/openbas/rest/mapper/form/ExportMapperInput.java create mode 100644 openbas-api/src/test/java/io/openbas/service/MapperServiceExportTest.java create mode 100644 openbas-front/src/admin/components/settings/data_ingestion/ImportUploaderMapper.tsx diff --git a/openbas-api/src/main/java/io/openbas/rest/exception/ImportException.java b/openbas-api/src/main/java/io/openbas/rest/exception/ImportException.java new file mode 100644 index 0000000000..31a4bd9fca --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/exception/ImportException.java @@ -0,0 +1,15 @@ +package io.openbas.rest.exception; + +public class ImportException extends Exception { + + private final String field; + + public ImportException(String field, String message) { + super(message); + this.field = field; + } + + public String getField() { + return field; + } +} diff --git a/openbas-api/src/main/java/io/openbas/rest/helper/RestBehavior.java b/openbas-api/src/main/java/io/openbas/rest/helper/RestBehavior.java index 1a2fa3bf2e..de5a289cad 100644 --- a/openbas-api/src/main/java/io/openbas/rest/helper/RestBehavior.java +++ b/openbas-api/src/main/java/io/openbas/rest/helper/RestBehavior.java @@ -8,9 +8,7 @@ import io.openbas.database.model.Organization; import io.openbas.database.model.User; import io.openbas.database.repository.UserRepository; -import io.openbas.rest.exception.ElementNotFoundException; -import io.openbas.rest.exception.FileTooBigException; -import io.openbas.rest.exception.InputValidationException; +import io.openbas.rest.exception.*; import jakarta.annotation.Resource; import lombok.extern.java.Log; import org.hibernate.exception.ConstraintViolationException; @@ -77,6 +75,18 @@ public ValidationErrorBag handleInputValidationExceptions(InputValidationExcepti return bag; } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(ImportException.class) + public ValidationErrorBag handleBadRequestExceptions(ImportException ex) { + ValidationErrorBag bag = new ValidationErrorBag(HttpStatus.BAD_REQUEST.value(), ex.getMessage()); + ValidationError errors = new ValidationError(); + Map errorsBag = new HashMap<>(); + errorsBag.put(ex.getField(), new ValidationContent(ex.getMessage())); + errors.setChildren(errorsBag); + bag.setErrors(errors); + return bag; + } + @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(AccessDeniedException.class) public ValidationErrorBag handleValidationExceptions() { diff --git a/openbas-api/src/main/java/io/openbas/rest/mapper/MapperApi.java b/openbas-api/src/main/java/io/openbas/rest/mapper/MapperApi.java index 14894a17e7..861c1e5191 100644 --- a/openbas-api/src/main/java/io/openbas/rest/mapper/MapperApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/mapper/MapperApi.java @@ -1,12 +1,15 @@ package io.openbas.rest.mapper; +import com.fasterxml.jackson.core.type.TypeReference; import io.openbas.database.model.ImportMapper; import io.openbas.database.model.Scenario; import io.openbas.database.raw.RawPaginationImportMapper; import io.openbas.database.repository.ImportMapperRepository; import io.openbas.rest.exception.ElementNotFoundException; import io.openbas.rest.exception.FileTooBigException; +import io.openbas.rest.exception.ImportException; import io.openbas.rest.helper.RestBehavior; +import io.openbas.rest.mapper.form.ExportMapperInput; import io.openbas.rest.mapper.form.ImportMapperAddInput; import io.openbas.rest.mapper.form.ImportMapperUpdateInput; import io.openbas.rest.scenario.form.InjectsImportTestInput; @@ -16,6 +19,7 @@ import io.openbas.service.MapperService; import io.openbas.utils.pagination.SearchPaginationInput; import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletResponse; import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -23,12 +27,19 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.io.FilenameUtils; import org.springframework.data.domain.Page; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.reactive.function.UnsupportedMediaTypeException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.UUID; @@ -72,6 +83,39 @@ public ImportMapper createImportMapper(@RequestBody @Valid final ImportMapperAdd return mapperService.createAndSaveImportMapper(importMapperAddInput); } + @Secured(ROLE_ADMIN) + @PostMapping(value="/api/mappers/export") + public void exportMappers( + @RequestBody @Valid final ExportMapperInput exportMapperInput, + HttpServletResponse response) { + try { + String jsonMappers = mapperService.exportMappers(exportMapperInput.getIdsToExport()); + String rightNow = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); + String filename = MessageFormat.format("mappers_{0}.json", rightNow); + + response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename); + response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_OK); + + response.getOutputStream().write(jsonMappers.getBytes(StandardCharsets.UTF_8)); + response.getOutputStream().flush(); + response.getOutputStream().close(); + } catch (IOException e) { + throw new RuntimeException("Error during export", e); + } + } + + @Secured(ROLE_ADMIN) + @PostMapping("/api/mappers/import") + public void importMappers(@RequestPart("file") @NotNull MultipartFile file) throws ImportException { + try { + mapperService.importMappers(mapper.readValue(file.getInputStream().readAllBytes(), new TypeReference<>() { + })); + } catch (Exception e) { + throw new ImportException("Mapper import", "Error during import"); + } + } + @Secured(ROLE_ADMIN) @PutMapping("/api/mappers/{mapperId}") public ImportMapper updateImportMapper(@PathVariable String mapperId, @Valid @RequestBody ImportMapperUpdateInput importMapperUpdateInput) { diff --git a/openbas-api/src/main/java/io/openbas/rest/mapper/export/MapperExportMixins.java b/openbas-api/src/main/java/io/openbas/rest/mapper/export/MapperExportMixins.java new file mode 100644 index 0000000000..605824d33f --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/mapper/export/MapperExportMixins.java @@ -0,0 +1,36 @@ +package io.openbas.rest.mapper.export; + +import com.fasterxml.jackson.annotation.JsonIncludeProperties; + +public class MapperExportMixins { + + private MapperExportMixins() { + + } + + @JsonIncludeProperties(value = { + "import_mapper_name", + "import_mapper_inject_type_column", + "import_mapper_inject_importers", + }) + public static class ImportMapper { + } + + @JsonIncludeProperties(value = { + "inject_importer_type_value", + "inject_importer_injector_contract", + "inject_importer_rule_attributes", + }) + public static class InjectImporter { + } + + @JsonIncludeProperties(value = { + "rule_attribute_columns", + "rule_attribute_name", + "rule_attribute_default_value", + "rule_attribute_additional_config", + }) + public static class RuleAttribute { + } + +} diff --git a/openbas-api/src/main/java/io/openbas/rest/mapper/form/ExportMapperInput.java b/openbas-api/src/main/java/io/openbas/rest/mapper/form/ExportMapperInput.java new file mode 100644 index 0000000000..c6f28b68f7 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/mapper/form/ExportMapperInput.java @@ -0,0 +1,18 @@ +package io.openbas.rest.mapper.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +import static io.openbas.config.AppConfig.MANDATORY_MESSAGE; + +@Data +public class ExportMapperInput { + + @NotNull(message = MANDATORY_MESSAGE) + @JsonProperty("ids_to_export") + private List idsToExport; + +} diff --git a/openbas-api/src/main/java/io/openbas/rest/mapper/form/ImportMapperAddInput.java b/openbas-api/src/main/java/io/openbas/rest/mapper/form/ImportMapperAddInput.java index 72c9b9e560..4bff96888e 100644 --- a/openbas-api/src/main/java/io/openbas/rest/mapper/form/ImportMapperAddInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/mapper/form/ImportMapperAddInput.java @@ -15,15 +15,15 @@ public class ImportMapperAddInput { @NotBlank(message = MANDATORY_MESSAGE) - @JsonProperty("mapper_name") + @JsonProperty("import_mapper_name") private String name; @Pattern(regexp="^[A-Z]{1,2}$") - @JsonProperty("mapper_inject_type_column") + @JsonProperty("import_mapper_inject_type_column") @NotBlank private String injectTypeColumn; - @JsonProperty("mapper_inject_importers") + @JsonProperty("import_mapper_inject_importers") @NotNull private List importers = new ArrayList<>(); diff --git a/openbas-api/src/main/java/io/openbas/rest/mapper/form/ImportMapperUpdateInput.java b/openbas-api/src/main/java/io/openbas/rest/mapper/form/ImportMapperUpdateInput.java index 1a7dfe4d37..5d7461a0b9 100644 --- a/openbas-api/src/main/java/io/openbas/rest/mapper/form/ImportMapperUpdateInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/mapper/form/ImportMapperUpdateInput.java @@ -15,15 +15,15 @@ public class ImportMapperUpdateInput { @NotBlank(message = MANDATORY_MESSAGE) - @JsonProperty("mapper_name") + @JsonProperty("import_mapper_name") private String name; @Pattern(regexp="^[A-Z]{1,2}$") - @JsonProperty("mapper_inject_type_column") + @JsonProperty("import_mapper_inject_type_column") @NotBlank private String injectTypeColumn; - @JsonProperty("mapper_inject_importers") + @JsonProperty("import_mapper_inject_importers") @NotNull private List importers = new ArrayList<>(); } diff --git a/openbas-api/src/main/java/io/openbas/rest/mapper/form/InjectImporterAddInput.java b/openbas-api/src/main/java/io/openbas/rest/mapper/form/InjectImporterAddInput.java index 4adb2814be..496acfba33 100644 --- a/openbas-api/src/main/java/io/openbas/rest/mapper/form/InjectImporterAddInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/mapper/form/InjectImporterAddInput.java @@ -17,7 +17,7 @@ public class InjectImporterAddInput { private String injectTypeValue; @NotBlank(message = MANDATORY_MESSAGE) - @JsonProperty("inject_importer_injector_contract_id") + @JsonProperty("inject_importer_injector_contract") private String injectorContractId; @JsonProperty("inject_importer_rule_attributes") diff --git a/openbas-api/src/main/java/io/openbas/rest/mapper/form/InjectImporterUpdateInput.java b/openbas-api/src/main/java/io/openbas/rest/mapper/form/InjectImporterUpdateInput.java index e5871037ac..1c7301e078 100644 --- a/openbas-api/src/main/java/io/openbas/rest/mapper/form/InjectImporterUpdateInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/mapper/form/InjectImporterUpdateInput.java @@ -20,7 +20,7 @@ public class InjectImporterUpdateInput { private String injectTypeValue; @NotBlank(message = MANDATORY_MESSAGE) - @JsonProperty("inject_importer_injector_contract_id") + @JsonProperty("inject_importer_injector_contract") private String injectorContractId; @JsonProperty("inject_importer_rule_attributes") diff --git a/openbas-api/src/main/java/io/openbas/service/MapperService.java b/openbas-api/src/main/java/io/openbas/service/MapperService.java index dc5f593b1c..8e31f2291c 100644 --- a/openbas-api/src/main/java/io/openbas/service/MapperService.java +++ b/openbas-api/src/main/java/io/openbas/service/MapperService.java @@ -1,14 +1,19 @@ package io.openbas.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.openbas.database.model.ImportMapper; import io.openbas.database.model.InjectImporter; import io.openbas.database.model.InjectorContract; import io.openbas.database.model.RuleAttribute; import io.openbas.database.repository.ImportMapperRepository; import io.openbas.database.repository.InjectorContractRepository; +import io.openbas.helper.ObjectMapperHelper; import io.openbas.rest.exception.ElementNotFoundException; +import io.openbas.rest.mapper.export.MapperExportMixins; import io.openbas.rest.mapper.form.*; import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Service; import java.time.Instant; @@ -20,6 +25,8 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; +import static java.util.stream.StreamSupport.stream; + @RequiredArgsConstructor @Service public class MapperService { @@ -101,7 +108,7 @@ public ImportMapper updateImportMapper(String mapperId, ImportMapperUpdateInput * @return The map of injector contracts by ids */ private Map getMapOfInjectorContracts(List ids) { - return StreamSupport.stream(injectorContractRepository.findAllById(ids).spliterator(), false) + return stream(injectorContractRepository.findAllById(ids).spliterator(), false) .collect(Collectors.toMap(InjectorContract::getId, Function.identity())); } @@ -176,4 +183,25 @@ private void updateInjectImporter(List injectImporter }); } + public String exportMappers(@NotNull final List idsToExport) throws JsonProcessingException { + ObjectMapper objectMapper = ObjectMapperHelper.openBASJsonMapper(); + List mappersList = StreamSupport.stream( + importMapperRepository.findAllById(idsToExport.stream().map(UUID::fromString).toList()).spliterator(), false + ).toList(); + + objectMapper.addMixIn(ImportMapper.class, MapperExportMixins.ImportMapper.class); + objectMapper.addMixIn(InjectImporter.class, MapperExportMixins.InjectImporter.class); + objectMapper.addMixIn(RuleAttribute.class, MapperExportMixins.RuleAttribute.class); + + return objectMapper.writeValueAsString(mappersList); + } + + public void importMappers(List mappers) { + importMapperRepository.saveAll( + mappers.stream() + .map(this::createImportMapper) + .peek((m) -> m.setName(m.getName() + " (Import)")) + .toList() + ); + } } diff --git a/openbas-api/src/test/java/io/openbas/service/MapperServiceExportTest.java b/openbas-api/src/test/java/io/openbas/service/MapperServiceExportTest.java new file mode 100644 index 0000000000..e130a5823e --- /dev/null +++ b/openbas-api/src/test/java/io/openbas/service/MapperServiceExportTest.java @@ -0,0 +1,47 @@ +package io.openbas.service; + +import io.openbas.IntegrationTest; +import io.openbas.database.model.ImportMapper; +import io.openbas.database.repository.ImportMapperRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MapperServiceExportTest extends IntegrationTest { + + @Autowired + private ImportMapperRepository importMapperRepository; + + @Autowired + private MapperService mapperService; + + @DisplayName("Test exporting a mapper") + @Test + void exportMapper() throws Exception { + // -- PREPARE -- + ImportMapper mapper = new ImportMapper(); + mapper.setName("Test Mapper"); + mapper.setInjectTypeColumn("injectType"); + mapper.setInjectImporters(new ArrayList<>()); + ImportMapper mapperSaved = this.importMapperRepository.save(mapper); + + // -- EXECUTE -- + String json = this.mapperService.exportMappers(List.of(mapperSaved.getId())); + + // -- ASSERT -- + assertEquals("[{" + + "\"import_mapper_name\":\"Test Mapper\"," + + "\"import_mapper_inject_type_column\":\"injectType\"," + + "\"import_mapper_inject_importers\":[]" + + "}]", json); + + // -- CLEAN -- + this.importMapperRepository.delete(mapperSaved); + } + +} diff --git a/openbas-api/src/test/java/io/openbas/service/MapperServiceTest.java b/openbas-api/src/test/java/io/openbas/service/MapperServiceTest.java index 696b909e40..ff1bde0929 100644 --- a/openbas-api/src/test/java/io/openbas/service/MapperServiceTest.java +++ b/openbas-api/src/test/java/io/openbas/service/MapperServiceTest.java @@ -189,7 +189,7 @@ void updateSpecificMapperWithUpdatedElements() throws Exception { when(importMapperRepository.findById(any())).thenReturn(Optional.of(importMapper)); when(importMapperRepository.save(any())).thenReturn(importMapper); when(injectorContractRepository.findAllById(any())).thenReturn(importMapper.getInjectImporters().stream().map(InjectImporter::getInjectorContract).toList()); - // -- EXECUTE -- + // -- EXECUTE -- ImportMapper response = mapperService.updateImportMapper(importMapper.getId(), importMapperInput); diff --git a/openbas-front/src/actions/mapper/mapper-actions.ts b/openbas-front/src/actions/mapper/mapper-actions.ts index ddc376740a..c25a93504f 100644 --- a/openbas-front/src/actions/mapper/mapper-actions.ts +++ b/openbas-front/src/actions/mapper/mapper-actions.ts @@ -1,4 +1,11 @@ -import type { ImportMapperAddInput, ImportMapperUpdateInput, InjectsImportTestInput, RawPaginationImportMapper, SearchPaginationInput } from '../../utils/api-types'; +import type { + ExportMapperInput, + ImportMapperAddInput, + ImportMapperUpdateInput, + InjectsImportTestInput, + RawPaginationImportMapper, + SearchPaginationInput, +} from '../../utils/api-types'; import { simpleCall, simpleDelCall, simplePostCall, simplePutCall } from '../../utils/Action'; const XLS_MAPPER_URI = '/api/mappers'; @@ -39,3 +46,15 @@ export const testXlsFile = (importId: string, input: InjectsImportTestInput) => const uri = `${XLS_MAPPER_URI}/store/${importId}`; return simplePostCall(uri, input); }; + +export const exportMapper = (input: ExportMapperInput) => { + const uri = `${XLS_MAPPER_URI}/export`; + return simplePostCall(uri, input).then((response) => { + return { data: response.data, filename: response.headers['content-disposition'].split('filename=')[1] }; + }); +}; + +export const importMapper = (formData: FormData) => { + const uri = `${XLS_MAPPER_URI}/import`; + return simplePostCall(uri, formData); +}; diff --git a/openbas-front/src/actions/mapper/mapper.ts b/openbas-front/src/actions/mapper/mapper.ts index 42a56cba59..50217270c8 100644 --- a/openbas-front/src/actions/mapper/mapper.ts +++ b/openbas-front/src/actions/mapper/mapper.ts @@ -4,6 +4,6 @@ export type InjectImporterStore = Omit & { - inject_importers: InjectImporterStore[]; +export type ImportMapperStore = Omit & { + import_mapper_inject_importers: InjectImporterStore[]; }; diff --git a/openbas-front/src/admin/components/settings/data_ingestion/ImportUploaderMapper.tsx b/openbas-front/src/admin/components/settings/data_ingestion/ImportUploaderMapper.tsx new file mode 100644 index 0000000000..5a56012915 --- /dev/null +++ b/openbas-front/src/admin/components/settings/data_ingestion/ImportUploaderMapper.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import ImportUploader from '../../../../components/common/ImportUploader'; +import { importMapper } from '../../../../actions/mapper/mapper-actions'; + +const ImportUploaderMapper = () => { + // Standard hooks + const navigate = useNavigate(); + + const handleUpload = async (formData: FormData) => { + importMapper(formData).then((result: { data: { [x: string]: string; } }) => { + if (!Object.prototype.hasOwnProperty.call(result, 'FINAL_FORM/form-error')) { + navigate(0); + } + }); + }; + + return ( + + ); +}; + +export default ImportUploaderMapper; diff --git a/openbas-front/src/admin/components/settings/data_ingestion/XlsMapperPopover.tsx b/openbas-front/src/admin/components/settings/data_ingestion/XlsMapperPopover.tsx index b9414dba5f..d39b8b99c1 100644 --- a/openbas-front/src/admin/components/settings/data_ingestion/XlsMapperPopover.tsx +++ b/openbas-front/src/admin/components/settings/data_ingestion/XlsMapperPopover.tsx @@ -2,22 +2,25 @@ import React, { FunctionComponent, useState } from 'react'; import { PopoverEntry } from '../../../../components/common/ButtonPopover'; import IconPopover from '../../../../components/common/IconPopover'; import type { RawPaginationImportMapper } from '../../../../utils/api-types'; -import { deleteMapper } from '../../../../actions/mapper/mapper-actions'; +import { deleteMapper, exportMapper } from '../../../../actions/mapper/mapper-actions'; import DialogDelete from '../../../../components/common/DialogDelete'; import { useFormatter } from '../../../../components/i18n'; import Drawer from '../../../../components/common/Drawer'; import XlsMapperUpdate from './xls_mapper/XlsMapperUpdate'; +import { download } from '../../../../utils/utils'; interface Props { mapper: RawPaginationImportMapper; onUpdate?: (result: RawPaginationImportMapper) => void; onDelete?: (result: string) => void; + onExport?: (result: string) => void; } const XlsMapperPopover: FunctionComponent = ({ mapper, onUpdate, onDelete, + onExport, }) => { // Standard hooks const { t } = useFormatter(); @@ -41,9 +44,23 @@ const XlsMapperPopover: FunctionComponent = ({ handleCloseDelete(); }; + const exportMapperAction = () => { + exportMapper({ + ids_to_export: [mapper.import_mapper_id], + }).then( + (result:{ data: string, filename: string }) => { + download(JSON.stringify(result.data, null, 2), result.filename, 'application/json'); + }, + ); + if (onExport) { + onExport(mapper.import_mapper_id); + } + }; + const entries: PopoverEntry[] = [ { label: 'Update', action: handleOpenEdit }, { label: 'Delete', action: handleOpenDelete }, + { label: 'Export', action: exportMapperAction }, ]; return ( diff --git a/openbas-front/src/admin/components/settings/data_ingestion/XlsMappers.tsx b/openbas-front/src/admin/components/settings/data_ingestion/XlsMappers.tsx index f18e2dfe20..2863061b38 100644 --- a/openbas-front/src/admin/components/settings/data_ingestion/XlsMappers.tsx +++ b/openbas-front/src/admin/components/settings/data_ingestion/XlsMappers.tsx @@ -13,6 +13,7 @@ import DataIngestionMenu from '../DataIngestionMenu'; import XlsMapperCreation from './xls_mapper/XlsMapperCreation'; import PaginationComponent from '../../../../components/common/pagination/PaginationComponent'; import XlsMapperPopover from './XlsMapperPopover'; +import ImportUploaderMapper from './ImportUploaderMapper'; const useStyles = makeStyles(() => ({ container: { @@ -42,7 +43,7 @@ const useStyles = makeStyles(() => ({ })); const inlineStyles: Record = { - mapper_name: { + import_mapper_name: { width: '30%', cursor: 'default', }, @@ -56,7 +57,7 @@ const XlsMappers = () => { // Headers const headers = [ { - field: 'mapper_name', + field: 'import_mapper_name', label: 'Name', isSortable: true, value: (mapper: RawPaginationImportMapper) => mapper.import_mapper_name, @@ -76,7 +77,9 @@ const XlsMappers = () => { fetch={searchMappers} searchPaginationInput={searchPaginationInput} setContent={setMappers} - /> + > + + = ({ onSubmit, editing, initialValues = { - mapper_name: '', - mapper_inject_type_column: '', - mapper_inject_importers: [], + import_mapper_name: '', + import_mapper_inject_type_column: '', + import_mapper_inject_importers: [], }, }) => { // Standard hooks @@ -52,7 +52,7 @@ const MapperForm: React.FC = ({ const importerZodObject = z.object({ inject_importer_type_value: z.string().min(1, { message: t('Should not be empty') }), - inject_importer_injector_contract_id: z.string().min(1, { message: t('Should not be empty') }), + inject_importer_injector_contract: z.string().min(1, { message: t('Should not be empty') }), inject_importer_rule_attributes: z.array(ruleAttributeZodObject).optional(), }); @@ -60,10 +60,10 @@ const MapperForm: React.FC = ({ mode: 'onTouched', resolver: zodResolver( zodImplement().with({ - mapper_name: z.string().min(1, { message: t('Should not be empty') }), - mapper_inject_importers: z.array(importerZodObject) + import_mapper_name: z.string().min(1, { message: t('Should not be empty') }), + import_mapper_inject_importers: z.array(importerZodObject) .min(1, { message: t('At least one inject type is required') }), - mapper_inject_type_column: z.string() + import_mapper_inject_type_column: z.string() .min(1, { message: t('Should not be empty') }), }), ), @@ -74,7 +74,7 @@ const MapperForm: React.FC = ({ const { fields, append, remove } = useFieldArray({ control, - name: 'mapper_inject_importers', + name: 'import_mapper_inject_importers', }); const [openTest, setOpenTest] = useState(false); @@ -87,15 +87,15 @@ const MapperForm: React.FC = ({ fullWidth label={t('Mapper name')} style={{ marginTop: 10 }} - error={!!methods.formState.errors.mapper_name} - helperText={methods.formState.errors.mapper_name?.message} - inputProps={methods.register('mapper_name')} + error={!!methods.formState.errors.import_mapper_name} + helperText={methods.formState.errors.import_mapper_name?.message} + inputProps={methods.register('import_mapper_name')} InputLabelProps={{ required: true }} />
( = ({ color="secondary" aria-label="Add" onClick={() => { - append({ inject_importer_type_value: '', inject_importer_injector_contract_id: '', inject_importer_rule_attributes: [] }); + append({ inject_importer_type_value: '', inject_importer_injector_contract: '', inject_importer_rule_attributes: [] }); }} size="large" >
- {methods.formState.errors.mapper_inject_importers?.message} + {methods.formState.errors.import_mapper_inject_importers?.message}
diff --git a/openbas-front/src/admin/components/settings/data_ingestion/xls_mapper/RulesContractContent.tsx b/openbas-front/src/admin/components/settings/data_ingestion/xls_mapper/RulesContractContent.tsx index 527e85f9ed..b13e422898 100644 --- a/openbas-front/src/admin/components/settings/data_ingestion/xls_mapper/RulesContractContent.tsx +++ b/openbas-front/src/admin/components/settings/data_ingestion/xls_mapper/RulesContractContent.tsx @@ -51,7 +51,7 @@ const useStyles = makeStyles(() => ({ })); interface Props { - field: FieldArrayWithId; + field: FieldArrayWithId; methods: UseFormReturn; index: number; remove: UseFieldArrayRemove; @@ -72,7 +72,7 @@ const RulesContractContent: React.FC = ({ const { fields: rulesFields, remove: rulesRemove, append: rulesAppend } = useFieldArray({ control, - name: `mapper_inject_importers.${index}.inject_importer_rule_attributes`, + name: `import_mapper_inject_importers.${index}.inject_importer_rule_attributes`, }); const [contractFields, setContractFields] = useState([]); @@ -124,8 +124,8 @@ const RulesContractContent: React.FC = ({ }; useEffect(() => { - if (methods.getValues(`mapper_inject_importers.${index}.inject_importer_injector_contract_id`)) { - directFetchInjectorContract(methods.getValues(`mapper_inject_importers.${index}.inject_importer_injector_contract_id`)).then((result: { + if (methods.getValues(`import_mapper_inject_importers.${index}.inject_importer_injector_contract`)) { + directFetchInjectorContract(methods.getValues(`import_mapper_inject_importers.${index}.inject_importer_injector_contract`)).then((result: { data: InjectorContractConverted }) => { const injectorContract = result.data; @@ -138,7 +138,7 @@ const RulesContractContent: React.FC = ({ }, []); const onChangeInjectorContractId = () => { - directFetchInjectorContract(methods.getValues(`mapper_inject_importers.${index}.inject_importer_injector_contract_id`)).then((result: { data: InjectorContractConverted }) => { + directFetchInjectorContract(methods.getValues(`import_mapper_inject_importers.${index}.inject_importer_injector_contract`)).then((result: { data: InjectorContractConverted }) => { const injectorContract = result.data; setInjectorContractLabel(tPick(injectorContract.injector_contract_labels)); const tmp = injectorContract?.convertedContent?.fields @@ -175,7 +175,7 @@ const RulesContractContent: React.FC = ({ variant="outlined" style={{ width: '100%', marginBottom: '10px' }} className={classNames({ - [classes.red]: !!errors.mapper_inject_importers?.[index], + [classes.red]: !!errors.import_mapper_inject_importers?.[index], })} > = ({ fullWidth label={t('Matching type in the xls')} style={{ marginTop: 10 }} - inputProps={methods.register(`mapper_inject_importers.${index}.inject_importer_type_value` as const)} + inputProps={methods.register(`import_mapper_inject_importers.${index}.inject_importer_type_value` as const)} InputLabelProps={{ required: true }} - error={!!methods.formState.errors.mapper_inject_importers?.[index]?.inject_importer_type_value} - helperText={methods.formState.errors.mapper_inject_importers?.[index]?.inject_importer_type_value?.message} + error={!!methods.formState.errors.import_mapper_inject_importers?.[index]?.inject_importer_type_value} + helperText={methods.formState.errors.import_mapper_inject_importers?.[index]?.inject_importer_type_value?.message} /> = ({ ( = ({
{t(ruleField.rule_attribute_name[0].toUpperCase() + ruleField.rule_attribute_name.slice(1))} {isMandatoryField(ruleField.rule_attribute_name) @@ -247,7 +247,7 @@ const RulesContractContent: React.FC = ({ ( = ({ ? ( ) : ( ) @@ -297,7 +297,7 @@ const RulesContractContent: React.FC = ({ {currentRuleIndex === rulesFields.findIndex((r) => r.rule_attribute_name === 'trigger_time') &&
@@ -305,7 +305,7 @@ const RulesContractContent: React.FC = ({ label={t('Time pattern')} fullWidth style={{ marginTop: 10 }} - inputProps={methods.register(`mapper_inject_importers.${index}.inject_importer_rule_attributes.${currentRuleIndex}.rule_attribute_additional_config.timePattern`)} + inputProps={methods.register(`import_mapper_inject_importers.${index}.inject_importer_rule_attributes.${currentRuleIndex}.rule_attribute_additional_config.timePattern`)} /> handleClose, }) => { const initialValues = { - mapper_name: xlsMapper.import_mapper_name ?? '', - mapper_inject_type_column: xlsMapper.import_mapper_inject_type_column ?? '', - mapper_inject_importers: xlsMapper.inject_importers?.map((i) => ({ - inject_importer_injector_contract_id: i.inject_importer_injector_contract, + import_mapper_name: xlsMapper.import_mapper_name ?? '', + import_mapper_inject_type_column: xlsMapper.import_mapper_inject_type_column ?? '', + import_mapper_inject_importers: xlsMapper.import_mapper_inject_importers?.map((i) => ({ + inject_importer_injector_contract: i.inject_importer_injector_contract, inject_importer_type_value: i.inject_importer_type_value, - inject_importer_rule_attributes: i.rule_attributes?.map((r) => ({ + inject_importer_rule_attributes: i.inject_importer_rule_attributes?.map((r) => ({ rule_attribute_name: r.rule_attribute_name, rule_attribute_columns: r.rule_attribute_columns, rule_attribute_default_value: r.rule_attribute_default_value, diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 5d030f19e7..6cb18072ff 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -916,6 +916,10 @@ export interface ExpectationUpdateInput { source_type: string; } +export interface ExportMapperInput { + ids_to_export: string[]; +} + export interface Filter { key: string; mode?: "and" | "or"; @@ -1000,27 +1004,27 @@ export interface ImportMapper { /** @format date-time */ import_mapper_created_at?: string; import_mapper_id: string; + import_mapper_inject_importers?: InjectImporter[]; import_mapper_inject_type_column: string; import_mapper_name: string; /** @format date-time */ import_mapper_updated_at?: string; - inject_importers?: InjectImporter[]; listened?: boolean; updateAttributes?: object; } export interface ImportMapperAddInput { - mapper_inject_importers: InjectImporterAddInput[]; + import_mapper_inject_importers: InjectImporterAddInput[]; /** @pattern ^[A-Z]{1,2}$ */ - mapper_inject_type_column: string; - mapper_name: string; + import_mapper_inject_type_column: string; + import_mapper_name: string; } export interface ImportMapperUpdateInput { - mapper_inject_importers: InjectImporterUpdateInput[]; + import_mapper_inject_importers: InjectImporterUpdateInput[]; /** @pattern ^[A-Z]{1,2}$ */ - mapper_inject_type_column: string; - mapper_name: string; + import_mapper_inject_type_column: string; + import_mapper_name: string; } export interface ImportMessage { @@ -1188,23 +1192,23 @@ export interface InjectImporter { inject_importer_created_at?: string; inject_importer_id: string; inject_importer_injector_contract: InjectorContract; + inject_importer_rule_attributes?: RuleAttribute[]; inject_importer_type_value: string; /** @format date-time */ inject_importer_updated_at?: string; listened?: boolean; - rule_attributes?: RuleAttribute[]; updateAttributes?: object; } export interface InjectImporterAddInput { - inject_importer_injector_contract_id: string; + inject_importer_injector_contract: string; inject_importer_rule_attributes?: RuleAttributeAddInput[]; inject_importer_type_value: string; } export interface InjectImporterUpdateInput { inject_importer_id?: string; - inject_importer_injector_contract_id: string; + inject_importer_injector_contract: string; inject_importer_rule_attributes?: RuleAttributeUpdateInput[]; inject_importer_type_value: string; } @@ -1245,7 +1249,6 @@ export interface InjectOutput { /** @uniqueItems true */ inject_tags?: string[]; inject_teams?: string[]; - inject_testable?: boolean; inject_title?: string; inject_type?: string; } diff --git a/openbas-model/src/main/java/io/openbas/database/model/ImportMapper.java b/openbas-model/src/main/java/io/openbas/database/model/ImportMapper.java index 03b98f9cee..e8e77c4b65 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/ImportMapper.java +++ b/openbas-model/src/main/java/io/openbas/database/model/ImportMapper.java @@ -42,7 +42,7 @@ public class ImportMapper implements Base { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) @JoinColumn(name = "importer_mapper_id", nullable = false) - @JsonProperty("inject_importers") + @JsonProperty("import_mapper_inject_importers") private List injectImporters = new ArrayList<>(); @CreationTimestamp diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectImporter.java b/openbas-model/src/main/java/io/openbas/database/model/InjectImporter.java index 610ffab944..1f9d3b2e02 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectImporter.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectImporter.java @@ -46,7 +46,7 @@ public class InjectImporter implements Base { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) @JoinColumn(name = "attribute_inject_importer_id", nullable = false) - @JsonProperty("rule_attributes") + @JsonProperty("inject_importer_rule_attributes") private List ruleAttributes = new ArrayList<>(); @CreationTimestamp From bd8bf3ff2ba18d90ff1d4a31680f35a8ce9c0c18 Mon Sep 17 00:00:00 2001 From: Stephanya Casanova Date: Fri, 9 Aug 2024 10:15:44 +0200 Subject: [PATCH 08/13] [backend/frontend] Fix score in learned lessons (#1234) --- .../io/openbas/execution/ExecutionContextService.java | 1 + .../components/simulations/simulation/lessons/Lessons.js | 9 ++++++++- .../src/public/components/lessons/LessonsPlayer.js | 4 +++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openbas-framework/src/main/java/io/openbas/execution/ExecutionContextService.java b/openbas-framework/src/main/java/io/openbas/execution/ExecutionContextService.java index 9da402f4ea..8f57f94850 100644 --- a/openbas-framework/src/main/java/io/openbas/execution/ExecutionContextService.java +++ b/openbas-framework/src/main/java/io/openbas/execution/ExecutionContextService.java @@ -41,6 +41,7 @@ public ExecutionContext executionContext(@NotNull final User user, Injection inj executionContext.put(CHALLENGES_URI, baseUrl + "/challenges/" + exerciseId + queryParams); executionContext.put(SCOREBOARD_URI, baseUrl + "/scoreboard/" + exerciseId + queryParams); executionContext.put(LESSONS_URI, baseUrl + "/lessons/" + exerciseId + queryParams); + executionContext.put(EXERCISE, injection.getExercise()); fillDynamicVariable(executionContext, exerciseId); } return executionContext; diff --git a/openbas-front/src/admin/components/simulations/simulation/lessons/Lessons.js b/openbas-front/src/admin/components/simulations/simulation/lessons/Lessons.js index f576784b41..4770417080 100644 --- a/openbas-front/src/admin/components/simulations/simulation/lessons/Lessons.js +++ b/openbas-front/src/admin/components/simulations/simulation/lessons/Lessons.js @@ -550,11 +550,18 @@ const Lessons = () => { {t('Score')} -
+
+ + {answer.lessons_answer_score}% +
diff --git a/openbas-front/src/public/components/lessons/LessonsPlayer.js b/openbas-front/src/public/components/lessons/LessonsPlayer.js index 8df9e495f7..5833413820 100644 --- a/openbas-front/src/public/components/lessons/LessonsPlayer.js +++ b/openbas-front/src/public/components/lessons/LessonsPlayer.js @@ -78,10 +78,11 @@ const LessonsPlayer = () => { ]), ); requiredFields.forEach((field) => { - if (!values[field]) { + if (!values[field] && values[field] !== 0) { errors[field] = t('This field is required.'); } }); + return errors; }; const submitForm = (data) => { @@ -256,6 +257,7 @@ const LessonsPlayer = () => { step={10} min={0} max={100} + defaultValue={0} /> From b597647070f8dcbacb3c8813ffc15f38f958da3c Mon Sep 17 00:00:00 2001 From: Stephanya Casanova Date: Sun, 11 Aug 2024 20:14:53 +0200 Subject: [PATCH 09/13] [frontend] Add validation mode to manual expectations (#184) --- .../java/io/openbas/helper/InjectHelper.java | 8 +- .../io/openbas/importer/V1_DataImporter.java | 2 +- .../injectors/caldera/CalderaContract.java | 4 +- .../challenge/ChallengeContract.java | 14 + .../challenge/ChallengeExecutor.java | 19 +- .../challenge/model/ChallengeContent.java | 4 + .../injectors/channel/ChannelContract.java | 4 +- .../injectors/channel/ChannelExecutor.java | 4 +- .../injectors/email/EmailExecutor.java | 2 +- .../injectors/opencti/OpenCTIExecutor.java | 2 +- .../openbas/injectors/ovh/OvhSmsExecutor.java | 2 +- .../V2_63__InjectExpectation_upgrade.java | 6 +- .../openbas/migration/V3_30__Score_type.java | 19 ++ .../rest/atomic_testing/AtomicTestingApi.java | 7 +- .../openbas/rest/challenge/ChallengeApi.java | 117 +------- .../challenge/output/ChallengeOutput.java | 2 +- .../challenge/response/PublicChallenge.java | 6 +- .../io/openbas/rest/channel/ChannelApi.java | 121 +------- .../exercise/form/ExpectationUpdateInput.java | 2 +- .../io/openbas/rest/helper/TeamHelper.java | 27 +- .../io/openbas/service/ChallengeService.java | 235 ++++++++++----- .../io/openbas/service/ChannelService.java | 129 +++++++++ .../service/ExerciseExpectationService.java | 120 +++++++- .../io/openbas/service/InjectService.java | 6 +- .../io/openbas/utils/AtomicTestingUtils.java | 123 +++++--- .../io/openbas/utils/ExpectationUtils.java | 74 +++++ .../java/io/openbas/utils/ResultUtils.java | 1 + .../injects/email/EmailExecutorTest.java | 2 +- .../ExerciseExpectationServiceTest.java | 4 +- .../io/openbas/service/InjectServiceTest.java | 8 +- .../io/openbas/asset/AssetGroupService.java | 1 - .../io/openbas/atomic_testing/TargetType.java | 1 + .../java/io/openbas/execution/Injector.java | 88 ++++-- .../InjectExpectationService.java | 8 +- .../InjectExpectationUtils.java | 2 +- .../openbas/integrations/PayloadService.java | 4 +- .../java/io/openbas/model/Expectation.java | 3 +- .../expectation/ChallengeExpectation.java | 15 +- .../model/expectation/ChannelExpectation.java | 15 +- .../expectation/DetectionExpectation.java | 10 +- .../model/expectation/ManualExpectation.java | 23 +- .../expectation/PreventionExpectation.java | 10 +- .../model/inject/form/Expectation.java | 2 +- .../atomic_testings/atomic-testing-actions.ts | 7 +- .../atomic_testing/AtomicTesting.tsx | 17 +- .../atomic_testing/AtomicTestingDetail.tsx | 12 +- .../atomic_testing/TargetResultsDetail.tsx | 233 +++++++++------ .../types/nodes/NodeResultStep.tsx | 2 +- .../components/common/injects/ResponsePie.tsx | 3 +- .../injects/expectations/Expectation.ts | 3 +- .../expectations/ExpectationFormCreate.tsx | 10 +- .../expectations/ExpectationFormUpdate.tsx | 10 +- .../expectations/ExpectationFormUtils.ts | 5 +- .../injects/expectations/ExpectationUtils.tsx | 2 +- .../field/ExpectationGroupField.tsx | 16 +- .../validation/common/TeamOrAssetLine.tsx | 30 +- .../expectations/ManualExpectations.tsx | 274 +++++++++++++----- .../ManualExpectationsValidationForm.tsx | 143 +++++---- openbas-front/src/utils/Localization.js | 30 +- openbas-front/src/utils/String.js | 29 ++ openbas-front/src/utils/api-types.d.ts | 2 +- .../io/openbas/database/model/Challenge.java | 2 +- .../io/openbas/database/model/Inject.java | 4 +- .../database/model/InjectExpectation.java | 34 ++- .../model/InjectExpectationResult.java | 2 +- .../java/io/openbas/database/model/Team.java | 16 +- .../io/openbas/database/model/TeamSimple.java | 18 +- .../raw/RawGlobalInjectExpectation.java | 4 +- .../database/raw/RawInjectExpectation.java | 6 +- .../raw/impl/SimpleRawInjectExpectation.java | 5 +- .../repository/ExerciseRepository.java | 4 +- .../InjectExpectationRepository.java | 77 +++-- 72 files changed, 1483 insertions(+), 773 deletions(-) create mode 100644 openbas-api/src/main/java/io/openbas/migration/V3_30__Score_type.java create mode 100644 openbas-api/src/main/java/io/openbas/service/ChannelService.java create mode 100644 openbas-api/src/main/java/io/openbas/utils/ExpectationUtils.java diff --git a/openbas-api/src/main/java/io/openbas/helper/InjectHelper.java b/openbas-api/src/main/java/io/openbas/helper/InjectHelper.java index e9b5ba25f1..eb6e8679df 100644 --- a/openbas-api/src/main/java/io/openbas/helper/InjectHelper.java +++ b/openbas-api/src/main/java/io/openbas/helper/InjectHelper.java @@ -41,7 +41,13 @@ public class InjectHelper { private List getInjectTeams(@NotNull final Inject inject) { Exercise exercise = inject.getExercise(); - return inject.isAllTeams() ? exercise.getTeams() : inject.getTeams(); + if(inject.isAllTeams()) { // In order to process expectations from players, we also need to load players into teams + exercise.getTeams().forEach(team -> Hibernate.initialize(team.getUsers())); + return exercise.getTeams(); + } else { + inject.getTeams().forEach(team -> Hibernate.initialize(team.getUsers())); + return inject.getTeams(); + } } // -- INJECTION -- diff --git a/openbas-api/src/main/java/io/openbas/importer/V1_DataImporter.java b/openbas-api/src/main/java/io/openbas/importer/V1_DataImporter.java index 6201683379..95e4a9fe55 100644 --- a/openbas-api/src/main/java/io/openbas/importer/V1_DataImporter.java +++ b/openbas-api/src/main/java/io/openbas/importer/V1_DataImporter.java @@ -558,7 +558,7 @@ private Challenge createChallenge(JsonNode nodeChallenge, Map base challenge.setName(nodeChallenge.get("challenge_name").textValue()); challenge.setCategory(nodeChallenge.get("challenge_category").textValue()); challenge.setContent(nodeChallenge.get("challenge_content").textValue()); - challenge.setScore(nodeChallenge.get("challenge_score").asInt(0)); + challenge.setScore(nodeChallenge.get("challenge_score").asDouble(0.0)); challenge.setMaxAttempts(nodeChallenge.get("challenge_max_attempts").asInt(0)); challenge.setDocuments( resolveJsonIds(nodeChallenge, "challenge_documents") diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java index a25e60bf09..327c2fc75f 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java +++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaContract.java @@ -87,12 +87,12 @@ private ContractExpectations expectations() { Expectation preventionExpectation = new Expectation(); preventionExpectation.setType(PREVENTION); preventionExpectation.setName("Expect inject to be prevented"); - preventionExpectation.setScore(100); + preventionExpectation.setScore(100.0); // Detection Expectation detectionExpectation = new Expectation(); detectionExpectation.setType(DETECTION); detectionExpectation.setName("Expect inject to be detected"); - detectionExpectation.setScore(100); + detectionExpectation.setScore(100.0); return expectationsField("expectations", "Expectations", List.of(preventionExpectation, detectionExpectation)); } diff --git a/openbas-api/src/main/java/io/openbas/injectors/challenge/ChallengeContract.java b/openbas-api/src/main/java/io/openbas/injectors/challenge/ChallengeContract.java index 12d8f1bc2a..60f91c0a0f 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/challenge/ChallengeContract.java +++ b/openbas-api/src/main/java/io/openbas/injectors/challenge/ChallengeContract.java @@ -6,17 +6,21 @@ import io.openbas.injector_contract.ContractorIcon; import io.openbas.injector_contract.fields.ContractElement; import io.openbas.database.model.Endpoint; +import io.openbas.injector_contract.fields.ContractExpectations; +import io.openbas.model.inject.form.Expectation; import org.springframework.stereotype.Component; import java.io.InputStream; import java.util.List; import java.util.Map; +import static io.openbas.database.model.InjectExpectation.EXPECTATION_TYPE.CHALLENGE; import static io.openbas.injector_contract.Contract.executableContract; import static io.openbas.injector_contract.ContractCardinality.Multiple; import static io.openbas.injector_contract.ContractDef.contractBuilder; import static io.openbas.injector_contract.fields.ContractChallenge.challengeField; import static io.openbas.injector_contract.fields.ContractAttachment.attachmentField; +import static io.openbas.injector_contract.fields.ContractExpectations.expectationsField; import static io.openbas.injector_contract.fields.ContractTeam.teamField; import static io.openbas.injector_contract.fields.ContractCheckbox.checkboxField; import static io.openbas.injector_contract.fields.ContractText.textField; @@ -61,8 +65,18 @@ public List contracts() { Kind regards,
The animation team """; + // We include the expectations for challenges + Expectation expectation = new Expectation(); + expectation.setType(CHALLENGE); + expectation.setName("Expect targets to complete the challenge(s)"); + expectation.setScore(0.0); + ContractExpectations expectationsField = expectationsField( + "expectations", "Expectations", List.of(expectation) + ); List publishInstance = contractBuilder() .mandatory(challengeField("challenges", "Challenges", Multiple)) + // Contract specific + .optional(expectationsField) .mandatory(textField("subject", "Subject", "New challenges published for ${user.email}")) .mandatory(richTextareaField("body", "Body", messageBody)) .optional(checkboxField("encrypted", "Encrypted", false)) diff --git a/openbas-api/src/main/java/io/openbas/injectors/challenge/ChallengeExecutor.java b/openbas-api/src/main/java/io/openbas/injectors/challenge/ChallengeExecutor.java index 0422f5debf..1c7b7c2eb6 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/challenge/ChallengeExecutor.java +++ b/openbas-api/src/main/java/io/openbas/injectors/challenge/ChallengeExecutor.java @@ -12,7 +12,7 @@ import io.openbas.model.ExecutionProcess; import io.openbas.model.Expectation; import io.openbas.model.expectation.ChallengeExpectation; -import io.openbas.rest.exception.ElementNotFoundException; +import io.openbas.model.expectation.ManualExpectation; import jakarta.annotation.Resource; import jakarta.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Autowired; @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; import static io.openbas.database.model.InjectStatusExecution.traceError; import static io.openbas.database.model.InjectStatusExecution.traceSuccess; @@ -102,7 +103,21 @@ public ExecutionProcess process(@NotNull final Execution execution, @NotNull fin }); // Return expectations List expectations = new ArrayList<>(); - challenges.forEach(challenge -> expectations.add(new ChallengeExpectation(challenge.getScore(), challenge))); + if (!content.getExpectations().isEmpty()) { + expectations.addAll( + content.getExpectations() + .stream() + .flatMap((entry) -> switch (entry.getType()) { + case MANUAL -> Stream.of( + (Expectation) new ManualExpectation(entry.getScore(), entry.getName(), entry.getDescription(), entry.isExpectationGroup()) + ); + case CHALLENGE -> challenges.stream() + .map(challenge -> (Expectation) new ChallengeExpectation(entry.getScore(), challenge, entry.isExpectationGroup())); + default -> Stream.of(); + }) + .toList() + ); + } return new ExecutionProcess(false, expectations); } else { throw new UnsupportedOperationException("Unknown contract " + contract); diff --git a/openbas-api/src/main/java/io/openbas/injectors/challenge/model/ChallengeContent.java b/openbas-api/src/main/java/io/openbas/injectors/challenge/model/ChallengeContent.java index 48f13e34b1..6b58cfe16a 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/challenge/model/ChallengeContent.java +++ b/openbas-api/src/main/java/io/openbas/injectors/challenge/model/ChallengeContent.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.openbas.injectors.email.model.EmailContent; +import io.openbas.model.inject.form.Expectation; import lombok.Getter; import lombok.Setter; @@ -15,4 +16,7 @@ public class ChallengeContent extends EmailContent { @JsonProperty("challenges") private List challenges = new ArrayList<>(); + @JsonProperty("expectations") + private List expectations = new ArrayList<>(); + } diff --git a/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelContract.java b/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelContract.java index c17da0b546..70ade09f5b 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelContract.java +++ b/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelContract.java @@ -74,8 +74,8 @@ public List contracts() { ContractCheckbox emailingField = checkboxField("emailing", "Send email", true); Expectation expectation = new Expectation(); expectation.setType(ARTICLE); - expectation.setName("Expect teams to read the article(s)"); - expectation.setScore(0); + expectation.setName("Expect targets to read the article(s)"); + expectation.setScore(0.0); ContractExpectations expectationsField = expectationsField( "expectations", "Expectations", List.of(expectation) ); diff --git a/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelExecutor.java b/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelExecutor.java index 3469e4bb9d..5aab36bc42 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelExecutor.java +++ b/openbas-api/src/main/java/io/openbas/injectors/channel/ChannelExecutor.java @@ -118,10 +118,10 @@ public ExecutionProcess process(@NotNull final Execution execution, @NotNull fin .stream() .flatMap((entry) -> switch (entry.getType()) { case MANUAL -> Stream.of( - (Expectation) new ManualExpectation(entry.getScore(), entry.getName(), entry.getDescription()) + (Expectation) new ManualExpectation(entry.getScore(), entry.getName(), entry.getDescription(), entry.isExpectationGroup()) ); case ARTICLE -> articles.stream() - .map(article -> (Expectation) new ChannelExpectation(entry.getScore(), article)); + .map(article -> (Expectation) new ChannelExpectation(entry.getScore(), article, entry.isExpectationGroup())); default -> Stream.of(); }) .toList() diff --git a/openbas-api/src/main/java/io/openbas/injectors/email/EmailExecutor.java b/openbas-api/src/main/java/io/openbas/injectors/email/EmailExecutor.java index d159a36e12..dcc202ee77 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/email/EmailExecutor.java +++ b/openbas-api/src/main/java/io/openbas/injectors/email/EmailExecutor.java @@ -86,7 +86,7 @@ public ExecutionProcess process(@NotNull final Execution execution, @NotNull fin .stream() .flatMap((entry) -> switch (entry.getType()) { case MANUAL -> - Stream.of((Expectation) new ManualExpectation(entry.getScore(), entry.getName(), entry.getDescription())); + Stream.of((Expectation) new ManualExpectation(entry.getScore(), entry.getName(), entry.getDescription(), entry.isExpectationGroup())); default -> Stream.of(); }) .toList(); diff --git a/openbas-api/src/main/java/io/openbas/injectors/opencti/OpenCTIExecutor.java b/openbas-api/src/main/java/io/openbas/injectors/opencti/OpenCTIExecutor.java index a5f72ff340..8cc96b176a 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/opencti/OpenCTIExecutor.java +++ b/openbas-api/src/main/java/io/openbas/injectors/opencti/OpenCTIExecutor.java @@ -65,7 +65,7 @@ public ExecutionProcess process(@NotNull final Execution execution, @NotNull fin .stream() .flatMap((entry) -> switch (entry.getType()) { case MANUAL -> - Stream.of((Expectation) new ManualExpectation(entry.getScore(), entry.getName(), entry.getDescription())); + Stream.of((Expectation) new ManualExpectation(entry.getScore(), entry.getName(), entry.getDescription(), entry.isExpectationGroup())); default -> Stream.of(); }) .toList(); diff --git a/openbas-api/src/main/java/io/openbas/injectors/ovh/OvhSmsExecutor.java b/openbas-api/src/main/java/io/openbas/injectors/ovh/OvhSmsExecutor.java index e0fd99617f..1faf2ee3e7 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/ovh/OvhSmsExecutor.java +++ b/openbas-api/src/main/java/io/openbas/injectors/ovh/OvhSmsExecutor.java @@ -66,7 +66,7 @@ public ExecutionProcess process(@NotNull final Execution execution, @NotNull fin .stream() .flatMap(entry -> switch (entry.getType()) { case MANUAL -> - Stream.of((Expectation) new ManualExpectation(entry.getScore(), entry.getName(), entry.getDescription())); + Stream.of((Expectation) new ManualExpectation(entry.getScore(), entry.getName(), entry.getDescription(), entry.isExpectationGroup())); default -> Stream.of(); }) .toList(); diff --git a/openbas-api/src/main/java/io/openbas/migration/V2_63__InjectExpectation_upgrade.java b/openbas-api/src/main/java/io/openbas/migration/V2_63__InjectExpectation_upgrade.java index 347af4935e..c31584f4d3 100644 --- a/openbas-api/src/main/java/io/openbas/migration/V2_63__InjectExpectation_upgrade.java +++ b/openbas-api/src/main/java/io/openbas/migration/V2_63__InjectExpectation_upgrade.java @@ -72,7 +72,7 @@ public static class OvhSmsContentOld { private String message; private String expectationType; - private Integer expectationScore; + private Double expectationScore; OvhSmsContentNew toNewContent() { OvhSmsContentNew content = new OvhSmsContentNew(); @@ -99,7 +99,7 @@ public static class EmailContentOld { private String inReplyTo; private boolean encrypted; private String expectationType; - private Integer expectationScore; + private Double expectationScore; EmailContent toNewContent() { EmailContent content = new EmailContent(); @@ -122,7 +122,7 @@ public static class MediaContentOld extends EmailContentOld { private List articles; private boolean expectation; - private Integer expectationScore; + private Double expectationScore; private boolean emailing; ChannelContent toNewContent() { diff --git a/openbas-api/src/main/java/io/openbas/migration/V3_30__Score_type.java b/openbas-api/src/main/java/io/openbas/migration/V3_30__Score_type.java new file mode 100644 index 0000000000..2a2fa73d4e --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/migration/V3_30__Score_type.java @@ -0,0 +1,19 @@ +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.Statement; + +@Component +public class V3_30__Score_type extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + Statement select = context.getConnection().createStatement(); + select.execute("ALTER TABLE challenges alter column challenge_score type DOUBLE PRECISION;"); + select.execute("ALTER TABLE injects_expectations alter column inject_expectation_score type DOUBLE PRECISION;"); + select.execute("ALTER TABLE injects_expectations alter column inject_expectation_expected_score type DOUBLE PRECISION;"); + } +} diff --git a/openbas-api/src/main/java/io/openbas/rest/atomic_testing/AtomicTestingApi.java b/openbas-api/src/main/java/io/openbas/rest/atomic_testing/AtomicTestingApi.java index 6f6c551c7b..7a4aebf273 100644 --- a/openbas-api/src/main/java/io/openbas/rest/atomic_testing/AtomicTestingApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/atomic_testing/AtomicTestingApi.java @@ -73,10 +73,11 @@ public Inject tryAtomicTesting(@PathVariable String injectId) { @GetMapping("/{injectId}/target_results/{targetId}/types/{targetType}") public List findTargetResult( - @PathVariable String targetId, @PathVariable String injectId, - @PathVariable String targetType) { - return injectExpectationService.findExpectationsByInjectAndTargetAndTargetType(injectId, targetId, targetType); + @PathVariable String targetId, + @PathVariable String targetType, + @RequestParam(required = false) String parentTargetId ) { + return injectExpectationService.findExpectationsByInjectAndTargetAndTargetType(injectId, targetId, parentTargetId, targetType); } @PutMapping("/{injectId}/tags") diff --git a/openbas-api/src/main/java/io/openbas/rest/challenge/ChallengeApi.java b/openbas-api/src/main/java/io/openbas/rest/challenge/ChallengeApi.java index 63b29398b5..3e456643d4 100644 --- a/openbas-api/src/main/java/io/openbas/rest/challenge/ChallengeApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/challenge/ChallengeApi.java @@ -1,7 +1,10 @@ package io.openbas.rest.challenge; -import io.openbas.database.model.*; +import io.openbas.database.model.Challenge; +import io.openbas.database.model.ChallengeFlag; import io.openbas.database.model.ChallengeFlag.FLAG_TYPE; +import io.openbas.database.model.Exercise; +import io.openbas.database.model.User; import io.openbas.database.repository.*; import io.openbas.rest.challenge.form.ChallengeCreateInput; import io.openbas.rest.challenge.form.ChallengeTryInput; @@ -14,16 +17,14 @@ import io.openbas.service.ChallengeService; import jakarta.transaction.Transactional; import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.AllArgsConstructor; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.time.Instant; -import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.regex.Pattern; import static io.openbas.config.OpenBASAnonymous.ANONYMOUS; import static io.openbas.database.model.User.ROLE_ADMIN; @@ -31,6 +32,7 @@ import static io.openbas.helper.StreamHelper.iterableToSet; @RestController +@AllArgsConstructor public class ChallengeApi extends RestBehavior { private ChallengeRepository challengeRepository; @@ -38,50 +40,9 @@ public class ChallengeApi extends RestBehavior { private TagRepository tagRepository; private DocumentRepository documentRepository; private ExerciseRepository exerciseRepository; - private InjectExpectationRepository injectExpectationRepository; private ChallengeService challengeService; private UserRepository userRepository; - @Autowired - public void setUserRepository(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Autowired - public void setChallengeService(ChallengeService challengeService) { - this.challengeService = challengeService; - } - - @Autowired - public void setInjectExpectationRepository(InjectExpectationRepository injectExpectationRepository) { - this.injectExpectationRepository = injectExpectationRepository; - } - - @Autowired - public void setChallengeFlagRepository(ChallengeFlagRepository challengeFlagRepository) { - this.challengeFlagRepository = challengeFlagRepository; - } - - @Autowired - public void setChallengeRepository(ChallengeRepository challengeRepository) { - this.challengeRepository = challengeRepository; - } - - @Autowired - public void setTagRepository(TagRepository tagRepository) { - this.tagRepository = tagRepository; - } - - @Autowired - public void setDocumentRepository(DocumentRepository documentRepository) { - this.documentRepository = documentRepository; - } - - @Autowired - public void setExerciseRepository(ExerciseRepository exerciseRepository) { - this.exerciseRepository = exerciseRepository; - } - @GetMapping("/api/challenges") public Iterable challenges() { return fromIterable(challengeRepository.findAll()).stream() @@ -136,25 +97,11 @@ public Challenge createChallenge(@Valid @RequestBody ChallengeCreateInput input) @GetMapping("/api/player/challenges/{exerciseId}") public ChallengesReader playerChallenges(@PathVariable String exerciseId, @RequestParam Optional userId) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(ElementNotFoundException::new); final User user = impersonateUser(userRepository, userId); if (user.getId().equals(ANONYMOUS)) { throw new UnsupportedOperationException("User must be logged or dynamic player is required"); } - ChallengesReader reader = new ChallengesReader(exercise); - List teamIds = user.getTeams().stream().map(Team::getId).toList(); - List challengeExpectations = injectExpectationRepository.findChallengeExpectations(exerciseId, - teamIds); - List challenges = challengeExpectations.stream() - .map(injectExpectation -> { - Challenge challenge = injectExpectation.getChallenge(); - challenge.setVirtualPublication(injectExpectation.getCreatedAt()); - return new ChallengeInformation(challenge, injectExpectation); - }) - .sorted(Comparator.comparing(o -> o.getChallenge().getVirtualPublication())) - .toList(); - reader.setExerciseChallenges(challenges); - return reader; + return challengeService.playerChallenges(exerciseId, user); } @GetMapping("/api/observer/challenges/{exerciseId}") @@ -174,32 +121,9 @@ public void deleteChallenge(@PathVariable String challengeId) { challengeRepository.deleteById(challengeId); } - private boolean checkFlag(ChallengeFlag flag, String value) { - switch (flag.getType()) { - case VALUE -> { - return value.equalsIgnoreCase(flag.getValue()); - } - case VALUE_CASE -> { - return value.equals(flag.getValue()); - } - case REGEXP -> { - return Pattern.compile(flag.getValue()).matcher(value).matches(); - } - default -> { - return false; - } - } - } - @PostMapping("/api/challenges/{challengeId}/try") public ChallengeResult tryChallenge(@PathVariable String challengeId, @Valid @RequestBody ChallengeTryInput input) { - Challenge challenge = challengeRepository.findById(challengeId).orElseThrow(ElementNotFoundException::new); - for (ChallengeFlag flag : challenge.getFlags()) { - if (checkFlag(flag, input.getValue())) { - return new ChallengeResult(true); - } - } - return new ChallengeResult(false); + return challengeService.tryChallenge(challengeId, input); } @PostMapping("/api/player/challenges/{exerciseId}/{challengeId}/validate") @@ -212,29 +136,6 @@ public ChallengesReader validateChallenge(@PathVariable String exerciseId, if (user.getId().equals(ANONYMOUS)) { throw new UnsupportedOperationException("User must be logged or dynamic player is required"); } - ChallengeResult challengeResult = tryChallenge(challengeId, input); - if (challengeResult.getResult()) { - List teamIds = user.getTeams().stream().map(Team::getId).toList(); - List challengeExpectations = injectExpectationRepository.findChallengeExpectations(exerciseId, - teamIds, challengeId); - challengeExpectations.forEach(injectExpectationExecution -> { - injectExpectationExecution.setUser(user); - injectExpectationExecution.setResults(List.of( - InjectExpectationResult.builder() - .sourceId("challenge") - .sourceType("challenge") - .sourceName("Challenge validation") - .result(Instant.now().toString()) - .date(Instant.now().toString()) - .score(injectExpectationExecution.getExpectedScore()) - .build() - )); - injectExpectationExecution.setScore(injectExpectationExecution.getExpectedScore()); - injectExpectationExecution.setUpdatedAt(Instant.now()); - injectExpectationRepository.save(injectExpectationExecution); - }); - } - return playerChallenges(exerciseId, userId); + return challengeService.validateChallenge(exerciseId, challengeId, input, user); } - } diff --git a/openbas-api/src/main/java/io/openbas/rest/challenge/output/ChallengeOutput.java b/openbas-api/src/main/java/io/openbas/rest/challenge/output/ChallengeOutput.java index 1298450e57..4a522d5128 100644 --- a/openbas-api/src/main/java/io/openbas/rest/challenge/output/ChallengeOutput.java +++ b/openbas-api/src/main/java/io/openbas/rest/challenge/output/ChallengeOutput.java @@ -32,7 +32,7 @@ public class ChallengeOutput { private String content; @JsonProperty("challenge_score") - private Integer score; + private Double score; @JsonProperty("challenge_max_attempts") private Integer maxAttempts; diff --git a/openbas-api/src/main/java/io/openbas/rest/challenge/response/PublicChallenge.java b/openbas-api/src/main/java/io/openbas/rest/challenge/response/PublicChallenge.java index 17af32367c..c5b65c192a 100644 --- a/openbas-api/src/main/java/io/openbas/rest/challenge/response/PublicChallenge.java +++ b/openbas-api/src/main/java/io/openbas/rest/challenge/response/PublicChallenge.java @@ -23,7 +23,7 @@ public class PublicChallenge { private String content; @JsonProperty("challenge_score") - private Integer score; + private Double score; @JsonProperty("challenge_flags") private List flags; @@ -85,11 +85,11 @@ public void setContent(String content) { this.content = content; } - public Integer getScore() { + public Double getScore() { return score; } - public void setScore(Integer score) { + public void setScore(Double score) { this.score = score; } diff --git a/openbas-api/src/main/java/io/openbas/rest/channel/ChannelApi.java b/openbas-api/src/main/java/io/openbas/rest/channel/ChannelApi.java index 740b93470b..6ea6429b96 100644 --- a/openbas-api/src/main/java/io/openbas/rest/channel/ChannelApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/channel/ChannelApi.java @@ -1,35 +1,33 @@ package io.openbas.rest.channel; -import com.fasterxml.jackson.core.JsonProcessingException; import io.openbas.database.model.*; import io.openbas.database.repository.*; -import io.openbas.injectors.channel.model.ChannelContent; import io.openbas.rest.channel.form.*; -import io.openbas.rest.channel.model.VirtualArticle; import io.openbas.rest.channel.response.ChannelReader; import io.openbas.rest.exception.ElementNotFoundException; import io.openbas.rest.helper.RestBehavior; +import io.openbas.service.ChannelService; import io.openbas.service.ScenarioService; import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.AllArgsConstructor; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.time.Instant; -import java.util.*; -import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import static io.openbas.config.OpenBASAnonymous.ANONYMOUS; import static io.openbas.database.model.User.ROLE_ADMIN; -import static io.openbas.helper.StreamHelper.fromIterable; -import static io.openbas.injectors.channel.ChannelContract.CHANNEL_PUBLISH; import static io.openbas.rest.channel.ChannelHelper.enrichArticleWithVirtualPublication; import static io.openbas.rest.scenario.ScenarioApi.SCENARIO_URI; @RestController +@AllArgsConstructor public class ChannelApi extends RestBehavior { private ExerciseRepository exerciseRepository; @@ -37,44 +35,8 @@ public class ChannelApi extends RestBehavior { private ArticleRepository articleRepository; private ChannelRepository channelRepository; private DocumentRepository documentRepository; - private InjectExpectationRepository injectExpectationExecutionRepository; private UserRepository userRepository; - - @Autowired - public void setUserRepository(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Autowired - public void setArticleRepository(ArticleRepository articleRepository) { - this.articleRepository = articleRepository; - } - - @Autowired - public void setInjectExpectationExecutionRepository( - InjectExpectationRepository injectExpectationExecutionRepository) { - this.injectExpectationExecutionRepository = injectExpectationExecutionRepository; - } - - @Autowired - public void setChannelRepository(ChannelRepository channelRepository) { - this.channelRepository = channelRepository; - } - - @Autowired - public void setExerciseRepository(ExerciseRepository exerciseRepository) { - this.exerciseRepository = exerciseRepository; - } - - @Autowired - public void setScenarioService(ScenarioService scenarioService) { - this.scenarioService = scenarioService; - } - - @Autowired - public void setDocumentRepository(DocumentRepository documentRepository) { - this.documentRepository = documentRepository; - } + private ChannelService channelService; // -- CHANNELS -- @@ -157,78 +119,11 @@ public ChannelReader playerArticles( @PathVariable String exerciseId, @PathVariable String channelId, @RequestParam Optional userId) { - ChannelReader channelReader; - Channel channel = channelRepository.findById(channelId).orElseThrow(ElementNotFoundException::new); - List injects; - - Optional exerciseOpt = exerciseRepository.findById(exerciseId); - if (exerciseOpt.isPresent()) { - Exercise exercise = exerciseOpt.get(); - channelReader = new ChannelReader(channel, exercise); - injects = exercise.getInjects(); - } else { - Scenario scenario = this.scenarioService.scenario(exerciseId); - channelReader = new ChannelReader(channel, scenario); - injects = scenario.getInjects(); - } - final User user = impersonateUser(userRepository, userId); if (user.getId().equals(ANONYMOUS)) { throw new UnsupportedOperationException("User must be logged or dynamic player is required"); } - Map toPublishArticleIdsMap = injects.stream() - .filter(inject -> inject.getInjectorContract() - .map(contract -> contract.getId().equals(CHANNEL_PUBLISH)) - .orElse(false)) - .filter(inject -> inject.getStatus().isPresent()) - .sorted(Comparator.comparing(inject -> inject.getStatus().get().getTrackingSentDate())) - .flatMap(inject -> { - Instant virtualInjectDate = inject.getStatus().get().getTrackingSentDate(); - try { - ChannelContent content = mapper.treeToValue(inject.getContent(), ChannelContent.class); - if (content.getArticles() != null) { - return content.getArticles().stream().map(article -> new VirtualArticle(virtualInjectDate, article)); - } - return null; - } catch (JsonProcessingException e) { - // Invalid channel content. - return null; - } - }) - .filter(Objects::nonNull) - .distinct() - .collect(Collectors.toMap(VirtualArticle::id, VirtualArticle::date)); - if (!toPublishArticleIdsMap.isEmpty()) { - List
publishedArticles = fromIterable(articleRepository.findAllById(toPublishArticleIdsMap.keySet())) - .stream().filter(article -> article.getChannel().equals(channel)) - .peek(article -> article.setVirtualPublication(toPublishArticleIdsMap.get(article.getId()))) - .sorted(Comparator.comparing(Article::getVirtualPublication).reversed()) - .toList(); - channelReader.setChannelArticles(publishedArticles); - // Fulfill article expectations - List finalInjects = injects; - List expectationExecutions = publishedArticles.stream() - .flatMap(article -> finalInjects.stream() - .flatMap(inject -> inject.getUserExpectationsForArticle(user, article).stream())) - .filter(exec -> exec.getResults().isEmpty()).toList(); - expectationExecutions.forEach(injectExpectationExecution -> { - injectExpectationExecution.setUser(user); - injectExpectationExecution.setResults(List.of( - InjectExpectationResult.builder() - .sourceId("media-pressure") - .sourceType("media-pressure") - .sourceName("Media pressure read") - .result(Instant.now().toString()) - .date(Instant.now().toString()) - .score(injectExpectationExecution.getExpectedScore()) - .build() - )); - injectExpectationExecution.setScore(injectExpectationExecution.getExpectedScore()); - injectExpectationExecution.setUpdatedAt(Instant.now()); - injectExpectationExecutionRepository.save(injectExpectationExecution); - }); - } - return channelReader; + return channelService.validateArticles(exerciseId, channelId, user); } // -- EXERCISES -- diff --git a/openbas-api/src/main/java/io/openbas/rest/exercise/form/ExpectationUpdateInput.java b/openbas-api/src/main/java/io/openbas/rest/exercise/form/ExpectationUpdateInput.java index 0a713b61df..7064c6163f 100644 --- a/openbas-api/src/main/java/io/openbas/rest/exercise/form/ExpectationUpdateInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/exercise/form/ExpectationUpdateInput.java @@ -24,5 +24,5 @@ public class ExpectationUpdateInput { @JsonProperty("expectation_score") @NotNull - private Integer score; + private Double score; } diff --git a/openbas-api/src/main/java/io/openbas/rest/helper/TeamHelper.java b/openbas-api/src/main/java/io/openbas/rest/helper/TeamHelper.java index cf91cf3882..9d757b21c2 100644 --- a/openbas-api/src/main/java/io/openbas/rest/helper/TeamHelper.java +++ b/openbas-api/src/main/java/io/openbas/rest/helper/TeamHelper.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -55,19 +56,21 @@ public static List rawTeamToSimplerTeam(List teams, rawTeam.getTeam_expectations().stream().map( expectation -> { // We set the inject expectation using the map we generated earlier - RawInjectExpectation raw = mapInjectExpectation.get(expectation); InjectExpectation injectExpectation = new InjectExpectation(); - injectExpectation.setScore(raw.getInject_expectation_score()); - injectExpectation.setExpectedScore(raw.getInject_expectation_expected_score()); - injectExpectation.setId(raw.getInject_expectation_id()); - injectExpectation.setExpectedScore(raw.getInject_expectation_expected_score()); - if(raw.getExercise_id() != null) { - injectExpectation.setExercise(new Exercise()); - injectExpectation.getExercise().setId(raw.getExercise_id()); - } - injectExpectation.setTeam(new Team()); - injectExpectation.getTeam().setId(rawTeam.getTeam_id()); - injectExpectation.setType(InjectExpectation.EXPECTATION_TYPE.valueOf(raw.getInject_expectation_type())); + Optional raw = Optional.ofNullable(mapInjectExpectation.get(expectation)); + raw.ifPresent(toProcess -> { + injectExpectation.setScore(toProcess.getInject_expectation_score()); + injectExpectation.setExpectedScore(toProcess.getInject_expectation_expected_score()); + injectExpectation.setId(toProcess.getInject_expectation_id()); + injectExpectation.setExpectedScore(toProcess.getInject_expectation_expected_score()); + if (toProcess.getExercise_id() != null) { + injectExpectation.setExercise(new Exercise()); + injectExpectation.getExercise().setId(toProcess.getExercise_id()); + } + injectExpectation.setTeam(new Team()); + injectExpectation.getTeam().setId(rawTeam.getTeam_id()); + injectExpectation.setType(InjectExpectation.EXPECTATION_TYPE.valueOf(toProcess.getInject_expectation_type())); + }); return injectExpectation; } ).toList() diff --git a/openbas-api/src/main/java/io/openbas/service/ChallengeService.java b/openbas-api/src/main/java/io/openbas/service/ChallengeService.java index 15d6f81fdc..8400f5c24d 100644 --- a/openbas-api/src/main/java/io/openbas/service/ChallengeService.java +++ b/openbas-api/src/main/java/io/openbas/service/ChallengeService.java @@ -2,91 +2,186 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.openbas.database.model.Challenge; -import io.openbas.database.model.Exercise; -import io.openbas.database.model.Inject; -import io.openbas.database.model.Scenario; +import io.openbas.database.model.*; import io.openbas.database.repository.ChallengeRepository; import io.openbas.database.repository.ExerciseRepository; +import io.openbas.database.repository.InjectExpectationRepository; import io.openbas.database.repository.InjectRepository; import io.openbas.injectors.challenge.model.ChallengeContent; +import io.openbas.rest.challenge.form.ChallengeTryInput; +import io.openbas.rest.challenge.response.ChallengeInformation; +import io.openbas.rest.challenge.response.ChallengeResult; +import io.openbas.rest.challenge.response.ChallengesReader; +import io.openbas.rest.exception.ElementNotFoundException; +import io.openbas.utils.ExpectationUtils; import jakarta.annotation.Resource; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; -import java.util.List; +import java.time.Instant; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; import static io.openbas.helper.StreamHelper.fromIterable; import static io.openbas.injectors.challenge.ChallengeContract.CHALLENGE_PUBLISH; @Service +@AllArgsConstructor public class ChallengeService { - @Resource - protected ObjectMapper mapper; - - private ExerciseRepository exerciseRepository; - private ChallengeRepository challengeRepository; - private InjectRepository injectRepository; - - @Autowired - public void setInjectRepository(InjectRepository injectRepository) { - this.injectRepository = injectRepository; - } - - @Autowired - public void setChallengeRepository(ChallengeRepository challengeRepository) { - this.challengeRepository = challengeRepository; - } - - @Autowired - public void setExerciseRepository(ExerciseRepository exerciseRepository) { - this.exerciseRepository = exerciseRepository; - } - - public Challenge enrichChallengeWithExercisesOrScenarios(@NotNull Challenge challenge) { - List injects = fromIterable(this.injectRepository.findAllForChallengeId("%" + challenge.getId() + "%")); - List exerciseIds = injects.stream().filter(i -> i.getExercise() != null).map(i -> i.getExercise().getId()).distinct().toList(); - challenge.setExerciseIds(exerciseIds); - List scenarioIds = injects.stream().filter(i -> i.getScenario() != null).map(i -> i.getScenario().getId()).distinct().toList(); - challenge.setScenarioIds(scenarioIds); - return challenge; - } - - public Iterable getExerciseChallenges(@NotBlank final String exerciseId) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - return resolveChallenges(exercise.getInjects()) - .map(this::enrichChallengeWithExercisesOrScenarios) - .toList(); - } - - public Iterable getScenarioChallenges(@NotNull final Scenario scenario) { - return resolveChallenges(scenario.getInjects()) - .map(this::enrichChallengeWithExercisesOrScenarios) - .toList(); - } - - // -- PRIVATE -- - private Stream resolveChallenges(@NotNull final List injects) { - List challenges = injects.stream() - .filter(inject -> inject.getInjectorContract() - .map(contract -> contract.getId().equals(CHALLENGE_PUBLISH)) - .orElse(false)) - .filter(inject -> inject.getContent() != null) - .flatMap(inject -> { - try { - ChallengeContent content = mapper.treeToValue(inject.getContent(), ChallengeContent.class); - return content.getChallenges().stream(); - } catch (JsonProcessingException e) { - return Stream.empty(); - } - }) - .distinct() - .toList(); - - return fromIterable(this.challengeRepository.findAllById(challenges)).stream(); - } + @Resource + protected ObjectMapper mapper; + + private ExerciseRepository exerciseRepository; + private ChallengeRepository challengeRepository; + private InjectRepository injectRepository; + private InjectExpectationRepository injectExpectationRepository; + + public Challenge enrichChallengeWithExercisesOrScenarios(@NotNull Challenge challenge) { + List injects = fromIterable(this.injectRepository.findAllForChallengeId("%" + challenge.getId() + "%")); + List exerciseIds = injects.stream().filter(i -> i.getExercise() != null).map(i -> i.getExercise().getId()).distinct().toList(); + challenge.setExerciseIds(exerciseIds); + List scenarioIds = injects.stream().filter(i -> i.getScenario() != null).map(i -> i.getScenario().getId()).distinct().toList(); + challenge.setScenarioIds(scenarioIds); + return challenge; + } + + public Iterable getExerciseChallenges(@NotBlank final String exerciseId) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + return resolveChallenges(exercise.getInjects()) + .map(this::enrichChallengeWithExercisesOrScenarios) + .toList(); + } + + public Iterable getScenarioChallenges(@NotNull final Scenario scenario) { + return resolveChallenges(scenario.getInjects()) + .map(this::enrichChallengeWithExercisesOrScenarios) + .toList(); + } + + + public ChallengeResult tryChallenge(String challengeId, ChallengeTryInput input) { + Challenge challenge = challengeRepository.findById(challengeId).orElseThrow(ElementNotFoundException::new); + for (ChallengeFlag flag : challenge.getFlags()) { + if (checkFlag(flag, input.getValue())) { + return new ChallengeResult(true); + } + } + return new ChallengeResult(false); + } + + public ChallengesReader playerChallenges(String exerciseId, User user) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(ElementNotFoundException::new); + ChallengesReader reader = new ChallengesReader(exercise); + List challengeExpectations = injectExpectationRepository.findChallengeExpectationsByExerciseAndUser(exerciseId, user.getId()); + + // Filter expectations by unique challenge + Set seenChallenges = new HashSet<>(); + List distinctExpectations = new ArrayList<>(); + + for (InjectExpectation expectation : challengeExpectations) { + String challengeId = expectation.getChallenge().getId(); + if (!seenChallenges.contains(challengeId)) { + seenChallenges.add(challengeId); + distinctExpectations.add(expectation); + } + } + + List challenges = distinctExpectations.stream() + .map(injectExpectation -> { + Challenge challenge = injectExpectation.getChallenge(); + challenge.setVirtualPublication(injectExpectation.getCreatedAt()); + return new ChallengeInformation(challenge, injectExpectation); + }) + .sorted(Comparator.comparing(o -> o.getChallenge().getVirtualPublication())) + .toList(); + reader.setExerciseChallenges(challenges); + return reader; + } + + public ChallengesReader validateChallenge(String exerciseId, + String challengeId, + ChallengeTryInput input, + User user) { + ChallengeResult challengeResult = tryChallenge(challengeId, input); + if (challengeResult.getResult()) { + // Find and update the expectations linked to the user + List playerExpectations = injectExpectationRepository.findByUserAndExerciseAndChallenge(user.getId(), exerciseId, challengeId); + playerExpectations.forEach(playerExpectation -> { + playerExpectation.setScore(playerExpectation.getExpectedScore()); + InjectExpectationResult expectationResult = InjectExpectationResult.builder() + .sourceId("challenge") + .sourceType("challenge") + .sourceName("Challenge validation") + .result(Instant.now().toString()) + .date(Instant.now().toString()) + .score(playerExpectation.getExpectedScore()) + .build(); + playerExpectation.getResults().add(expectationResult); + playerExpectation.setUpdatedAt(Instant.now()); + injectExpectationRepository.save(playerExpectation); + }); + + // -- VALIDATION TYPE -- + processByValidationType(exerciseId, challengeId, user, true); + } + return playerChallenges(exerciseId, user); + } + + private void processByValidationType(String exerciseId, String challengeId, User user, boolean isaNewExpectationResult) { + // Process expectations linked to teams where the user is a member + List teamIds = user.getTeams().stream().map(Team::getId).toList(); + // Find all expectations for this exercise, challenge and teams + List challengeExpectations = injectExpectationRepository.findChallengeExpectations(exerciseId, teamIds, challengeId); + // If user is null then expectation is from a team + List parentExpectations = challengeExpectations.stream().filter(exp -> exp.getUser() == null).toList(); + // If user is not null then expectation is from a player + Map> playerByTeam = challengeExpectations.stream().filter(exp -> exp.getUser() != null).collect(Collectors.groupingBy(InjectExpectation::getTeam)); + + // Depending on type of validation, We process the parent expectations: + List toUpdate = ExpectationUtils.processByValidationType(isaNewExpectationResult, challengeExpectations, parentExpectations, playerByTeam); + injectExpectationRepository.saveAll(toUpdate); + } + + // -- PRIVATE -- + private Stream resolveChallenges(@NotNull final List injects) { + List challenges = injects.stream() + .filter(inject -> inject.getInjectorContract() + .map(contract -> contract.getId().equals(CHALLENGE_PUBLISH)) + .orElse(false)) + .filter(inject -> inject.getContent() != null) + .flatMap(inject -> { + try { + ChallengeContent content = mapper.treeToValue(inject.getContent(), ChallengeContent.class); + return content.getChallenges().stream(); + } catch (JsonProcessingException e) { + return Stream.empty(); + } + }) + .distinct() + .toList(); + + return fromIterable(this.challengeRepository.findAllById(challenges)).stream(); + } + + private boolean checkFlag(ChallengeFlag flag, String value) { + switch (flag.getType()) { + case VALUE -> { + return value.equalsIgnoreCase(flag.getValue()); + } + case VALUE_CASE -> { + return value.equals(flag.getValue()); + } + case REGEXP -> { + return Pattern.compile(flag.getValue()).matcher(value).matches(); + } + default -> { + return false; + } + } + } } diff --git a/openbas-api/src/main/java/io/openbas/service/ChannelService.java b/openbas-api/src/main/java/io/openbas/service/ChannelService.java new file mode 100644 index 0000000000..3b3e2b6e4f --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/service/ChannelService.java @@ -0,0 +1,129 @@ +package io.openbas.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.openbas.database.model.*; +import io.openbas.database.repository.ArticleRepository; +import io.openbas.database.repository.ChannelRepository; +import io.openbas.database.repository.ExerciseRepository; +import io.openbas.database.repository.InjectExpectationRepository; +import io.openbas.injectors.channel.model.ChannelContent; +import io.openbas.rest.channel.model.VirtualArticle; +import io.openbas.rest.channel.response.ChannelReader; +import io.openbas.rest.exception.ElementNotFoundException; +import io.openbas.utils.ExpectationUtils; +import jakarta.annotation.Resource; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +import static io.openbas.helper.StreamHelper.fromIterable; +import static io.openbas.injectors.channel.ChannelContract.CHANNEL_PUBLISH; + +@Service +@AllArgsConstructor +public class ChannelService { + + @Resource + protected ObjectMapper mapper; + + private InjectExpectationRepository injectExpectationExecutionRepository; + private ExerciseRepository exerciseRepository; + private ScenarioService scenarioService; + private ArticleRepository articleRepository; + private ChannelRepository channelRepository; + + + public ChannelReader validateArticles(String exerciseId, String channelId, User user) { + ChannelReader channelReader; + Channel channel = channelRepository.findById(channelId).orElseThrow(ElementNotFoundException::new); + List injects; + + Optional exerciseOpt = exerciseRepository.findById(exerciseId); + if (exerciseOpt.isPresent()) { + Exercise exercise = exerciseOpt.get(); + channelReader = new ChannelReader(channel, exercise); + injects = exercise.getInjects(); + } else { + Scenario scenario = this.scenarioService.scenario(exerciseId); + channelReader = new ChannelReader(channel, scenario); + injects = scenario.getInjects(); + } + + Map toPublishArticleIdsMap = injects.stream() + .filter(inject -> inject.getInjectorContract() + .map(contract -> contract.getId().equals(CHANNEL_PUBLISH)) + .orElse(false)) + .filter(inject -> inject.getStatus().isPresent()) + .sorted(Comparator.comparing(inject -> inject.getStatus().get().getTrackingSentDate())) + .flatMap(inject -> { + Instant virtualInjectDate = inject.getStatus().get().getTrackingSentDate(); + try { + ChannelContent content = mapper.treeToValue(inject.getContent(), ChannelContent.class); + if (content.getArticles() != null) { + return content.getArticles().stream().map(article -> new VirtualArticle(virtualInjectDate, article)); + } + return null; + } catch (JsonProcessingException e) { + // Invalid channel content. + return null; + } + }) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toMap(VirtualArticle::id, VirtualArticle::date)); + if (!toPublishArticleIdsMap.isEmpty()) { + List
publishedArticles = fromIterable(articleRepository.findAllById(toPublishArticleIdsMap.keySet())) + .stream().filter(article -> article.getChannel().equals(channel)) + .peek(article -> article.setVirtualPublication(toPublishArticleIdsMap.get(article.getId()))) + .sorted(Comparator.comparing(Article::getVirtualPublication).reversed()) + .toList(); + channelReader.setChannelArticles(publishedArticles); + // Fulfill article expectations + List finalInjects = injects; + List expectationExecutions = publishedArticles.stream() + .flatMap(article -> finalInjects.stream() + .flatMap(inject -> inject.getUserExpectationsForArticle(user, article).stream())) + .filter(exec -> exec.getResults().isEmpty()).toList(); + + // Update all expectations linked to player + expectationExecutions.forEach(injectExpectationExecution -> { + injectExpectationExecution.setResults(List.of( + InjectExpectationResult.builder() + .sourceId("media-pressure") + .sourceType("media-pressure") + .sourceName("Media pressure read") + .result(Instant.now().toString()) + .date(Instant.now().toString()) + .score(injectExpectationExecution.getExpectedScore()) + .build() + )); + injectExpectationExecution.setScore(injectExpectationExecution.getExpectedScore()); + injectExpectationExecution.setUpdatedAt(Instant.now()); + injectExpectationExecutionRepository.save(injectExpectationExecution); + }); + + // -- VALIDATION TYPE -- + processByValidationType(user, injects, publishedArticles, expectationExecutions.size()>0); + } + return channelReader; + } + + private void processByValidationType(User user, List injects, List
publishedArticles, boolean isaNewExpectationResult) { + // Process expectation linked to teams where user if part of + List injectIds = injects.stream().map(Inject::getId).toList(); + List teamIds = user.getTeams().stream().map(Team::getId).toList(); + List articleIds = publishedArticles.stream().map(Article::getId).toList(); //Articles with the same channel + // Find all expectations linked to teams' user, channel and exercise + List channelExpectations = injectExpectationExecutionRepository.findChannelExpectations(injectIds, teamIds, articleIds); + List parentExpectations = channelExpectations.stream().filter(exp -> exp.getUser() == null).toList(); + Map> playerByTeam = channelExpectations.stream().filter(exp -> exp.getUser() != null).collect(Collectors.groupingBy(InjectExpectation::getTeam)); + + // Depending on type of validation, we process the parent expectations: + List toUpdate = ExpectationUtils.processByValidationType(isaNewExpectationResult, channelExpectations, parentExpectations, playerByTeam); + injectExpectationExecutionRepository.saveAll(toUpdate); + } +} diff --git a/openbas-api/src/main/java/io/openbas/service/ExerciseExpectationService.java b/openbas-api/src/main/java/io/openbas/service/ExerciseExpectationService.java index e3e9f82e08..416c8cb7b5 100644 --- a/openbas-api/src/main/java/io/openbas/service/ExerciseExpectationService.java +++ b/openbas-api/src/main/java/io/openbas/service/ExerciseExpectationService.java @@ -6,16 +6,15 @@ import io.openbas.database.model.InjectExpectationResult; import io.openbas.database.repository.ExerciseRepository; import io.openbas.database.repository.InjectExpectationRepository; +import io.openbas.rest.exception.ElementNotFoundException; import io.openbas.rest.exercise.form.ExpectationUpdateInput; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.time.Instant; +import java.util.*; import static java.time.Instant.now; @@ -42,13 +41,19 @@ public InjectExpectation updateInjectExpectation( String result = ""; if (injectExpectation.getType() == EXPECTATION_TYPE.MANUAL) { - if (input.getScore() >= injectExpectation.getExpectedScore()) { - result = "Success"; - } else if (input.getScore() > 0) { - result = "Partial"; + if (injectExpectation.getTeam() != null && injectExpectation.getUser() == null) { //If it is a team expectation + result = input.getScore() > 0 ? "Success" : "Failed"; } else { - result = "Failed"; + if (input.getScore() >= injectExpectation.getExpectedScore()) { + result = "Success"; + } else if (input.getScore() > 0) { + result = "Partial"; + } else { + result = "Failed"; + } } + injectExpectation.getResults().clear(); + exists = Optional.empty(); } else if (injectExpectation.getType() == EXPECTATION_TYPE.DETECTION) { if (input.getScore() >= injectExpectation.getExpectedScore()) { result = "Detected"; @@ -91,7 +96,13 @@ public InjectExpectation updateInjectExpectation( } } injectExpectation.setUpdatedAt(now()); - return this.injectExpectationRepository.save(injectExpectation); + InjectExpectation updated = this.injectExpectationRepository.save(injectExpectation); + + // If The expectation is type manual, We should update expectations for teams and players + if (updated.getType() == EXPECTATION_TYPE.MANUAL && updated.getTeam() != null) { + computeExpectationsForTeamsAndPlayer(updated, result); + } + return updated; } public InjectExpectation deleteInjectExpectationResult( @@ -109,11 +120,94 @@ public InjectExpectation deleteInjectExpectationResult( if (injectExpectation.getType() == EXPECTATION_TYPE.MANUAL) { injectExpectation.setScore(null); } else { - List scores = injectExpectation.getResults().stream().map(InjectExpectationResult::getScore).filter(Objects::nonNull).toList(); - injectExpectation.setScore(!scores.isEmpty() ? Collections.max(scores) : 0); + List scores = injectExpectation.getResults().stream().map(InjectExpectationResult::getScore).filter(Objects::nonNull).toList(); + injectExpectation.setScore(!scores.isEmpty() ? Collections.max(scores) : 0.0); } } injectExpectation.setUpdatedAt(now()); - return this.injectExpectationRepository.save(injectExpectation); + InjectExpectation updated = this.injectExpectationRepository.save(injectExpectation); + + // If The expectation is type manual, We should update expectations for teams and players + if (updated.getType() == EXPECTATION_TYPE.MANUAL && updated.getTeam() != null) { + computeExpectationsForTeamsAndPlayer(updated, null); + } + + return updated; + } + + // -- VALIDATION TYPE -- + + private void computeExpectationsForTeamsAndPlayer(InjectExpectation updated, String result) { + //If the updated expectation was a player expectation, We have to update the team expectation using player expectations (based on validation type) + if (updated.getUser() != null) { + List toProcess = injectExpectationRepository.findAllByInjectAndTeamAndExpectationName(updated.getInject().getId(), updated.getTeam().getId(), updated.getName()); + InjectExpectation parentExpectation = toProcess.stream().filter(exp -> exp.getUser() == null).findFirst().orElseThrow(ElementNotFoundException::new); + int playersSize = toProcess.size() - 1; // Without Parent expectation + long zeroPlayerResponses = toProcess.stream().filter(exp -> exp.getUser() != null).filter(exp -> exp.getScore() != null).filter(exp -> exp.getScore() == 0.0).count(); + long nullPlayerResponses = toProcess.stream().filter(exp -> exp.getUser() != null).filter(exp -> exp.getScore() == null).count(); + + if (updated.isExpectationGroup()) { //If true is at least one + OptionalDouble avgAtLeastOnePlayer = toProcess.stream().filter(exp -> exp.getUser() != null).filter(exp -> exp.getScore() != null).filter(exp -> exp.getScore() > 0.0).mapToDouble(InjectExpectation::getScore).average(); + if (avgAtLeastOnePlayer.isPresent()) { //Any response is positive + parentExpectation.setScore(avgAtLeastOnePlayer.getAsDouble()); + result = "Success"; + } else { + if (zeroPlayerResponses == playersSize) { //All players had failed + parentExpectation.setScore(0.0); + result = "Failed"; + } else { + parentExpectation.setScore(null); + result = "Pending"; + } + } + } else { // all + if(nullPlayerResponses == 0){ + OptionalDouble avgAllPlayer = toProcess.stream().filter(exp -> exp.getUser() != null).mapToDouble(InjectExpectation::getScore).average(); + parentExpectation.setScore(avgAllPlayer.getAsDouble()); + result = zeroPlayerResponses > 0 ? "Failed" : "Success"; + }else{ + if(zeroPlayerResponses == 0) { + parentExpectation.setScore(null); + result = "Pending"; + }else{ + double sumAllPlayer = toProcess.stream().filter(exp -> exp.getUser() != null).filter(exp->exp.getScore() != null).mapToDouble(InjectExpectation::getScore).sum(); + parentExpectation.setScore(sumAllPlayer/playersSize); + result = "Failed"; + } + } + } + parentExpectation.setUpdatedAt(Instant.now()); + parentExpectation.getResults().clear(); + InjectExpectationResult expectationResult = InjectExpectationResult.builder() + .sourceId("player-manual-validation") + .sourceType("player-manual-validation") + .sourceName("Player Manual Validation") + .result(result) + .date(now().toString()) + .score(parentExpectation.getScore()) + .build(); + parentExpectation.getResults().add(expectationResult); + injectExpectationRepository.save(parentExpectation); + } else { + // If I update the expectation team: What happens with children? -> update expectation score for all children -> set score from InjectExpectation + List toProcess = injectExpectationRepository.findAllByInjectAndTeamAndExpectationNameAndUserIsNotNull(updated.getInject().getId(), updated.getTeam().getId(), updated.getName()); + for (InjectExpectation expectation : toProcess) { + expectation.setScore(updated.getScore()); + expectation.setUpdatedAt(Instant.now()); + expectation.getResults().clear(); + if(result != null) { + InjectExpectationResult expectationResult = InjectExpectationResult.builder() + .sourceId("team-manual-validation") + .sourceType("team-manual-validation") + .sourceName("Team Manual Validation") + .result(result) + .date(now().toString()) + .score(updated.getScore()) + .build(); + expectation.getResults().add(expectationResult); + } + injectExpectationRepository.save(expectation); + } + } } } diff --git a/openbas-api/src/main/java/io/openbas/service/InjectService.java b/openbas-api/src/main/java/io/openbas/service/InjectService.java index f146f2d1ca..8a068e67bf 100644 --- a/openbas-api/src/main/java/io/openbas/service/InjectService.java +++ b/openbas-api/src/main/java/io/openbas/service/InjectService.java @@ -728,10 +728,10 @@ private List addFields(Inject inject, RuleAttribute ruleAttribute Double columnValueExpectation = columns.stream() .map(column -> getValueAsDouble(row, column)) .reduce(0.0, Double::sum); - expectation.get().setExpectedScore(columnValueExpectation.intValue()); + expectation.get().setExpectedScore(columnValueExpectation.doubleValue()); } else { try { - expectation.get().setExpectedScore(Integer.parseInt(ruleAttribute.getDefaultValue())); + expectation.get().setExpectedScore(Double.parseDouble(ruleAttribute.getDefaultValue())); } catch (NumberFormatException exception) { List importMessages = new ArrayList<>(); importMessages.add(new ImportMessage(ImportMessage.MessageLevel.WARN, @@ -743,7 +743,7 @@ private List addFields(Inject inject, RuleAttribute ruleAttribute } } } else { - expectation.get().setExpectedScore(Integer.parseInt(ruleAttribute.getDefaultValue())); + expectation.get().setExpectedScore(Double.parseDouble(ruleAttribute.getDefaultValue())); } } else if ("name".equals(ruleAttribute.getName().split("_")[1])) { if(ruleAttribute.getColumns() != null) { diff --git a/openbas-api/src/main/java/io/openbas/utils/AtomicTestingUtils.java b/openbas-api/src/main/java/io/openbas/utils/AtomicTestingUtils.java index ca214705e6..4e67a33966 100644 --- a/openbas-api/src/main/java/io/openbas/utils/AtomicTestingUtils.java +++ b/openbas-api/src/main/java/io/openbas/utils/AtomicTestingUtils.java @@ -20,9 +20,9 @@ public class AtomicTestingUtils { public static List getTargets( - final List teams, - final List assets, - final List assetGroups) { + final List teams, + final List assets, + final List assetGroups) { List targets = new ArrayList<>(); targets.addAll(teams .stream() @@ -30,7 +30,7 @@ public static List getTargets( .toList()); targets.addAll(assets .stream() - .map(t -> new InjectTargetWithResult(TargetType.ASSETS, t.getId(), t.getName(), List.of(), Objects.equals(t.getType(), "Endpoint") ? ((Endpoint) Hibernate.unproxy(t)).getPlatform(): null)) + .map(t -> new InjectTargetWithResult(TargetType.ASSETS, t.getId(), t.getName(), List.of(), Objects.equals(t.getType(), "Endpoint") ? ((Endpoint) Hibernate.unproxy(t)).getPlatform() : null)) .toList()); targets.addAll(assetGroups .stream() @@ -41,22 +41,22 @@ public static List getTargets( } public static List getTargetsFromRaw( - final List teams, - final List assets, - final List assetGroups) { + final List teams, + final List assets, + final List assetGroups) { List targets = new ArrayList<>(); targets.addAll(teams - .stream() - .map(t -> new InjectTargetWithResult(TargetType.TEAMS, t.getTeam_id(), t.getTeam_name(), List.of(), null)) - .toList()); + .stream() + .map(t -> new InjectTargetWithResult(TargetType.TEAMS, t.getTeam_id(), t.getTeam_name(), List.of(), null)) + .toList()); targets.addAll(assets - .stream() - .map(t -> new InjectTargetWithResult(TargetType.ASSETS, t.getAsset_id(), t.getAsset_name(), List.of(), Objects.equals(t.getAsset_type(), "Endpoint") ? Endpoint.PLATFORM_TYPE.valueOf(t.getEndpoint_platform()): null)) - .toList()); + .stream() + .map(t -> new InjectTargetWithResult(TargetType.ASSETS, t.getAsset_id(), t.getAsset_name(), List.of(), Objects.equals(t.getAsset_type(), "Endpoint") ? Endpoint.PLATFORM_TYPE.valueOf(t.getEndpoint_platform()) : null)) + .toList()); targets.addAll(assetGroups - .stream() - .map(t -> new InjectTargetWithResult(TargetType.ASSETS_GROUPS, t.getAsset_group_id(), t.getAsset_group_name(), List.of(), null)) - .toList()); + .stream() + .map(t -> new InjectTargetWithResult(TargetType.ASSETS_GROUPS, t.getAsset_group_id(), t.getAsset_group_name(), List.of(), null)) + .toList()); return targets; } @@ -66,12 +66,17 @@ public static List getTargetsWithResults(final Inject in List expectations = inject.getExpectations(); List teamExpectations = new ArrayList<>(); + List playerExpectations = new ArrayList<>(); List assetExpectations = new ArrayList<>(); List assetGroupExpectations = new ArrayList<>(); expectations.forEach(expectation -> { if (expectation.getTeam() != null) { - teamExpectations.add(expectation); + if (expectation.getUser() != null) { + playerExpectations.add(expectation); + } else { + teamExpectations.add(expectation); + } } if (expectation.getAsset() != null) { assetExpectations.add(expectation); @@ -84,6 +89,13 @@ public static List getTargetsWithResults(final Inject in List targets = new ArrayList<>(); List assetsToRefine = new ArrayList<>(); + // Players + Map>> groupedByTeamAndUser = playerExpectations.stream() + .collect(Collectors.groupingBy( + InjectExpectation::getTeam, + Collectors.groupingBy(InjectExpectation::getUser) + )); + /* Match Target with expectations * */ inject.getTeams().forEach(team -> { @@ -97,7 +109,7 @@ public static List getTargetsWithResults(final Inject in team.getName(), defaultExpectationResultsByTypes, null - ); + ); targets.add(target); } }); @@ -171,7 +183,7 @@ public static List getTargetsWithResults(final Inject in ) ) .entrySet().stream() - .map(entry -> new InjectTargetWithResult(TargetType.TEAMS, entry.getKey().getId(), entry.getKey().getName(), entry.getValue(), null)) + .map(entry -> new InjectTargetWithResult(TargetType.TEAMS, entry.getKey().getId(), entry.getKey().getName(), entry.getValue(), playerExpectations.isEmpty() ? List.of() : calculateResultsforPlayers(groupedByTeamAndUser.get(entry.getKey())), null)) .toList() ); } @@ -267,6 +279,18 @@ public static List getTargetsWithResults(final Inject in return sortResults(targets); } + private static List calculateResultsforPlayers(Map> expectationsByUser) { + return expectationsByUser.entrySet().stream() + .map(userEntry -> new InjectTargetWithResult( + TargetType.PLAYER, + userEntry.getKey().getId(), + userEntry.getKey().getName(), + getExpectationResultByTypes(userEntry.getValue()), + null + )) + .toList(); + } + private static List sortResults(List targets) { return targets.stream().sorted(Comparator.comparing(InjectTargetWithResult::getName)).toList(); } @@ -327,6 +351,7 @@ public static List getRefinedExpectations(Inject inject, List .getExpectations() .stream() .filter(expectation -> targetIds.contains(expectation.getTargetId())) + .filter(expectation -> expectation.getUser() == null) // Filter expectations linked to players. For global results, We use Team expectations .toList(); } @@ -349,16 +374,32 @@ public static List getScores(final List types, final L .stream() .filter(e -> types.contains(e.getType())) .map(injectExpectation -> { - if( injectExpectation.getScore() == null ) { + if (injectExpectation.getScore() == null) { return null; } - if( injectExpectation.getScore() >= injectExpectation.getExpectedScore() ) { - return 1.0; - } - if( injectExpectation.getScore() == 0 ) { - return 0.0; + if (injectExpectation.getTeam() != null) { + if (injectExpectation.isExpectationGroup()) { + if (injectExpectation.getScore() > 0) { + return 1.0; + } else { + return 0.0; + } + } else { + if (injectExpectation.getScore() >= injectExpectation.getExpectedScore()) { + return 1.0; + } else { + return 0.0; + } + } + } else { + if (injectExpectation.getScore() >= injectExpectation.getExpectedScore()) { + return 1.0; + } + if (injectExpectation.getScore() == 0) { + return 0.0; + } + return 0.5; } - return 0.5; }) .toList(); } @@ -368,16 +409,32 @@ public static List getRawScores(final List types, fina .stream() .filter(e -> types.contains(EXPECTATION_TYPE.valueOf(e.getInject_expectation_type()))) .map(rawInjectExpectation -> { - if( rawInjectExpectation.getInject_expectation_score() == null ) { + if (rawInjectExpectation.getInject_expectation_score() == null) { return null; } - if( rawInjectExpectation.getInject_expectation_score() >= rawInjectExpectation.getInject_expectation_expected_score() ) { - return 1.0; - } - if( rawInjectExpectation.getInject_expectation_score() == 0 ) { - return 0.0; + if (rawInjectExpectation.getTeam_id() != null) { + if (rawInjectExpectation.getInject_expectation_group()) { + if (rawInjectExpectation.getInject_expectation_score() > 0) { + return 1.0; + } else { + return 0.0; + } + } else { + if (rawInjectExpectation.getInject_expectation_score() >= rawInjectExpectation.getInject_expectation_expected_score()) { + return 1.0; + } else { + return 0.0; + } + } + } else { + if (rawInjectExpectation.getInject_expectation_score() >= rawInjectExpectation.getInject_expectation_expected_score()) { + return 1.0; + } + if (rawInjectExpectation.getInject_expectation_score() == 0) { + return 0.0; + } + return 0.5; } - return 0.5; }) .toList(); } diff --git a/openbas-api/src/main/java/io/openbas/utils/ExpectationUtils.java b/openbas-api/src/main/java/io/openbas/utils/ExpectationUtils.java new file mode 100644 index 0000000000..d50d179840 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/utils/ExpectationUtils.java @@ -0,0 +1,74 @@ +package io.openbas.utils; + +import io.openbas.database.model.InjectExpectation; +import io.openbas.database.model.InjectExpectationResult; +import io.openbas.database.model.Team; +import io.openbas.rest.exception.ElementNotFoundException; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.OptionalDouble; + +public class ExpectationUtils { + + + public static List processByValidationType(boolean isaNewExpectationResult, List childrenExpectations, List parentExpectations, Map> playerByTeam) { + List updatedExpectations = new ArrayList<>(); + + childrenExpectations.stream().findAny().ifPresentOrElse(process -> { + boolean isValidationAtLeastOneTarget = process.isExpectationGroup(); + + parentExpectations.forEach(parentExpectation -> { + List toProcess = playerByTeam.get(parentExpectation.getTeam()); + int playersSize = toProcess.size(); // Without Parent expectation + long zeroPlayerResponses = toProcess.stream().filter(exp -> exp.getScore() != null).filter(exp -> exp.getScore() == 0.0).count(); + long nullPlayerResponses = toProcess.stream().filter(exp -> exp.getScore() == null).count(); + + if (isValidationAtLeastOneTarget) { // Type atLeast + OptionalDouble avgAtLeastOnePlayer = toProcess.stream().filter(exp -> exp.getScore() != null).filter(exp -> exp.getScore() > 0.0).mapToDouble(InjectExpectation::getScore).average(); + if (avgAtLeastOnePlayer.isPresent()) { //Any response is positive + parentExpectation.setScore(avgAtLeastOnePlayer.getAsDouble()); + } else { + if (zeroPlayerResponses == playersSize) { //All players had failed + parentExpectation.setScore(0.0); + } else { + parentExpectation.setScore(null); + } + } + } else { // type all + if(nullPlayerResponses == 0){ + OptionalDouble avgAllPlayer = toProcess.stream().mapToDouble(InjectExpectation::getScore).average(); + parentExpectation.setScore(avgAllPlayer.getAsDouble()); + }else{ + if(zeroPlayerResponses == 0) { + parentExpectation.setScore(null); + }else{ + double sumAllPlayer = toProcess.stream().filter(exp->exp.getScore() != null).mapToDouble(InjectExpectation::getScore).sum(); + parentExpectation.setScore(sumAllPlayer/playersSize); + } + } + } + + if(isaNewExpectationResult) { + InjectExpectationResult result = InjectExpectationResult.builder() + .sourceId("media-pressure") + .sourceType("media-pressure") + .sourceName("Media pressure read") + .result(Instant.now().toString()) + .date(Instant.now().toString()) + .score(process.getExpectedScore()) + .build(); + parentExpectation.getResults().add(result); + } + + parentExpectation.setUpdatedAt(Instant.now()); + updatedExpectations.add(parentExpectation); + }); + }, ElementNotFoundException::new); + + return updatedExpectations; + } + +} diff --git a/openbas-api/src/main/java/io/openbas/utils/ResultUtils.java b/openbas-api/src/main/java/io/openbas/utils/ResultUtils.java index b0a823e4e6..f6f2ec5951 100644 --- a/openbas-api/src/main/java/io/openbas/utils/ResultUtils.java +++ b/openbas-api/src/main/java/io/openbas/utils/ResultUtils.java @@ -26,6 +26,7 @@ public static List computeGlobalExpectationResults(@No List expectations = injects .stream() .flatMap(inject -> inject.getExpectations().stream()) + .filter(expectation -> expectation.getUser() == null) // Filter expectations linked to players .toList(); return AtomicTestingUtils.getExpectationResultByTypes(expectations); } diff --git a/openbas-api/src/test/java/io/openbas/injects/email/EmailExecutorTest.java b/openbas-api/src/test/java/io/openbas/injects/email/EmailExecutorTest.java index 528e00c696..d94dff667e 100644 --- a/openbas-api/src/test/java/io/openbas/injects/email/EmailExecutorTest.java +++ b/openbas-api/src/test/java/io/openbas/injects/email/EmailExecutorTest.java @@ -49,7 +49,7 @@ void process() throws Exception { content.setBody("A body"); Expectation expectation = new Expectation(); expectation.setName("The animation team can validate the audience reaction"); - expectation.setScore(10); + expectation.setScore(10.0); expectation.setType(InjectExpectation.EXPECTATION_TYPE.MANUAL); content.setExpectations(List.of(expectation)); Inject inject = new Inject(); diff --git a/openbas-api/src/test/java/io/openbas/service/ExerciseExpectationServiceTest.java b/openbas-api/src/test/java/io/openbas/service/ExerciseExpectationServiceTest.java index 7c363485c2..baa025e6e7 100644 --- a/openbas-api/src/test/java/io/openbas/service/ExerciseExpectationServiceTest.java +++ b/openbas-api/src/test/java/io/openbas/service/ExerciseExpectationServiceTest.java @@ -75,7 +75,7 @@ void updateInjectExpectation() { // -- EXECUTE -- ExpectationUpdateInput input = new ExpectationUpdateInput(); - input.setScore(7); + input.setScore(7.0); InjectExpectation expectation = this.exerciseExpectationService.updateInjectExpectation(id, input); // -- ASSERT -- @@ -114,7 +114,7 @@ private void getInjectExpectation(Inject injectCreated, Team teamCreated, Exerci expectation.setTeam(teamCreated); expectation.setType(MANUAL); expectation.setName(EXPECTATION_NAME); - expectation.setExpectedScore(10); + expectation.setExpectedScore(10.0); expectation.setExercise(exerciseCreated); this.injectExpectationRepository.save(expectation); } diff --git a/openbas-api/src/test/java/io/openbas/service/InjectServiceTest.java b/openbas-api/src/test/java/io/openbas/service/InjectServiceTest.java index 2e4151ea1e..ef5e342fb4 100644 --- a/openbas-api/src/test/java/io/openbas/service/InjectServiceTest.java +++ b/openbas-api/src/test/java/io/openbas/service/InjectServiceTest.java @@ -159,10 +159,10 @@ void testImportXlsRelativeDate() throws IOException { verify(teamRepository, times(1)).save(any()); assertEquals(30 * 24 * 60 * 60, importTestSummary.getInjects().getLast().getDependsDuration()); - ObjectNode jsonNodeMail = (ObjectNode) mapper.readTree("{\"message\":\"message1\",\"expectations\":[{\"expectation_description\":\"expectation\",\"expectation_name\":\"expectation done\",\"expectation_score\":100,\"expectation_type\":\"MANUAL\",\"expectation_expectation_group\":false}]}"); + ObjectNode jsonNodeMail = (ObjectNode) mapper.readTree("{\"message\":\"message1\",\"expectations\":[{\"expectation_description\":\"expectation\",\"expectation_name\":\"expectation done\",\"expectation_score\":100.0,\"expectation_type\":\"MANUAL\",\"expectation_expectation_group\":false}]}"); assertEquals(jsonNodeMail, importTestSummary.getInjects().getFirst().getContent()); - ObjectNode jsonNodeSms = (ObjectNode) mapper.readTree("{\"subject\":\"subject\",\"body\":\"message2\",\"expectations\":[{\"expectation_description\":\"expectation\",\"expectation_name\":\"expectation done\",\"expectation_score\":100,\"expectation_type\":\"MANUAL\",\"expectation_expectation_group\":false}]}"); + ObjectNode jsonNodeSms = (ObjectNode) mapper.readTree("{\"subject\":\"subject\",\"body\":\"message2\",\"expectations\":[{\"expectation_description\":\"expectation\",\"expectation_name\":\"expectation done\",\"expectation_score\":100.0,\"expectation_type\":\"MANUAL\",\"expectation_expectation_group\":false}]}"); assertEquals(jsonNodeSms, importTestSummary.getInjects().getLast().getContent()); } } @@ -577,7 +577,7 @@ private List createRuleAttributeSms() { RuleAttribute ruleAttributeExpectationScore = new RuleAttribute(); ruleAttributeExpectationScore.setName("expectation_score"); ruleAttributeExpectationScore.setColumns("J"); - ruleAttributeExpectationScore.setDefaultValue("500"); + ruleAttributeExpectationScore.setDefaultValue("500.0"); RuleAttribute ruleAttributeExpectationName = new RuleAttribute(); ruleAttributeExpectationName.setName("expectation_name"); @@ -637,7 +637,7 @@ private List createRuleAttributeMail() { RuleAttribute ruleAttributeExpectationScore = new RuleAttribute(); ruleAttributeExpectationScore.setName("expectation_score"); ruleAttributeExpectationScore.setColumns("J"); - ruleAttributeExpectationScore.setDefaultValue("500"); + ruleAttributeExpectationScore.setDefaultValue("500.0"); RuleAttribute ruleAttributeExpectationName = new RuleAttribute(); ruleAttributeExpectationName.setName("expectation_name"); diff --git a/openbas-framework/src/main/java/io/openbas/asset/AssetGroupService.java b/openbas-framework/src/main/java/io/openbas/asset/AssetGroupService.java index ae56bb0935..e53246accb 100644 --- a/openbas-framework/src/main/java/io/openbas/asset/AssetGroupService.java +++ b/openbas-framework/src/main/java/io/openbas/asset/AssetGroupService.java @@ -103,7 +103,6 @@ private List computeDynamicAssets(@NotNull final List as assetGroup.setDynamicAssets(filteredAssets); } }); - return assetGroups; } diff --git a/openbas-framework/src/main/java/io/openbas/atomic_testing/TargetType.java b/openbas-framework/src/main/java/io/openbas/atomic_testing/TargetType.java index 8a05ed10d0..b6a958b42c 100644 --- a/openbas-framework/src/main/java/io/openbas/atomic_testing/TargetType.java +++ b/openbas-framework/src/main/java/io/openbas/atomic_testing/TargetType.java @@ -3,5 +3,6 @@ public enum TargetType { ASSETS, ASSETS_GROUPS, + PLAYER, TEAMS } diff --git a/openbas-framework/src/main/java/io/openbas/execution/Injector.java b/openbas-framework/src/main/java/io/openbas/execution/Injector.java index 5356e5cffc..88159998d9 100644 --- a/openbas-framework/src/main/java/io/openbas/execution/Injector.java +++ b/openbas-framework/src/main/java/io/openbas/execution/Injector.java @@ -22,6 +22,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static io.openbas.database.model.InjectStatusExecution.traceError; @@ -52,30 +54,49 @@ public void setFileService(FileService fileService) { public abstract ExecutionProcess process(Execution execution, ExecutableInject injection) throws Exception; private InjectExpectation expectationConverter( - @NotNull final ExecutableInject executableInject, - Expectation expectation) { + @NotNull final ExecutableInject executableInject, + Expectation expectation) { InjectExpectation expectationExecution = new InjectExpectation(); return this.expectationConverter(expectationExecution, executableInject, expectation); } + private InjectExpectation expectationConverter( - @NotNull final Team team, - @NotNull final ExecutableInject executableInject, - Expectation expectation) { + @NotNull final Team team, + @NotNull final ExecutableInject executableInject, + Expectation expectation) { InjectExpectation expectationExecution = new InjectExpectation(); expectationExecution.setTeam(team); return this.expectationConverter(expectationExecution, executableInject, expectation); } + private InjectExpectation expectationConverter( - @NotNull InjectExpectation expectationExecution, - @NotNull final ExecutableInject executableInject, - @NotNull final Expectation expectation) { + @NotNull final Team team, + @NotNull final User user, + @NotNull final ExecutableInject executableInject, + Expectation expectation) { + InjectExpectation expectationExecution = new InjectExpectation(); + expectationExecution.setTeam(team); + expectationExecution.setUser(user); + return this.expectationConverter(expectationExecution, executableInject, expectation); + } + + private InjectExpectation expectationConverter( + @NotNull InjectExpectation expectationExecution, + @NotNull final ExecutableInject executableInject, + @NotNull final Expectation expectation) { expectationExecution.setExercise(executableInject.getInjection().getExercise()); expectationExecution.setInject(executableInject.getInjection().getInject()); expectationExecution.setExpectedScore(expectation.getScore()); expectationExecution.setExpectationGroup(expectation.isExpectationGroup()); switch (expectation.type()) { - case ARTICLE -> expectationExecution.setArticle(((ChannelExpectation) expectation).getArticle()); - case CHALLENGE -> expectationExecution.setChallenge(((ChallengeExpectation) expectation).getChallenge()); + case ARTICLE -> { + expectationExecution.setName(expectation.getName()); + expectationExecution.setArticle(((ChannelExpectation) expectation).getArticle()); + } + case CHALLENGE -> { + expectationExecution.setName(expectation.getName()); + expectationExecution.setChallenge(((ChallengeExpectation) expectation).getChallenge()); + } case DOCUMENT -> expectationExecution.setType(EXPECTATION_TYPE.DOCUMENT); case TEXT -> expectationExecution.setType(EXPECTATION_TYPE.TEXT); case DETECTION -> { @@ -125,15 +146,48 @@ public Execution execute(ExecutableInject executableInject) { List assetGroups = executableInject.getAssetGroups(); if ((isScheduledInject || isAtomicTesting) && !expectations.isEmpty()) { if (!teams.isEmpty()) { - List injectExpectations = teams.stream() - .flatMap(team -> expectations.stream() - .map(expectation -> expectationConverter(team, executableInject, expectation))) - .toList(); - this.injectExpectationRepository.saveAll(injectExpectations); + List injectExpectationsByTeam; + + List injectExpectationsByUserAndTeam; + // If atomicTesting, We create expectation for every player and every team + if (isAtomicTesting) { + injectExpectationsByTeam = teams.stream() + .flatMap(team -> expectations.stream() + .map(expectation -> expectationConverter(team, executableInject, expectation))) + .collect(Collectors.toList()); + + injectExpectationsByUserAndTeam = teams.stream() + .flatMap(team -> team.getUsers().stream() + .flatMap(user -> expectations.stream() + .map(expectation -> expectationConverter(team, user, executableInject, expectation)))) + .toList(); + } else { + // Create expectations for every enabled player in every team + injectExpectationsByUserAndTeam = teams.stream() + .flatMap(team -> team.getExerciseTeamUsers().stream() + .filter(exerciseTeamUser -> exerciseTeamUser.getExercise().getId().equals(executableInject.getInjection().getExercise().getId())) + .flatMap(exerciseTeamUser -> expectations.stream() + .map(expectation -> expectationConverter(team, exerciseTeamUser.getUser(), executableInject, expectation)))) + .toList(); + + // Create a set of teams that have at least one enabled player + Set teamsWithEnabledPlayers = injectExpectationsByUserAndTeam.stream() + .map(InjectExpectation::getTeam) + .collect(Collectors.toSet()); + + // Add only the expectations where the team has at least one enabled player + injectExpectationsByTeam = teamsWithEnabledPlayers.stream() + .flatMap(team -> expectations.stream() + .map(expectation -> expectationConverter(team, executableInject, expectation))) + .collect(Collectors.toList()); + } + + injectExpectationsByTeam.addAll(injectExpectationsByUserAndTeam); + this.injectExpectationRepository.saveAll(injectExpectationsByTeam); } else if (!assets.isEmpty() || !assetGroups.isEmpty()) { List injectExpectations = expectations.stream() - .map(expectation -> expectationConverter(executableInject, expectation)) - .toList(); + .map(expectation -> expectationConverter(executableInject, expectation)) + .toList(); this.injectExpectationRepository.saveAll(injectExpectations); } } diff --git a/openbas-framework/src/main/java/io/openbas/inject_expectation/InjectExpectationService.java b/openbas-framework/src/main/java/io/openbas/inject_expectation/InjectExpectationService.java index 3d0c3288b1..a40fe015cd 100644 --- a/openbas-framework/src/main/java/io/openbas/inject_expectation/InjectExpectationService.java +++ b/openbas-framework/src/main/java/io/openbas/inject_expectation/InjectExpectationService.java @@ -51,8 +51,8 @@ public InjectExpectation computeExpectation( computeResult(expectation, sourceId, sourceType, sourceName, result, expectation.getExpectedScore()); expectation.setScore(expectation.getExpectedScore()); } else if (expectation.getScore() == null) { - computeResult(expectation, sourceId, sourceType, sourceName, result, 0); - expectation.setScore(0); + computeResult(expectation, sourceId, sourceType, sourceName, result, 0.0); + expectation.setScore(0.0); } return this.update(expectation); } @@ -71,7 +71,7 @@ public void computeExpectationGroup( success = expectationAssets.stream().allMatch((e) -> e.getExpectedScore().equals(e.getScore())); } computeResult(expectationAssetGroup, sourceId, sourceType, sourceName, success ? "SUCCESS" : "FAILED", success ? expectationAssetGroup.getExpectedScore() : 0); - expectationAssetGroup.setScore(success ? expectationAssetGroup.getExpectedScore() : 0); + expectationAssetGroup.setScore(success ? expectationAssetGroup.getExpectedScore() : 0.0); this.update(expectationAssetGroup); } @@ -176,11 +176,13 @@ public List manualExpectationsNotFill() { public List findExpectationsByInjectAndTargetAndTargetType( @NotBlank final String injectId, @NotBlank final String targetId, + @NotBlank final String parentTargetId, @NotBlank final String targetType) { try { TargetType targetTypeEnum = TargetType.valueOf(targetType); return switch (targetTypeEnum) { case TEAMS -> injectExpectationRepository.findAllByInjectAndTeam(injectId, targetId); + case PLAYER -> injectExpectationRepository.findAllByInjectAndTeamAndPlayer(injectId, parentTargetId, targetId); case ASSETS -> injectExpectationRepository.findAllByInjectAndAsset(injectId, targetId); case ASSETS_GROUPS -> injectExpectationRepository.findAllByInjectAndAssetGroup(injectId, targetId); }; diff --git a/openbas-framework/src/main/java/io/openbas/inject_expectation/InjectExpectationUtils.java b/openbas-framework/src/main/java/io/openbas/inject_expectation/InjectExpectationUtils.java index 85edd8a6b8..4b56ddc389 100644 --- a/openbas-framework/src/main/java/io/openbas/inject_expectation/InjectExpectationUtils.java +++ b/openbas-framework/src/main/java/io/openbas/inject_expectation/InjectExpectationUtils.java @@ -29,7 +29,7 @@ public static void computeResult( @NotBlank final String sourceType, @NotBlank final String sourceName, @NotBlank final String result, - @NotBlank final Integer score + @NotBlank final Double score ) { Optional exists = expectation.getResults() .stream() diff --git a/openbas-framework/src/main/java/io/openbas/integrations/PayloadService.java b/openbas-framework/src/main/java/io/openbas/integrations/PayloadService.java index 07dc339cdd..6419739e88 100644 --- a/openbas-framework/src/main/java/io/openbas/integrations/PayloadService.java +++ b/openbas-framework/src/main/java/io/openbas/integrations/PayloadService.java @@ -130,12 +130,12 @@ private ContractExpectations expectations() { Expectation preventionExpectation = new Expectation(); preventionExpectation.setType(PREVENTION); preventionExpectation.setName("Expect inject to be prevented"); - preventionExpectation.setScore(100); + preventionExpectation.setScore(100.0); // Detection Expectation detectionExpectation = new Expectation(); detectionExpectation.setType(DETECTION); detectionExpectation.setName("Expect inject to be detected"); - detectionExpectation.setScore(100); + detectionExpectation.setScore(100.0); return expectationsField("expectations", "Expectations", List.of(preventionExpectation, detectionExpectation)); } diff --git a/openbas-framework/src/main/java/io/openbas/model/Expectation.java b/openbas-framework/src/main/java/io/openbas/model/Expectation.java index f8b9276df9..326a8e29fe 100644 --- a/openbas-framework/src/main/java/io/openbas/model/Expectation.java +++ b/openbas-framework/src/main/java/io/openbas/model/Expectation.java @@ -5,8 +5,9 @@ public interface Expectation { EXPECTATION_TYPE type(); - Integer getScore(); + Double getScore(); default boolean isExpectationGroup() { return false; } + String getName(); } diff --git a/openbas-framework/src/main/java/io/openbas/model/expectation/ChallengeExpectation.java b/openbas-framework/src/main/java/io/openbas/model/expectation/ChallengeExpectation.java index 6eae32638d..9024e25176 100644 --- a/openbas-framework/src/main/java/io/openbas/model/expectation/ChallengeExpectation.java +++ b/openbas-framework/src/main/java/io/openbas/model/expectation/ChallengeExpectation.java @@ -12,14 +12,23 @@ @Setter public class ChallengeExpectation implements Expectation { - private Integer score; + private Double score; private Challenge challenge; + private boolean expectationGroup; + private String name; - public ChallengeExpectation(Integer score, Challenge challenge) { - setScore(Objects.requireNonNullElse(score, 100)); + public ChallengeExpectation(Double score, Challenge challenge) { + setScore(Objects.requireNonNullElse(score, 100.0)); setChallenge(challenge); } + public ChallengeExpectation(Double score, Challenge challenge, boolean expectationGroup) { + setScore(Objects.requireNonNullElse(score, 100.0)); + setChallenge(challenge); + setName(challenge.getName()); + setExpectationGroup(expectationGroup); + } + @Override public InjectExpectation.EXPECTATION_TYPE type() { return InjectExpectation.EXPECTATION_TYPE.CHALLENGE; diff --git a/openbas-framework/src/main/java/io/openbas/model/expectation/ChannelExpectation.java b/openbas-framework/src/main/java/io/openbas/model/expectation/ChannelExpectation.java index a1682380be..120df00a78 100644 --- a/openbas-framework/src/main/java/io/openbas/model/expectation/ChannelExpectation.java +++ b/openbas-framework/src/main/java/io/openbas/model/expectation/ChannelExpectation.java @@ -12,14 +12,23 @@ @Setter public class ChannelExpectation implements Expectation { - private Integer score; + private Double score; private Article article; + private boolean expectationGroup; + private String name; - public ChannelExpectation(Integer score, Article article) { - setScore(Objects.requireNonNullElse(score, 100)); + public ChannelExpectation(Double score, Article article) { + setScore(Objects.requireNonNullElse(score, 100.0)); setArticle(article); } + public ChannelExpectation(Double score, Article article, boolean expectationGroup) { + setScore(Objects.requireNonNullElse(score, 100.0)); + setArticle(article); + setName(article.getName()); + setExpectationGroup(expectationGroup); + } + @Override public InjectExpectation.EXPECTATION_TYPE type() { return InjectExpectation.EXPECTATION_TYPE.ARTICLE; diff --git a/openbas-framework/src/main/java/io/openbas/model/expectation/DetectionExpectation.java b/openbas-framework/src/main/java/io/openbas/model/expectation/DetectionExpectation.java index 2b0989fa94..a6cb815273 100644 --- a/openbas-framework/src/main/java/io/openbas/model/expectation/DetectionExpectation.java +++ b/openbas-framework/src/main/java/io/openbas/model/expectation/DetectionExpectation.java @@ -20,7 +20,7 @@ @Setter public class DetectionExpectation implements Expectation { - private Integer score; + private Double score; private String name; private String description; private Asset asset; @@ -37,7 +37,7 @@ public InjectExpectation.EXPECTATION_TYPE type() { } public static DetectionExpectation detectionExpectationForAsset( - @Nullable final Integer score, + @Nullable final Double score, @NotBlank final String name, final String description, @NotNull final Asset asset, @@ -45,7 +45,7 @@ public static DetectionExpectation detectionExpectationForAsset( final List expectationSignatures ) { DetectionExpectation detectionExpectation = new DetectionExpectation(); - detectionExpectation.setScore(Objects.requireNonNullElse(score, 100)); + detectionExpectation.setScore(Objects.requireNonNullElse(score, 100.0)); detectionExpectation.setName(name); detectionExpectation.setDescription(description); detectionExpectation.setAsset(asset); @@ -55,7 +55,7 @@ public static DetectionExpectation detectionExpectationForAsset( } public static DetectionExpectation detectionExpectationForAssetGroup( - @Nullable final Integer score, + @Nullable final Double score, @NotBlank final String name, final String description, @NotNull final AssetGroup assetGroup, @@ -63,7 +63,7 @@ public static DetectionExpectation detectionExpectationForAssetGroup( final List expectationSignatures ) { DetectionExpectation detectionExpectation = new DetectionExpectation(); - detectionExpectation.setScore(Objects.requireNonNullElse(score, 100)); + detectionExpectation.setScore(Objects.requireNonNullElse(score, 100.0)); detectionExpectation.setName(name); detectionExpectation.setDescription(description); detectionExpectation.setAssetGroup(assetGroup); diff --git a/openbas-framework/src/main/java/io/openbas/model/expectation/ManualExpectation.java b/openbas-framework/src/main/java/io/openbas/model/expectation/ManualExpectation.java index f42f2d7509..1cc631c45a 100644 --- a/openbas-framework/src/main/java/io/openbas/model/expectation/ManualExpectation.java +++ b/openbas-framework/src/main/java/io/openbas/model/expectation/ManualExpectation.java @@ -19,7 +19,7 @@ @Setter public class ManualExpectation implements Expectation { - private Integer score; + private Double score; private String name; private String description; private Asset asset; @@ -29,25 +29,32 @@ public class ManualExpectation implements Expectation { public ManualExpectation() { } - public ManualExpectation(final Integer score) { - this.score = Objects.requireNonNullElse(score, 100); + public ManualExpectation(final Double score) { + this.score = Objects.requireNonNullElse(score, 100.0); } - public ManualExpectation(final Integer score, @NotBlank final String name, final String description) { + public ManualExpectation(final Double score, @NotBlank final String name, final String description) { this(score); this.name = name; this.description = description; } + public ManualExpectation(final Double score, @NotBlank final String name, final String description, final boolean expectationGroup) { + this(score); + this.name = name; + this.description = description; + this.expectationGroup = expectationGroup; + } + public static ManualExpectation manualExpectationForAsset( - @Nullable final Integer score, + @Nullable final Double score, @NotBlank final String name, final String description, @NotNull final Asset asset, final boolean expectationGroup ) { ManualExpectation manualExpectation = new ManualExpectation(); - manualExpectation.setScore(Objects.requireNonNullElse(score, 100)); + manualExpectation.setScore(Objects.requireNonNullElse(score, 100.0)); manualExpectation.setName(name); manualExpectation.setDescription(description); manualExpectation.setAsset(asset); @@ -56,14 +63,14 @@ public static ManualExpectation manualExpectationForAsset( } public static ManualExpectation manualExpectationForAssetGroup( - @Nullable final Integer score, + @Nullable final Double score, @NotBlank final String name, final String description, @NotNull final AssetGroup assetGroup, final boolean expectationGroup ) { ManualExpectation manualExpectation = new ManualExpectation(); - manualExpectation.setScore(Objects.requireNonNullElse(score, 100)); + manualExpectation.setScore(Objects.requireNonNullElse(score, 100.0)); manualExpectation.setName(name); manualExpectation.setDescription(description); manualExpectation.setAssetGroup(assetGroup); diff --git a/openbas-framework/src/main/java/io/openbas/model/expectation/PreventionExpectation.java b/openbas-framework/src/main/java/io/openbas/model/expectation/PreventionExpectation.java index a71690cdd3..355ca70a3d 100644 --- a/openbas-framework/src/main/java/io/openbas/model/expectation/PreventionExpectation.java +++ b/openbas-framework/src/main/java/io/openbas/model/expectation/PreventionExpectation.java @@ -20,7 +20,7 @@ @Setter public class PreventionExpectation implements Expectation { - private Integer score; + private Double score; private String name; private String description; private Asset asset; @@ -37,7 +37,7 @@ public EXPECTATION_TYPE type() { } public static PreventionExpectation preventionExpectationForAsset( - @Nullable final Integer score, + @Nullable final Double score, @NotBlank final String name, final String description, @NotNull final Asset asset, @@ -45,7 +45,7 @@ public static PreventionExpectation preventionExpectationForAsset( final List expectationSignatures ) { PreventionExpectation preventionExpectation = new PreventionExpectation(); - preventionExpectation.setScore(Objects.requireNonNullElse(score, 100)); + preventionExpectation.setScore(Objects.requireNonNullElse(score, 100.0)); preventionExpectation.setName(name); preventionExpectation.setDescription(description); preventionExpectation.setAsset(asset); @@ -55,7 +55,7 @@ public static PreventionExpectation preventionExpectationForAsset( } public static PreventionExpectation preventionExpectationForAssetGroup( - @Nullable final Integer score, + @Nullable final Double score, @NotBlank final String name, final String description, @NotNull final AssetGroup assetGroup, @@ -63,7 +63,7 @@ public static PreventionExpectation preventionExpectationForAssetGroup( final List expectationSignatures ) { PreventionExpectation preventionExpectation = new PreventionExpectation(); - preventionExpectation.setScore(Objects.requireNonNullElse(score, 100)); + preventionExpectation.setScore(Objects.requireNonNullElse(score, 100.0)); preventionExpectation.setName(name); preventionExpectation.setDescription(description); preventionExpectation.setAssetGroup(assetGroup); diff --git a/openbas-framework/src/main/java/io/openbas/model/inject/form/Expectation.java b/openbas-framework/src/main/java/io/openbas/model/inject/form/Expectation.java index 8027d4ff2d..ce1801671b 100644 --- a/openbas-framework/src/main/java/io/openbas/model/inject/form/Expectation.java +++ b/openbas-framework/src/main/java/io/openbas/model/inject/form/Expectation.java @@ -17,7 +17,7 @@ public class Expectation { private String description; @JsonProperty("expectation_score") - private Integer score; + private Double score; @JsonProperty("expectation_expectation_group") private boolean expectationGroup; diff --git a/openbas-front/src/actions/atomic_testings/atomic-testing-actions.ts b/openbas-front/src/actions/atomic_testings/atomic-testing-actions.ts index a97e97e5cb..a1564a99f6 100644 --- a/openbas-front/src/actions/atomic_testings/atomic-testing-actions.ts +++ b/openbas-front/src/actions/atomic_testings/atomic-testing-actions.ts @@ -29,8 +29,11 @@ export const tryAtomicTesting = (injectId: string) => { return simpleCall(uri); }; -export const fetchTargetResult = (injectId: string, targetId: string, targetType: string) => { - const uri = `${ATOMIC_TESTING_URI}/${injectId}/target_results/${targetId}/types/${targetType}`; +export const fetchTargetResult = (injectId: string, targetId: string, targetType: string, parentTargetId ?: string) => { + let uri = `${ATOMIC_TESTING_URI}/${injectId}/target_results/${targetId}/types/${targetType}`; + if (parentTargetId) { + uri += `?parentTargetId=${encodeURIComponent(parentTargetId)}`; + } return simpleCall(uri); }; diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTesting.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTesting.tsx index df70e62cee..e14c2a4ef4 100644 --- a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTesting.tsx +++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTesting.tsx @@ -43,19 +43,23 @@ const AtomicTesting = () => { const classes = useStyles(); const { t, tPick, fldt } = useFormatter(); const [selectedTarget, setSelectedTarget] = useState(); + const [currentParentTarget, setCurrentParentTarget] = useState(); const filtering = useSearchAnFilter('', 'name', ['name']); // Fetching data const { injectResultDto } = useContext(InjectResultDtoContext); useEffect(() => { - setSelectedTarget(injectResultDto?.inject_targets[0]); + setSelectedTarget(currentParentTarget || injectResultDto?.inject_targets[0]); }, [injectResultDto]); const sortedTargets: InjectTargetWithResult[] = filtering.filterAndSort(injectResultDto?.inject_targets ?? []); // Handles - const handleTargetClick = (target: InjectTargetWithResult) => { + const handleTargetClick = (target: InjectTargetWithResult, currentParent?: InjectTargetWithResult) => { setSelectedTarget(target); + if (currentParent) { + setCurrentParentTarget(currentParent); + } }; if (!injectResultDto) { @@ -225,10 +229,12 @@ const AtomicTesting = () => { {sortedTargets.map((target) => (
- + handleTargetClick(target)} target={target} selected={selectedTarget?.id === target.id} /> {target?.children?.map((child) => ( - + handleTargetClick(child, target)} + target={child} selected={selectedTarget?.id === child.id && currentParentTarget?.id === target.id} + /> ))}
@@ -246,8 +252,9 @@ const AtomicTesting = () => { {selectedTarget && !!injectResultDto.inject_type && ( diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingDetail.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingDetail.tsx index ffd7a9fc3a..aa035cb65d 100644 --- a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingDetail.tsx +++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingDetail.tsx @@ -73,11 +73,13 @@ const AtomicTestingDetail: FunctionComponent = () => { { injectResultDto.inject_expectations !== undefined && injectResultDto.inject_expectations.length > 0 - ? injectResultDto.inject_expectations.map((expectation, index) => ( - - {expectation.inject_expectation_name} - - )) : + ? Array.from(new Set(injectResultDto.inject_expectations.map((expectation) => expectation.inject_expectation_name))) + .map((name, index) => ( + + {name} + + )) + : {'-'} } diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/TargetResultsDetail.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/TargetResultsDetail.tsx index a54d9e570b..46595ed27e 100644 --- a/openbas-front/src/admin/components/atomic_testings/atomic_testing/TargetResultsDetail.tsx +++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/TargetResultsDetail.tsx @@ -6,6 +6,7 @@ import { CardActionArea, CardContent, CardHeader, + Chip, Dialog, DialogActions, DialogContent, @@ -17,6 +18,7 @@ import { MenuItem, Tab, Tabs, + Tooltip, Typography, } from '@mui/material'; import { makeStyles, useTheme } from '@mui/styles'; @@ -36,11 +38,12 @@ import ItemResult from '../../../../components/ItemResult'; import InjectIcon from '../../common/injects/InjectIcon'; import { isNotEmptyField } from '../../../../utils/utils'; import Transition from '../../../../components/common/Transition'; -import { emptyFilled } from '../../../../utils/String'; +import { emptyFilled, truncate } from '../../../../utils/String'; import DetectionPreventionExpectationsValidationForm from '../../simulations/simulation/validation/expectations/DetectionPreventionExpectationsValidationForm'; import { deleteInjectExpectationResult } from '../../../../actions/Exercise'; import { useAppDispatch } from '../../../../utils/hooks'; import type { InjectExpectationStore } from '../../../../actions/injects/Inject'; +import { isTechnicalExpectation } from '../../common/injects/expectations/ExpectationUtils'; interface Steptarget { label: string; @@ -81,6 +84,11 @@ const useStyles = makeStyles((theme) => ({ width: '100%', height: '100%', }, + score: { + fontSize: '0.75rem', + height: '20px', + padding: '0 4px', + }, })); interface Props { @@ -88,6 +96,7 @@ interface Props { lastExecutionStartDate: string, lastExecutionEndDate: string, target: InjectTargetWithResult, + parentTargetId?: string, } const TargetResultsDetailFlow: FunctionComponent = ({ @@ -95,6 +104,7 @@ const TargetResultsDetailFlow: FunctionComponent = ({ lastExecutionStartDate, lastExecutionEndDate, target, + parentTargetId, }) => { const classes = useStyles(); const dispatch = useAppDispatch(); @@ -213,7 +223,7 @@ const TargetResultsDetailFlow: FunctionComponent = ({ labelShowBg: false, labelStyle: { fill: theme.palette.text?.primary, fontSize: 9 }, }))); - fetchTargetResult(inject.inject_id, target.id!, target.targetType!).then( + fetchTargetResult(inject.inject_id, target.id!, target.targetType!, parentTargetId).then( (result: { data: InjectExpectationsStore[] }) => setTargetResults(result.data ?? []), ); setActiveTab(0); @@ -247,6 +257,8 @@ const TargetResultsDetailFlow: FunctionComponent = ({ } return status.every((s) => s === 'SUCCESS') ? 'Attack Detected' : 'Attack Not Detected'; case 'MANUAL': + case 'ARTICLE': + case 'CHALLENGE': if (status.includes('UNKNOWN')) { return 'No Expectation for Manual'; } @@ -266,7 +278,6 @@ const TargetResultsDetailFlow: FunctionComponent = ({ return ''; } }; - const getAvatar = (injectExpectation: InjectExpectationStore, expectationResult: InjectExpectationResult) => { if (expectationResult.sourceType === 'collector') { return ( @@ -330,12 +341,26 @@ const TargetResultsDetailFlow: FunctionComponent = ({ useEffect(() => { if (initialized && targetResults && targetResults.length > 0) { const groupedBy = groupedByExpectationType(targetResults); - const newSteps = Array.from(groupedBy).map(([targetType, targetResult]) => ({ + const newSteps = Array.from(groupedBy).flatMap(([targetType, results]) => results.sort((a: InjectExpectationsStore, b: InjectExpectationsStore) => { + if (a.inject_expectation_name && b.inject_expectation_name) { + return a.inject_expectation_name.localeCompare(b.inject_expectation_name); + } if (a.inject_expectation_name && !b.inject_expectation_name) { + return -1; // a comes before b + } if (!a.inject_expectation_name && b.inject_expectation_name) { + return 1; // b comes before a + } + return a.inject_expectation_id.localeCompare(b.inject_expectation_id); + }).map((expectation: InjectExpectationStore) => ({ key: 'result', - label: getStatusLabel(targetType, targetResult.map((tr: InjectExpectationsStore) => tr.inject_expectation_status)), + label: ( + + {getStatusLabel(targetType, [expectation.inject_expectation_status])} +
{truncate(expectation.inject_expectation_name, 20)} +
+ ), type: targetType, - status: getStatus(targetResult.map((tr: InjectExpectationsStore) => tr.inject_expectation_status)), - })); + status: getStatus([expectation.inject_expectation_status]), + }))); const mergedSteps: Steptarget[] = [...computeInitialSteps(initialSteps), ...newSteps]; // Custom sorting function mergedSteps.sort((a, b) => { @@ -356,6 +381,7 @@ const TargetResultsDetailFlow: FunctionComponent = ({ background: getColor(step.status).background, }, position: { x: 0, y: 0 }, + }))); setEdges([...Array(mergedSteps.length - 1)].map((_, i) => ({ id: `result-${i}->result-${i + 1}`, @@ -392,6 +418,17 @@ const TargetResultsDetailFlow: FunctionComponent = ({ type: 'straight', markerEnd: { type: MarkerType.ArrowClosed }, }; + const getLabelOfValidationType = (injectExpectation: InjectExpectationsStore): string => { + // eslint-disable-next-line no-nested-ternary + return isTechnicalExpectation(injectExpectation.inject_expectation_type) + ? injectExpectation.inject_expectation_group + ? t('At least one asset (per group) must validate the expectation') + : t('All assets (per group) must validate the expectation') + : injectExpectation.inject_expectation_group + ? t('At least one player (per team) must validate the expectation') + : t('All players (per team) must validate the expectation'); + }; + return ( <>
@@ -426,10 +463,10 @@ const TargetResultsDetailFlow: FunctionComponent = ({ nodesFocusable={false} elementsSelectable={false} maxZoom={1} - zoomOnScroll={false} + zoomOnScroll zoomOnPinch={false} zoomOnDoubleClick={false} - panOnDrag={false} + panOnDrag defaultEdgeOptions={defaultEdgeOptions} proOptions={proOptions} /> @@ -451,88 +488,106 @@ const TargetResultsDetailFlow: FunctionComponent = ({ )} {Object.keys(sortedGroupedResults).map((targetResult, targetResultIndex) => ( ); }; + return ( diff --git a/openbas-front/src/admin/components/common/injects/expectations/Expectation.ts b/openbas-front/src/admin/components/common/injects/expectations/Expectation.ts index 8e54bc934f..c1a2b677b1 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/Expectation.ts +++ b/openbas-front/src/admin/components/common/injects/expectations/Expectation.ts @@ -1,7 +1,8 @@ import type { InjectExpectation } from '../../../../../utils/api-types'; -export interface InjectExpectationsStore extends Omit { +export interface InjectExpectationsStore extends Omit { inject_expectation_team: string | undefined; + inject_expectation_user: string | undefined; inject_expectation_article: string | undefined; inject_expectation_challenge: string | undefined; inject_expectation_asset: string | undefined; diff --git a/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx b/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx index 00507fa8b5..64bb7e266d 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx +++ b/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx @@ -6,8 +6,8 @@ import type { ExpectationInput } from './Expectation'; import { formProps, infoMessage } from './ExpectationFormUtils'; import { useFormatter } from '../../../../../components/i18n'; import type { Theme } from '../../../../../components/Theme'; -import { hasExpectationByGroup } from './ExpectationUtils'; import ExpectationGroupField from './field/ExpectationGroupField'; +import { isTechnicalExpectation } from './ExpectationUtils'; const useStyles = makeStyles((theme: Theme) => ({ marginTop_2: { @@ -95,7 +95,7 @@ const ExpectationFormCreate: FunctionComponent = ({ {t('MANUAL')}
- {watchType === 'ARTICLE' + {(watchType === 'ARTICLE' || watchType === 'CHALLENGE') && = ({ } inputProps={register('expectation_score')} /> - - {hasExpectationByGroup(watchType) - && - } - +
- {getValues().expectation_type === 'ARTICLE' + {(getValues().expectation_type === 'ARTICLE' || getValues().expectation_type === 'CHALLENGE') && = ({ } inputProps={register('expectation_score')} /> - - {hasExpectationByGroup(initialValues.expectation_type) - && - } - +
)} diff --git a/openbas-front/src/admin/components/simulations/simulation/validation/common/TeamOrAssetLine.tsx b/openbas-front/src/admin/components/simulations/simulation/validation/common/TeamOrAssetLine.tsx index e5cbb341bc..44615d1360 100644 --- a/openbas-front/src/admin/components/simulations/simulation/validation/common/TeamOrAssetLine.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/validation/common/TeamOrAssetLine.tsx @@ -91,13 +91,13 @@ const TeamOrAssetLine: FunctionComponent = ({ const asset: EndpointStore = assetsMap[id]; const assetGroup: AssetGroupStore = assetGroupsMap[id]; - const groupedByExpectationType = (es: InjectExpectationsStore[]) => { + const groupByExpectationName = (es: InjectExpectationsStore[]) => { return es.reduce((group, expectation) => { - const { inject_expectation_type } = expectation; - if (inject_expectation_type) { - const values = group.get(inject_expectation_type) ?? []; + const { inject_expectation_name } = expectation; + if (inject_expectation_name) { + const values = group.get(inject_expectation_name) ?? []; values.push(expectation); - group.set(inject_expectation_type, values); + group.set(inject_expectation_name, values); } return group; }, new Map()); @@ -124,28 +124,28 @@ const TeamOrAssetLine: FunctionComponent = ({ /> - {Array.from(groupedByExpectationType(expectations)).map(([expectationType, es]) => { - if (expectationType === 'ARTICLE') { + {Array.from(groupByExpectationName(expectations)).map(([expectationName, es]) => { + if (es === 'ARTICLE') { const expectation = es[0]; const article = articlesMap[expectation.inject_expectation_article] || {}; const channel = channelsMap[article.article_channel] || {}; return ( - + ); } - if (expectationType === 'CHALLENGE') { + if (es === 'CHALLENGE') { const expectation = es[0]; const challenge = challengesMap[expectation.inject_expectation_challenge] || {}; return ( - + ); } - if (expectationType === 'PREVENTION' || expectationType === 'DETECTION') { + if (es === 'PREVENTION' || es === 'DETECTION') { const expectation = es[0]; if (asset) { return ( @@ -156,7 +156,7 @@ const TeamOrAssetLine: FunctionComponent = ({ return ( = ({ /> ); } - return (
); + return (
); } return ( - + ); })}
diff --git a/openbas-front/src/admin/components/simulations/simulation/validation/expectations/ManualExpectations.tsx b/openbas-front/src/admin/components/simulations/simulation/validation/expectations/ManualExpectations.tsx index 0ec340df45..8d4a0d7d2f 100644 --- a/openbas-front/src/admin/components/simulations/simulation/validation/expectations/ManualExpectations.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/validation/expectations/ManualExpectations.tsx @@ -1,17 +1,37 @@ import React, { FunctionComponent, useState } from 'react'; -import { List, ListItemButton, ListItemIcon, ListItemText, Chip } from '@mui/material'; -import { AssignmentTurnedIn } from '@mui/icons-material'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Alert, + AlertTitle, + Chip, + Divider, + List, + ListItemButton, + ListItemIcon, + ListItemText, + Tooltip, + Typography, +} from '@mui/material'; +import { AssignmentTurnedIn, ExpandMore, PersonOutlined } from '@mui/icons-material'; import { makeStyles } from '@mui/styles'; import * as R from 'ramda'; -import type { Team, Inject } from '../../../../../../utils/api-types'; -import { useHelper } from '../../../../../../store'; import type { InjectExpectationsStore } from '../../../../common/injects/expectations/Expectation'; import { useFormatter } from '../../../../../../components/i18n'; import type { Theme } from '../../../../../../components/Theme'; import colorStyles from '../../../../../../components/Color'; -import type { TeamsHelper } from '../../../../../../actions/teams/team-helper'; import Drawer from '../../../../../../components/common/Drawer'; import ManualExpectationsValidationForm from './ManualExpectationsValidationForm'; +import ExpandableText from '../../../../../../components/common/ExpendableText'; +import type { Inject, User } from '../../../../../../utils/api-types'; +import Paper from '../../../../../../components/common/Paper'; +import { computeColorStyle, computeLabel, resolveUserName, truncate } from '../../../../../../utils/String'; +import { useHelper } from '../../../../../../store'; +import type { UserHelper } from '../../../../../../actions/helper'; +import { useAppDispatch } from '../../../../../../utils/hooks'; +import useDataLoader from '../../../../../../utils/hooks/useDataLoader'; +import { fetchUsers } from '../../../../../../actions/User'; const useStyles = makeStyles((theme: Theme) => ({ item: { @@ -32,16 +52,28 @@ const useStyles = makeStyles((theme: Theme) => ({ textTransform: 'uppercase', width: 200, }, + chipStatusAcc: { + height: 30, + borderRadius: 4, + textTransform: 'uppercase', + width: 150, + float: 'right', + marginLeft: 5, + }, points: { height: 20, backgroundColor: 'rgba(236, 64, 122, 0.08)', border: '1px solid #ec407a', color: '#ec407a', }, + validationType: { + height: 20, + border: '1px solid', + borderRadius: 4, + }, })); interface Props { - exerciseId: string; inject: Inject; expectations: InjectExpectationsStore[]; } @@ -53,89 +85,183 @@ const ManualExpectations: FunctionComponent = ({ const classes = useStyles(); const { t } = useFormatter(); - const { teamsMap }: { teamsMap: Record } = useHelper((helper: TeamsHelper) => { + const [selectedItem, setSelectedItem] = useState(null); + const [currentExpectations, setCurrentExpectations] = useState(null); + const [expanded, setExpanded] = useState(false); + + const { usersMap }: { + usersMap: Record + } = useHelper((helper: UserHelper) => { return ({ - teamsMap: helper.getTeamsMap(), + usersMap: helper.getUsersMap(), }); }); + const dispatch = useAppDispatch(); + useDataLoader(() => { + dispatch(fetchUsers()); + }); - const groupedByTeam = expectations.reduce((group: Map, expectation) => { - const { inject_expectation_team } = expectation; - if (inject_expectation_team) { - const values = group.get(inject_expectation_team) ?? []; - values.push(expectation); - group.set(inject_expectation_team, values); - } - return group; - }, new Map()); + const handleItemClick = (expectationsToUpdate: InjectExpectationsStore[]) => { + setSelectedItem(expectationsToUpdate[0]?.inject_expectation_name || null); + setCurrentExpectations(expectationsToUpdate); + }; + const handleItemClose = () => { + setSelectedItem(null); + setCurrentExpectations(null); + }; - const [currentExpectations, setCurrentExpectations] = useState(null); + const handleChange = (panel: string) => (_event: React.SyntheticEvent, isExpanded: boolean) => { + setExpanded(isExpanded ? panel : false); + }; + + const parentExpectation = expectations.filter((e) => !e.inject_expectation_user)[0]; + const childrenExpectations = expectations.filter((e) => e.inject_expectation_user); + const validatedCount = expectations.filter((v) => !R.isEmpty(v.inject_expectation_results)).length; + const isAllValidated = validatedCount === expectations.length; + + const label = isAllValidated + ? `${t('Validated')} (${parentExpectation.inject_expectation_score})` + : t('Pending validation'); + + const style = isAllValidated ? colorStyles.green : colorStyles.orange; + + const targetLabel = (expectationToProcess: InjectExpectationsStore) => { + if (expectationToProcess.inject_expectation_user && usersMap[expectationToProcess.inject_expectation_user]) { + return truncate(resolveUserName(usersMap[expectationToProcess.inject_expectation_user]), 22); + } + return t('Unknown'); + }; return ( <> - {Array.from(groupedByTeam) - .map(([entry, values]) => { - const team = teamsMap[entry] || {}; - const expectationValues = values - .reduce((acc, el) => ({ - ...acc, - expected_score: acc.expected_score + (el.inject_expectation_expected_score ?? 0), - score: acc.score + (el.inject_expectation_score ?? 0), - result: acc.result + (el.inject_expectation_results ?? ''), - }), { expected_score: 0, score: 0, result: '' }); - const validated = values.filter((v) => !R.isEmpty(v.inject_expectation_results)).length; - let label = t('Pending validation'); - if (validated === values.length) { - label = `${t('Validated')} (${expectationValues.score})`; - } - return ( - setCurrentExpectations(values)} - > - - - - - {t('Manual expectations')} -
- - -
-
- )} - /> - - ); - })} + {expectations.length > 0 && ( + handleItemClick(expectations)} + selected={selectedItem === expectations[0].inject_expectation_name} + > + + + + + + + {expectations[0].inject_expectation_name ?? 'Manual Expectation'} + + +
+ + + +
+
+ )} + /> + + )} setCurrentExpectations(null)} + handleClose={() => handleItemClose()} title={t('Expectations of ') + inject.inject_title} > <> - { - expectations - && expectations.map((e) => ) - } + + + + + + + + {t('The score set for the team will also be applied to all players in the team')} + + + + {t('Team')} + + + + + + + {t('Players')} + +
+ {childrenExpectations.map((e) => { + const panelId = `panel-${e.inject_expectation_id}`; + + return ( + + } + aria-controls={`${panelId}-content`} + id={`${panelId}-header`} + style={{ + boxShadow: 'none', + border: 'none', + height: '10px', + }} + > +
+
+ + {targetLabel(e)} +
+
+ + +
+
+
+ + + +
+ ); + })} +
diff --git a/openbas-front/src/admin/components/simulations/simulation/validation/expectations/ManualExpectationsValidationForm.tsx b/openbas-front/src/admin/components/simulations/simulation/validation/expectations/ManualExpectationsValidationForm.tsx index e071f1fecd..31fae6eb4f 100644 --- a/openbas-front/src/admin/components/simulations/simulation/validation/expectations/ManualExpectationsValidationForm.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/validation/expectations/ManualExpectationsValidationForm.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useEffect } from 'react'; import { Button, Chip, TextField as MuiTextField, Typography } from '@mui/material'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -8,20 +8,28 @@ import type { InjectExpectationsStore } from '../../../../common/injects/expecta import { useFormatter } from '../../../../../../components/i18n'; import { updateInjectExpectation } from '../../../../../../actions/Exercise'; import { useAppDispatch } from '../../../../../../utils/hooks'; -import ExpandableText from '../../../../../../components/common/ExpendableText'; import type { Theme } from '../../../../../../components/Theme'; -import colorStyles from '../../../../../../components/Color'; import { zodImplement } from '../../../../../../utils/Zod'; +import type { TeamsHelper } from '../../../../../../actions/teams/team-helper'; +import type { UserHelper } from '../../../../../../actions/helper'; +import { useHelper } from '../../../../../../store'; +import type { Team, User } from '../../../../../../utils/api-types'; +import { resolveUserName, truncate, computeLabel, computeColorStyle } from '../../../../../../utils/String'; +import useDataLoader from '../../../../../../utils/hooks/useDataLoader'; +import { fetchUsers } from '../../../../../../actions/User'; +import { fetchTeams } from '../../../../../../actions/teams/team-actions'; const useStyles = makeStyles((theme: Theme) => ({ marginTop_2: { marginTop: theme.spacing(2), }, + scoreAcc: { + margin: 0, + }, buttons: { display: 'flex', placeContent: 'end', gap: theme.spacing(2), - marginTop: theme.spacing(2), }, chipInList: { height: 30, @@ -35,44 +43,41 @@ const useStyles = makeStyles((theme: Theme) => ({ interface FormProps { expectation: InjectExpectationsStore; onUpdate?: () => void; + withSummary?: boolean; } -const ManualExpectationsValidationForm: FunctionComponent = ({ expectation, onUpdate }) => { +const ManualExpectationsValidationForm: FunctionComponent = ({ expectation, onUpdate, withSummary = true }) => { const classes = useStyles(); const { t } = useFormatter(); + const { teamsMap, usersMap }: { + teamsMap: Record, + usersMap: Record + } = useHelper((helper: TeamsHelper & UserHelper) => { + return ({ + teamsMap: helper.getTeamsMap(), + usersMap: helper.getUsersMap(), + }); + }); const dispatch = useAppDispatch(); - const computeLabel = (e: InjectExpectationsStore) => { - if (e.inject_expectation_status === 'PENDING') { - return t('Pending validation'); - } - if (e.inject_expectation_status === 'SUCCESS') { - return t('Success'); - } - if (e.inject_expectation_status === 'PARTIAL') { - return t('Partial'); - } - return t('Failed'); - }; - const computeColorStyle = (e: InjectExpectationsStore) => { - if (e.inject_expectation_status === 'PENDING') { - return colorStyles.blueGrey; - } - if (e.inject_expectation_status === 'SUCCESS') { - return colorStyles.green; - } - if (e.inject_expectation_status === 'PARTIAL') { - return colorStyles.orange; - } - return colorStyles.red; - }; + useDataLoader(() => { + dispatch(fetchUsers()); + dispatch(fetchTeams()); + }); const onSubmit = (data: { expectation_score: number }) => { - dispatch(updateInjectExpectation(expectation.inject_expectation_id, { ...data, source_id: 'ui', source_type: 'ui', source_name: 'User input' })).then(() => { + dispatch(updateInjectExpectation(expectation.inject_expectation_id, { + ...data, + source_id: 'ui', + source_type: 'ui', + source_name: 'User input', + })).then(() => { onUpdate?.(); }); }; + const { register, handleSubmit, + reset, formState: { errors, isSubmitting }, } = useForm<{ expectation_score: number }>({ mode: 'onTouched', @@ -83,39 +88,53 @@ const ManualExpectationsValidationForm: FunctionComponent = ({ expect expectation_score: expectation.inject_expectation_score ?? expectation.inject_expectation_expected_score ?? 0, }, }); + useEffect(() => { + reset({ + expectation_score: expectation.inject_expectation_score ?? expectation.inject_expectation_expected_score ?? 0, + }); + }, [expectation, reset]); + + const targetLabel = (expectationToProcess: InjectExpectationsStore) => { + if (expectationToProcess.inject_expectation_user && usersMap[expectationToProcess.inject_expectation_user]) { + return truncate(resolveUserName(usersMap[expectationToProcess.inject_expectation_user]), 22); + } + if (expectationToProcess.inject_expectation_team) { + return teamsMap[expectationToProcess.inject_expectation_team]?.team_name; + } + return t('Unknown'); + }; + return ( - - - {t('Name')} - {expectation.inject_expectation_name} -
- {t('Description')} - -
- -
- -
- +
+
+ {withSummary && ()} + {withSummary && ({expectation.inject_expectation_user ? t('Player') : t('Team')})} + {withSummary && targetLabel(expectation)} + +
+ +
+ +
); }; diff --git a/openbas-front/src/utils/Localization.js b/openbas-front/src/utils/Localization.js index a0bc3a0153..81cf585b3c 100644 --- a/openbas-front/src/utils/Localization.js +++ b/openbas-front/src/utils/Localization.js @@ -346,8 +346,8 @@ const i18n = { 'Send email': 'Envoyer le mail', 'Add documents in this media pressure': 'Ajouter des documents à cette pression médiatique', - 'Expect teams to read the article(s)': - 'Les équipes doivent lire le(s) article(s)', + 'Expect targets to read the article(s)': + 'Les cibles doivent lire le(s) article(s)', 'Add media pressure': 'Ajouter de la pression médiatique', 'Remove from the media pressure': 'Supprimer de la pression médiatique', 'Raw request data': 'Données brutes de la requête', @@ -620,7 +620,8 @@ const i18n = { 'Each team should submit a text response': 'Chaque équipe doit soumettre une réponse texte', // -- Expectation start -- - 'This expectation is handled automatically by the platform and triggered when team reads articles': 'Cet attendu est géré automatiquement par la plateforme et déclenché lorsque une équipe lit les articles', + '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', 'Add expectations': 'Ajouter des attendus', 'Add expectation in this inject': 'Ajouter des attendus dans ce stimuli', 'Update the expectation': 'Modifier l\'attendu', @@ -628,7 +629,7 @@ const i18n = { 'Number of expectations': "Nombre d'attendus", 'Inject expectations': 'Attendus du stimuli', 'Manual expectations': 'Attendus manuels', - 'Expectations of ': 'Attendus de', + 'Expectations of ': 'Attendus de ', MANUAL: 'Manuel', ARTICLE: 'Automatique - Déclenché lorsque l\'équipe a lu l\'article', DETECTION: 'Automatique - Detection: Déclenché lorsque l\'injection est traitée', @@ -646,9 +647,13 @@ const i18n = { Failed: 'Echoué', 'Pending result': 'Résultat en attente', 'Validation mode': 'Type de validation', + 'All players (per team) must validate the expectation': 'Tous les joueurs (par équipe) doivent valider l\'attente', 'All assets (per group) must validate the expectation': 'Tous les actifs (par groupe) doivent valider l\'attente', + 'At least one player (per team) must validate the expectation': 'Au moins un joueur (par équipe) doit valider l\'attente', 'At least one asset (per group) must validate the expectation': 'Au moins un actif (par groupe) doit valider l\'attente', 'An isolated asset is considered as a group of one asset': 'Un actif isolé est considéré comme un groupe d\'un seul actif', + 'An isolated player is considered as a group of one player': 'Un jouer isolé est considéré comme un groupe d\'un seul jouer', + 'The score set for the team will also be applied to all players in the team': 'Le score attribué à l\'équipe sera également appliqué à tous les joueurs de l\'équipe', // -- Expectation end -- 'Distribution of expected total score by team': 'Distribution du score total attendu par équipe', @@ -696,6 +701,13 @@ const i18n = { Error: 'Erreur', 'This page is not found on this OpenBAS application.': 'Cette page est introuvable sur l\'application OpenBAS.', 'You must be logged to access this page': 'Vous devez être connecté pour accéder à cette page', + // Validation Type + 'Validation type': 'Type de validation', + 'User input': 'Entrée utilisateur', + 'Player Manual Validation': 'Validation manuelle du joueur', + 'Team Manual Validation': 'Validation manuelle de l\'équipe', + 'Challenge validation': 'Validation du défi', + 'Media pressure read': 'Pression médiatique', // Challenges Challenges: 'Challenges', // Variables @@ -729,6 +741,7 @@ const i18n = { 'Token key': 'Token', Example: 'Exemple', Score: 'Score', + 'Expected score': 'Score attendu', Message: 'Message', 'New control': 'Nouveau contrôle', 'Percent of reached score': 'Pourcentage du score atteint', @@ -742,6 +755,8 @@ const i18n = { Order: 'Ordre', Details: 'Détails', Team: 'Equipe', + Player: 'Jouer', + player: 'jouer', Template: 'Modèle', Questionnaire: 'Questionnaire', User: 'Utilisateur', @@ -767,6 +782,7 @@ const i18n = { 'Should not be empty': 'Ne doit pas être vide', // Assets Endpoints: 'Endpoints', + asset: 'actif', 'Teams of players': 'Equipes de joueurs', 'Create a new endpoint': 'Créer un nouvel endpoint', 'Update the endpoint': 'Modifier l\'endpoint', @@ -1565,7 +1581,7 @@ const i18n = { 'Send email': '发送邮件', 'Add documents in this media pressure': '将文档添加到媒体', - 'Expect teams to read the article(s)': + 'Expect targets to read the article(s)': '希望团队阅读文章', 'Add media pressure': '添加媒体', 'Remove from the media pressure': '从媒体移除', @@ -1860,7 +1876,9 @@ const i18n = { Failed: '已失败', 'Pending result': '待定结果', 'Validation mode': '验证模式', + 'All players (per team) must validate the expectation': '所有球员(每队)必须验证期望值', 'All assets (per group) must validate the expectation': '所有资产(每组)必须验证期望值', + 'At least one player (per team) must validate the expectation': '至少一名球员(每队)必须验证期望值', 'At least one asset (per group) must validate the expectation': '至少一个资产(每组)必须验证期望值', 'An isolated asset is considered as a group of one asset': '隔离的资产被视为一组资产', // -- Expectation end -- @@ -2467,7 +2485,7 @@ const i18n = { REGEXP: 'Regular expression', '-': 'None', MANUAL: 'Manual', - ARTICLE: 'Automatic - Triggered when team reads articles', + ARTICLE: 'Automatic - Triggered when target reads the articles', DETECTION: 'Automatic - Detection: Triggered when inject is processed', PREVENTION: 'Automatic - Prevention: Triggered when inject is processed', TYPE_ARTICLE: 'Article', diff --git a/openbas-front/src/utils/String.js b/openbas-front/src/utils/String.js index a30aa49db3..f1a589b5c5 100644 --- a/openbas-front/src/utils/String.js +++ b/openbas-front/src/utils/String.js @@ -1,4 +1,5 @@ import { isNotEmptyField } from './utils'; +import colorStyles from '../components/Color'; export const truncate = (str, limit) => { if (str === undefined || str === null || str.length <= limit) { @@ -52,3 +53,31 @@ export const getLabelOfRemainingItems = (items, start, property) => { export const getRemainingItemsCount = (items, visibleItems) => { return (items && visibleItems && items.length - visibleItems.length) || null; }; + +// Compute label for status +export const computeLabel = (status) => { + if (status === 'PENDING') { + return 'Pending validation'; + } + if (status === 'SUCCESS') { + return 'Success'; + } + if (status === 'PARTIAL') { + return 'Partial'; + } + return 'Failed'; +}; + +// compute color for status +export const computeColorStyle = (status) => { + if (status === 'PENDING') { + return colorStyles.blueGrey; + } + if (status === 'SUCCESS') { + return colorStyles.green; + } + if (status === 'PARTIAL') { + return colorStyles.orange; + } + return colorStyles.red; +}; diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 6cb18072ff..2e21a9d0a3 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -1347,7 +1347,7 @@ export interface InjectTargetWithResult { id: string; name?: string; platformType?: "Linux" | "Windows" | "MacOS" | "Container" | "Service" | "Generic" | "Internal" | "Unknown"; - targetType?: "ASSETS" | "ASSETS_GROUPS" | "TEAMS"; + targetType?: "ASSETS" | "ASSETS_GROUPS" | "PLAYER" | "TEAMS"; } export interface InjectTeamsInput { diff --git a/openbas-model/src/main/java/io/openbas/database/model/Challenge.java b/openbas-model/src/main/java/io/openbas/database/model/Challenge.java index 0b19ad3f82..aad574f86f 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Challenge.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Challenge.java @@ -57,7 +57,7 @@ public class Challenge implements Base { @Column(name = "challenge_score") @JsonProperty("challenge_score") - private Integer score; + private Double score; @Column(name = "challenge_max_attempts") @JsonProperty("challenge_max_attempts") 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 91de738556..d97653434a 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 @@ -310,7 +310,8 @@ public List getUserExpectationsForArticle(User user, Article return this.expectations.stream() .filter(execution -> execution.getType().equals(InjectExpectation.EXPECTATION_TYPE.ARTICLE)) .filter(execution -> execution.getArticle().equals(article)) - .filter(execution -> execution.getTeam().getUsers().contains(user)) + .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(); } @@ -418,6 +419,7 @@ public static Inject fromRawInject(RawInject rawInject, 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) { diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectExpectation.java b/openbas-model/src/main/java/io/openbas/database/model/InjectExpectation.java index a307b4956f..9752b9377a 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectExpectation.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectExpectation.java @@ -86,26 +86,44 @@ public enum EXPECTATION_STATUS { @Setter @Column(name = "inject_expectation_score") @JsonProperty("inject_expectation_score") - private Integer score; + private Double score; @JsonProperty("inject_expectation_status") public EXPECTATION_STATUS getResponse() { + if( this.getScore() == null ) { return EXPECTATION_STATUS.PENDING; } - if( this.getScore() >= this.getExpectedScore() ) { - return EXPECTATION_STATUS.SUCCESS; - } - if( this.getScore() == 0 ) { - return EXPECTATION_STATUS.FAILED; + + if (team != null) { + if(this.isExpectationGroup()){ + if( this.getScore() > 0) { + return EXPECTATION_STATUS.SUCCESS; + }else{ + return EXPECTATION_STATUS.FAILED; + } + }else{ + if( this.getScore() >= this.getExpectedScore()) { + return EXPECTATION_STATUS.SUCCESS; + }else{ + return EXPECTATION_STATUS.FAILED; + } + } + }else { + if (this.getScore() >= this.getExpectedScore()) { + return EXPECTATION_STATUS.SUCCESS; + } + if (this.getScore() == 0) { + return EXPECTATION_STATUS.FAILED; + } + return EXPECTATION_STATUS.PARTIAL; } - return EXPECTATION_STATUS.PARTIAL; } @Setter @Column(name = "inject_expectation_expected_score") @JsonProperty("inject_expectation_expected_score") - private Integer expectedScore; + private Double expectedScore; @Setter @Column(name = "inject_expectation_created_at") diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectExpectationResult.java b/openbas-model/src/main/java/io/openbas/database/model/InjectExpectationResult.java index 5fc2ea2aef..ec5765b77b 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectExpectationResult.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectExpectationResult.java @@ -16,7 +16,7 @@ public class InjectExpectationResult { private String date; - private Integer score; + private Double score; @NotBlank private String result; diff --git a/openbas-model/src/main/java/io/openbas/database/model/Team.java b/openbas-model/src/main/java/io/openbas/database/model/Team.java index ad3f16f7ce..6b34f973c8 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Team.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Team.java @@ -149,33 +149,33 @@ public long getInjectExceptationsNumber() { @JsonProperty("team_injects_expectations_total_score") @NotNull - public long getInjectExceptationsTotalScore() { + public double getInjectExceptationsTotalScore() { return getInjectExpectations().stream() .filter((inject) -> inject.getScore() != null) - .mapToLong(InjectExpectation::getScore).sum(); + .mapToDouble(InjectExpectation::getScore).sum(); } @JsonProperty("team_injects_expectations_total_score_by_exercise") @NotNull - public Map getInjectExceptationsTotalScoreByExercise() { + public Map getInjectExceptationsTotalScoreByExercise() { return getInjectExpectations().stream() .filter(expectation -> Objects.nonNull(expectation.getExercise()) && Objects.nonNull(expectation.getScore())) .collect(Collectors.groupingBy(expectation -> expectation.getExercise().getId(), - Collectors.summingLong(InjectExpectation::getScore))); + Collectors.summingDouble(InjectExpectation::getScore))); } @JsonProperty("team_injects_expectations_total_expected_score") @NotNull - public long getInjectExceptationsTotalExpectedScore() { - return getInjectExpectations().stream().mapToLong(InjectExpectation::getExpectedScore).sum(); + public double getInjectExceptationsTotalExpectedScore() { + return getInjectExpectations().stream().mapToDouble(InjectExpectation::getExpectedScore).sum(); } @JsonProperty("team_injects_expectations_total_expected_score_by_exercise") @NotNull - public Map getInjectExpectationsTotalExpectedScoreByExercise() { + public Map getInjectExpectationsTotalExpectedScoreByExercise() { return getInjectExpectations().stream() .filter(expectation -> Objects.nonNull(expectation.getExercise())) .collect(Collectors.groupingBy(expectation -> expectation.getExercise().getId(), - Collectors.summingLong(InjectExpectation::getExpectedScore))); + Collectors.summingDouble(InjectExpectation::getExpectedScore))); } // endregion diff --git a/openbas-model/src/main/java/io/openbas/database/model/TeamSimple.java b/openbas-model/src/main/java/io/openbas/database/model/TeamSimple.java index 4fedd2f751..1da1ed59c1 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/TeamSimple.java +++ b/openbas-model/src/main/java/io/openbas/database/model/TeamSimple.java @@ -94,33 +94,33 @@ public long getInjectExceptationsNumber() { @JsonProperty("team_injects_expectations_total_score") @NotNull - public long getInjectExceptationsTotalScore() { + public double getInjectExceptationsTotalScore() { return getInjectExpectations().stream() .filter((inject) -> inject.getScore() != null) - .mapToLong(InjectExpectation::getScore).sum(); + .mapToDouble(InjectExpectation::getScore).sum(); } @JsonProperty("team_injects_expectations_total_score_by_exercise") @NotNull - public Map getInjectExceptationsTotalScoreByExercise() { + public Map getInjectExceptationsTotalScoreByExercise() { return getInjectExpectations().stream() .filter(expectation -> Objects.nonNull(expectation.getExercise()) && Objects.nonNull(expectation.getScore())) .collect(Collectors.groupingBy(expectation -> expectation.getExercise().getId(), - Collectors.summingLong(InjectExpectation::getScore))); + Collectors.summingDouble(InjectExpectation::getScore))); } @JsonProperty("team_injects_expectations_total_expected_score") @NotNull - public long getInjectExceptationsTotalExpectedScore() { - return getInjectExpectations().stream().mapToLong(InjectExpectation::getExpectedScore).sum(); + public double getInjectExceptationsTotalExpectedScore() { + return getInjectExpectations().stream() .filter(expectation -> Objects.nonNull(expectation.getExpectedScore())).mapToDouble(InjectExpectation::getExpectedScore).sum(); } @JsonProperty("team_injects_expectations_total_expected_score_by_exercise") @NotNull - public Map getInjectExpectationsTotalExpectedScoreByExercise() { - Map result = getInjectExpectations().stream() + public Map getInjectExpectationsTotalExpectedScoreByExercise() { + Map result = getInjectExpectations().stream() .filter(expectation -> Objects.nonNull(expectation.getExercise())) .collect(Collectors.groupingBy(expectation -> expectation.getExercise().getId(), - Collectors.summingLong(InjectExpectation::getExpectedScore))); + Collectors.summingDouble(InjectExpectation::getExpectedScore))); return result; } // endregion diff --git a/openbas-model/src/main/java/io/openbas/database/raw/RawGlobalInjectExpectation.java b/openbas-model/src/main/java/io/openbas/database/raw/RawGlobalInjectExpectation.java index 28b17d2c7b..bdf025bbdb 100644 --- a/openbas-model/src/main/java/io/openbas/database/raw/RawGlobalInjectExpectation.java +++ b/openbas-model/src/main/java/io/openbas/database/raw/RawGlobalInjectExpectation.java @@ -4,9 +4,9 @@ public interface RawGlobalInjectExpectation { String getInject_expectation_type(); - Integer getInject_expectation_score(); + Double getInject_expectation_score(); - Integer getInject_expectation_expected_score(); + Double getInject_expectation_expected_score(); String getInject_title(); diff --git a/openbas-model/src/main/java/io/openbas/database/raw/RawInjectExpectation.java b/openbas-model/src/main/java/io/openbas/database/raw/RawInjectExpectation.java index 5fd149e763..ce568e9ccd 100644 --- a/openbas-model/src/main/java/io/openbas/database/raw/RawInjectExpectation.java +++ b/openbas-model/src/main/java/io/openbas/database/raw/RawInjectExpectation.java @@ -4,9 +4,9 @@ public interface RawInjectExpectation { String getInject_expectation_type(); - Integer getInject_expectation_score(); + Double getInject_expectation_score(); - Integer getInject_expectation_expected_score(); + Double getInject_expectation_expected_score(); String getTeam_id(); @@ -17,4 +17,6 @@ public interface RawInjectExpectation { String getInject_expectation_id(); String getExercise_id(); + + Boolean getInject_expectation_group(); } diff --git a/openbas-model/src/main/java/io/openbas/database/raw/impl/SimpleRawInjectExpectation.java b/openbas-model/src/main/java/io/openbas/database/raw/impl/SimpleRawInjectExpectation.java index cfce2dc84f..d11aa344cc 100644 --- a/openbas-model/src/main/java/io/openbas/database/raw/impl/SimpleRawInjectExpectation.java +++ b/openbas-model/src/main/java/io/openbas/database/raw/impl/SimpleRawInjectExpectation.java @@ -10,10 +10,11 @@ public class SimpleRawInjectExpectation implements RawInjectExpectation { private String inject_expectation_id; private String inject_expectation_type; - private Integer inject_expectation_score; - private Integer inject_expectation_expected_score; + private Double inject_expectation_score; + private Double inject_expectation_expected_score; private String team_id; private String asset_id; private String asset_group_id; private String exercise_id; + private Boolean inject_expectation_group; } 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 6e58f1aeb4..8a88ecd961 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 @@ -55,7 +55,7 @@ public interface ExerciseRepository extends CrudRepository, * @param from the max date of creation * @return the list of expectations */ - @Query(value = "SELECT ie.inject_expectation_type, ie.inject_expectation_score, ie.inject_expectation_expected_score " + + @Query(value = "SELECT ie.inject_expectation_type, ie.inject_expectation_group, ie.inject_expectation_score, ie.inject_expectation_expected_score " + "FROM injects_expectations ie " + "INNER JOIN injects ON ie.inject_id = injects.inject_id " + "INNER JOIN exercises ON injects.inject_exercise = exercises.exercise_id " + @@ -68,7 +68,7 @@ public interface ExerciseRepository extends CrudRepository, * @param userId the id of the user * @return the list of expectations */ - @Query(value = "SELECT ie.inject_expectation_type, ie.inject_expectation_score, ie.inject_expectation_expected_score " + + @Query(value = "SELECT ie.inject_expectation_type, ie.inject_expectation_group, ie.inject_expectation_score, ie.inject_expectation_expected_score " + "FROM injects_expectations ie " + "INNER JOIN injects ON ie.inject_id = injects.inject_id " + "INNER JOIN exercises e ON injects.inject_exercise = e.exercise_id " + diff --git a/openbas-model/src/main/java/io/openbas/database/repository/InjectExpectationRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/InjectExpectationRepository.java index 5889f8e770..6b7398126c 100644 --- a/openbas-model/src/main/java/io/openbas/database/repository/InjectExpectationRepository.java +++ b/openbas-model/src/main/java/io/openbas/database/repository/InjectExpectationRepository.java @@ -24,21 +24,32 @@ public interface InjectExpectationRepository extends CrudRepository findAllForExerciseAndInject( - @Param("exerciseId") @NotBlank final String exerciseId, - @Param("injectId") @NotBlank final String injectId + @Param("exerciseId") @NotBlank final String exerciseId, + @Param("injectId") @NotBlank final String injectId ); @Query(value = "select i from InjectExpectation i where i.exercise.id = :exerciseId " + - "and i.type = 'CHALLENGE' and i.team.id IN (:teamIds)") - List findChallengeExpectations(@Param("exerciseId") String exerciseId, - @Param("teamIds") List teamIds); + "and i.type = 'CHALLENGE' and i.user.id = :userId ") + List findChallengeExpectationsByExerciseAndUser(@Param("exerciseId") String exerciseId, + @Param("userId") String userId); @Query(value = "select i from InjectExpectation i where i.exercise.id = :exerciseId " + - "and i.challenge.id = :challengeId and i.team.id IN (:teamIds)") + "and i.type = 'CHALLENGE' and i.challenge.id = :challengeId and i.team.id in (:teamIds)") List findChallengeExpectations(@Param("exerciseId") String exerciseId, - @Param("teamIds") List teamIds, - @Param("challengeId") String challengeId); - + @Param("teamIds") List teamIds, + @Param("challengeId") String challengeId); + + @Query(value = "select i from InjectExpectation i where i.user.id = :userId and i.exercise.id = :exerciseId " + + "and i.challenge.id = :challengeId and i.type = 'CHALLENGE' ") + List findByUserAndExerciseAndChallenge(@Param("userId") String userId, + @Param("exerciseId") String exerciseId, + @Param("challengeId") String challengeId); + + @Query(value = "select i from InjectExpectation i where i.inject.id in (:injectIds) " + + "and i.article.id in (:articlesIds) and i.team.id in (:teamIds) and i.type = 'ARTICLE'") + List findChannelExpectations(@Param("injectIds") List injectIds, + @Param("teamIds") List teamIds, + @Param("articlesIds") List articlesIds); // -- PREVENTION -- @Query(value = "select i from InjectExpectation i where i.type = 'PREVENTION' and i.inject.id = :injectId and i.asset.id = :assetId") @@ -49,29 +60,53 @@ List findChallengeExpectations(@Param("exerciseId") String ex // -- BY TARGET TYPE - @Query(value = "select i from InjectExpectation i where i.inject.id = :injectId and i.team.id = :teamId") + @Query(value = "select i from InjectExpectation i where i.inject.id = :injectId and i.team.id = :teamId and i.user.id = :playerId") + List findAllByInjectAndTeamAndPlayer( + @Param("injectId") @NotBlank final String injectId, + @Param("teamId") @NotBlank final String teamId, + @Param("playerId") @NotBlank final String playerId + ); + + @Query("select ie from InjectExpectation ie " + + "where ie.inject.id = :injectId " + + "and ie.team.id = :teamId " + + "and ie.name = :expectationName ") + List findAllByInjectAndTeamAndExpectationName(final String injectId, final String teamId, final String expectationName); + + @Query("select ie from InjectExpectation ie " + + "where ie.inject.id = :injectId " + + "and ie.team.id = :teamId " + + "and ie.name = :expectationName " + + "and ie.user is not null") + List findAllByInjectAndTeamAndExpectationNameAndUserIsNotNull(final String injectId, final String teamId, final String expectationName); + + // -- RETRIEVE EXPECTATIONS FOR TEAM AND NOT FOR PLAYERS + @Query("select ie from InjectExpectation ie where ie.inject.id = :injectId and ie.team.id = :teamId and ie.name = :expectationName and ie.user is null") + Optional findByInjectAndTeamAndExpectationNameAndUserIsNull(String injectId, String teamId, String expectationName); + + @Query(value = "select i from InjectExpectation i where i.inject.id = :injectId and i.team.id = :teamId and i.user is null") List findAllByInjectAndTeam( - @Param("injectId") @NotBlank final String injectId, - @Param("teamId") @NotBlank final String teamId + @Param("injectId") @NotBlank final String injectId, + @Param("teamId") @NotBlank final String teamId ); @Query(value = "select i from InjectExpectation i where i.inject.id = :injectId and i.asset.id = :assetId") List findAllByInjectAndAsset( - @Param("injectId") @NotBlank final String injectId, - @Param("assetId") @NotBlank final String assetId + @Param("injectId") @NotBlank final String injectId, + @Param("assetId") @NotBlank final String assetId ); @Query(value = "select i from InjectExpectation i where i.inject.id = :injectId and i.assetGroup.id = :assetGroupId") List findAllByInjectAndAssetGroup( - @Param("injectId") @NotBlank final String injectId, - @Param("assetGroupId") @NotBlank final String assetGroupId + @Param("injectId") @NotBlank final String injectId, + @Param("assetGroupId") @NotBlank final String assetGroupId ); @Query(value = "SELECT " - + "team_id, asset_id, asset_group_id, inject_expectation_type, " - + "inject_expectation_score, inject_expectation_expected_score, inject_expectation_id, exercise_id " - + "FROM injects_expectations i " - + "where i.inject_expectation_id IN :ids", - nativeQuery = true) + + "team_id, asset_id, asset_group_id, inject_expectation_type, " + + "inject_expectation_score, inject_expectation_group, inject_expectation_expected_score, inject_expectation_id, exercise_id " + + "FROM injects_expectations i " + + "where i.inject_expectation_id IN :ids", + nativeQuery = true) List rawByIds(@Param("ids") final List ids); } From afc5d47ec3b32a0d5e762f301648121b5a933b43 Mon Sep 17 00:00:00 2001 From: Stephanya Casanova Date: Mon, 12 Aug 2024 11:42:37 +0200 Subject: [PATCH 10/13] [frontend] Add info message of starting date mandatory on scenario (#1319) --- .../java/io/openbas/rest/mapper/MapperApi.java | 7 +++++-- .../rest/mapper/form/ExportMapperInput.java | 3 +++ .../ImportUploaderInjectFromXlsInjects.tsx | 16 ++++++++++++++-- .../settings/data_ingestion/XlsMapperPopover.tsx | 1 + openbas-front/src/utils/Localization.js | 1 + openbas-front/src/utils/api-types.d.ts | 1 + 6 files changed, 25 insertions(+), 4 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/rest/mapper/MapperApi.java b/openbas-api/src/main/java/io/openbas/rest/mapper/MapperApi.java index 861c1e5191..d6ea76a5b7 100644 --- a/openbas-api/src/main/java/io/openbas/rest/mapper/MapperApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/mapper/MapperApi.java @@ -90,8 +90,11 @@ public void exportMappers( HttpServletResponse response) { try { String jsonMappers = mapperService.exportMappers(exportMapperInput.getIdsToExport()); - String rightNow = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); - String filename = MessageFormat.format("mappers_{0}.json", rightNow); + + String rightNow = DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDateTime.now()); + String name = exportMapperInput.getName().replace("(Import)", "").replace(" ", ""); + String exportFileName = name.length() > 15 ? name.substring(0, 15) : name; + String filename = MessageFormat.format("{0}-{1}.json", exportFileName, rightNow); response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename); response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); diff --git a/openbas-api/src/main/java/io/openbas/rest/mapper/form/ExportMapperInput.java b/openbas-api/src/main/java/io/openbas/rest/mapper/form/ExportMapperInput.java index c6f28b68f7..91ccc25d44 100644 --- a/openbas-api/src/main/java/io/openbas/rest/mapper/form/ExportMapperInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/mapper/form/ExportMapperInput.java @@ -11,6 +11,9 @@ @Data public class ExportMapperInput { + @JsonProperty("export_mapper_name") + private String name; + @NotNull(message = MANDATORY_MESSAGE) @JsonProperty("ids_to_export") private List idsToExport; diff --git a/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXlsInjects.tsx b/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXlsInjects.tsx index f2210c4dd7..16cc8c5109 100644 --- a/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXlsInjects.tsx +++ b/openbas-front/src/admin/components/common/injects/ImportUploaderInjectFromXlsInjects.tsx @@ -1,5 +1,6 @@ -import { Autocomplete as MuiAutocomplete, Box, Button, MenuItem, TextField } from '@mui/material'; +import { Autocomplete as MuiAutocomplete, Box, Button, MenuItem, TextField, Tooltip } from '@mui/material'; import { TableViewOutlined } from '@mui/icons-material'; +import { InformationOutline } from 'mdi-material-ui'; import React, { FunctionComponent, SyntheticEvent, useContext, useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -230,9 +231,20 @@ const ImportUploaderInjectFromXlsInjects: FunctionComponent = ({ fullWidth: true, error: !!fieldState.error, helperText: fieldState.error && fieldState.error?.message, + label: ( + + {t('Start date')} + + + + + ), }, }} - label={t('Start date')} /> )} />} diff --git a/openbas-front/src/admin/components/settings/data_ingestion/XlsMapperPopover.tsx b/openbas-front/src/admin/components/settings/data_ingestion/XlsMapperPopover.tsx index d39b8b99c1..49216e4db8 100644 --- a/openbas-front/src/admin/components/settings/data_ingestion/XlsMapperPopover.tsx +++ b/openbas-front/src/admin/components/settings/data_ingestion/XlsMapperPopover.tsx @@ -47,6 +47,7 @@ const XlsMapperPopover: FunctionComponent = ({ const exportMapperAction = () => { exportMapper({ ids_to_export: [mapper.import_mapper_id], + export_mapper_name: mapper.import_mapper_name, }).then( (result:{ data: string, filename: string }) => { download(JSON.stringify(result.data, null, 2), result.filename, 'application/json'); diff --git a/openbas-front/src/utils/Localization.js b/openbas-front/src/utils/Localization.js index 81cf585b3c..df1f567a23 100644 --- a/openbas-front/src/utils/Localization.js +++ b/openbas-front/src/utils/Localization.js @@ -1165,6 +1165,7 @@ const i18n = { 'Do you want to delete this representation?': 'Voulez-vous supprimer cette représentation ?', 'Trigger time': 'Heure de lancement', 'Attribute mapping configuration': 'Configuration d\'attribut de mappeur', + 'The imported file contains absolute dates (ex.: 9h30). A starting date must be provided for the Scenario to be build': 'Le fichier importé contient des dates absolues (ex.: 9h30). Une date de début doit être fournie pour que le scénario puisse être créé', Test: 'Test', 'Do you want to delete this XLS mapper ?': 'Voulez-vous supprimer ce mappage XLS ?', Expectation_name: 'Nom de l\'attendu', diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 2e21a9d0a3..de9caca2c7 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -918,6 +918,7 @@ export interface ExpectationUpdateInput { export interface ExportMapperInput { ids_to_export: string[]; + export_mapper_name?: string; } export interface Filter { From 9e0de0a8f9b48799b05090a4d6975b2471ae7e8e Mon Sep 17 00:00:00 2001 From: Filigran Automation Date: Mon, 12 Aug 2024 11:53:45 +0000 Subject: [PATCH 11/13] [all] Release 1.4.0 --- openbas-api/pom.xml | 4 ++-- openbas-framework/pom.xml | 4 ++-- openbas-model/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openbas-api/pom.xml b/openbas-api/pom.xml index 29776599f3..82c4654571 100644 --- a/openbas-api/pom.xml +++ b/openbas-api/pom.xml @@ -6,7 +6,7 @@ io.openbas openbas-platform - 1.3.1 + 1.4.0 openbas-api @@ -35,7 +35,7 @@ io.openbas openbas-framework - 1.3.1 + 1.4.0 com.rabbitmq diff --git a/openbas-framework/pom.xml b/openbas-framework/pom.xml index e82792dfd8..c525b9bf63 100644 --- a/openbas-framework/pom.xml +++ b/openbas-framework/pom.xml @@ -6,7 +6,7 @@ io.openbas openbas-platform - 1.3.1 + 1.4.0 openbas-framework @@ -17,7 +17,7 @@ io.openbas openbas-model - 1.3.1 + 1.4.0 com.rabbitmq diff --git a/openbas-model/pom.xml b/openbas-model/pom.xml index d3edd3670c..d1fcc4856e 100644 --- a/openbas-model/pom.xml +++ b/openbas-model/pom.xml @@ -6,7 +6,7 @@ io.openbas openbas-platform - 1.3.1 + 1.4.0 openbas-model diff --git a/pom.xml b/pom.xml index 52a364bd22..90af5835a0 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ io.openbas openbas-platform - 1.3.1 + 1.4.0 pom OpenBAS platform From a1b7c1044cc0081eca20bc7174f619f4d10c324f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:31:58 +0200 Subject: [PATCH 12/13] Update dependency axios to v1.7.4 [SECURITY] (#1322) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- openbas-front/package.json | 2 +- openbas-front/yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openbas-front/package.json b/openbas-front/package.json index 3a7ce7ea19..e5206060ee 100644 --- a/openbas-front/package.json +++ b/openbas-front/package.json @@ -23,7 +23,7 @@ "@redux-devtools/extension": "3.3.0", "@uiw/react-md-editor": "4.0.4", "apexcharts": "3.51.0", - "axios": "1.7.2", + "axios": "1.7.4", "ckeditor5-custom-build": "link:packages/ckeditor5-custom-build", "classnames": "2.5.1", "cronstrue": "2.50.0", diff --git a/openbas-front/yarn.lock b/openbas-front/yarn.lock index b959191b45..0f6677c94f 100644 --- a/openbas-front/yarn.lock +++ b/openbas-front/yarn.lock @@ -5864,14 +5864,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.7.2": - version: 1.7.2 - resolution: "axios@npm:1.7.2" +"axios@npm:1.7.4": + version: 1.7.4 + resolution: "axios@npm:1.7.4" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10c0/cbd47ce380fe045313364e740bb03b936420b8b5558c7ea36a4563db1258c658f05e40feb5ddd41f6633fdd96d37ac2a76f884dad599c5b0224b4c451b3fa7ae + checksum: 10c0/5ea1a93140ca1d49db25ef8e1bd8cfc59da6f9220159a944168860ad15a2743ea21c5df2967795acb15cbe81362f5b157fdebbea39d53117ca27658bab9f7f17 languageName: node linkType: hard @@ -12335,7 +12335,7 @@ __metadata: "@uiw/react-md-editor": "npm:4.0.4" "@vitejs/plugin-react": "npm:4.3.0" apexcharts: "npm:3.51.0" - axios: "npm:1.7.2" + axios: "npm:1.7.4" chokidar: "npm:3.6.0" ckeditor5-custom-build: "link:packages/ckeditor5-custom-build" classnames: "npm:2.5.1" From 26709d217f8c3de6666ba79c6a19bd7408c1ed55 Mon Sep 17 00:00:00 2001 From: Johanah LEKEU <49673066+johanah29@users.noreply.github.com> Date: Mon, 19 Aug 2024 12:00:52 +0200 Subject: [PATCH 13/13] [Frontend|Backend]Be able to test emails and sms related injects (#1295) --- .../V3_31__Add_Injects_tests_statuses.java | 38 + .../io/openbas/rest/inject/InjectApi.java | 29 +- .../InjectTestStatusApi.java | 41 + .../service/InjectTestStatusService.java | 79 ++ .../java/io/openbas/rest/TeamApiTest.java | 2 +- openbas-front/src/actions/Inject.js | 2 +- .../inject_test/inject-test-actions.ts | 17 + .../common/injects/InjectPopover.tsx | 80 +- .../components/common/injects/Injects.js | 1 + .../components/injects/InjectTestDetail.tsx | 161 +++ .../components/injects/InjectTestList.tsx | 190 +++ .../components/scenarios/scenario/Index.tsx | 8 + .../scenario/injects/ScenarioInjects.tsx | 1 + .../scenario/tests/ScenarioTests.tsx | 16 + .../simulations/simulation/Index.tsx | 10 + .../simulation/injects/ExerciseInjects.tsx | 17 +- .../simulation/tests/ExerciseTests.tsx | 15 + openbas-front/src/utils/Localization.js | 4 + openbas-front/src/utils/api-types.d.ts | 44 +- .../database/model/BaseInjectStatus.java | 105 ++ .../io/openbas/database/model/Inject.java | 1036 +++++++++-------- .../openbas/database/model/InjectStatus.java | 96 +- .../database/model/InjectTestStatus.java | 78 ++ .../InjectTestStatusRepository.java | 27 + 24 files changed, 1453 insertions(+), 644 deletions(-) create mode 100644 openbas-api/src/main/java/io/openbas/migration/V3_31__Add_Injects_tests_statuses.java create mode 100644 openbas-api/src/main/java/io/openbas/rest/inject_test_status/InjectTestStatusApi.java create mode 100644 openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java create mode 100644 openbas-front/src/actions/inject_test/inject-test-actions.ts create mode 100644 openbas-front/src/admin/components/injects/InjectTestDetail.tsx create mode 100644 openbas-front/src/admin/components/injects/InjectTestList.tsx create mode 100644 openbas-front/src/admin/components/scenarios/scenario/tests/ScenarioTests.tsx create mode 100644 openbas-front/src/admin/components/simulations/simulation/tests/ExerciseTests.tsx create mode 100644 openbas-model/src/main/java/io/openbas/database/model/BaseInjectStatus.java create mode 100644 openbas-model/src/main/java/io/openbas/database/model/InjectTestStatus.java create mode 100644 openbas-model/src/main/java/io/openbas/database/repository/InjectTestStatusRepository.java 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); +}