From faa80dfb04ac20215872f988c0c795a34a1e543b Mon Sep 17 00:00:00 2001 From: Johanah LEKEU Date: Wed, 25 Sep 2024 09:01:52 +0200 Subject: [PATCH] [backend|frontend]Add the ability to customize the expiration time for expectations --- .../ExpectationsExpirationManagerService.java | 135 ++++++++++-------- ...d_column_expiration_time_expectations.java | 32 +++++ .../injects/expectations/Expectation.ts | 7 + .../expectations/ExpectationFormCreate.tsx | 63 +++++++- .../expectations/ExpectationFormUpdate.tsx | 60 +++++++- .../expectations/ExpectationFormUtils.ts | 9 +- .../expectations/ExpectationPopover.tsx | 17 ++- .../expectations/InjectAddExpectation.tsx | 16 ++- openbas-front/src/utils/Localization.js | 2 + openbas-front/src/utils/api-types.d.ts | 13 +- .../database/model/InjectExpectation.java | 6 + 11 files changed, 271 insertions(+), 89 deletions(-) create mode 100644 openbas-api/src/main/java/io/openbas/migration/V3_40__Add_column_expiration_time_expectations.java diff --git a/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/service/ExpectationsExpirationManagerService.java b/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/service/ExpectationsExpirationManagerService.java index 64fc861136..476256d357 100644 --- a/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/service/ExpectationsExpirationManagerService.java +++ b/openbas-api/src/main/java/io/openbas/collectors/expectations_expiration_manager/service/ExpectationsExpirationManagerService.java @@ -20,74 +20,83 @@ @Log public class ExpectationsExpirationManagerService { - private final InjectExpectationService injectExpectationService; - private final ExpectationsExpirationManagerConfig config; + private final InjectExpectationService injectExpectationService; + private final ExpectationsExpirationManagerConfig config; - @Transactional(rollbackFor = Exception.class) - public void computeExpectations() { - List expectations = this.injectExpectationService.expectationsNotFill(); - if (!expectations.isEmpty()) { - this.computeExpectationsForAssets(expectations); - this.computeExpectationsForAssetGroups(expectations); - this.computeExpectations(expectations); - } + @Transactional(rollbackFor = Exception.class) + public void computeExpectations() { + List expectations = this.injectExpectationService.expectationsNotFill(); + if (!expectations.isEmpty()) { + this.computeExpectationsForAssets(expectations); + this.computeExpectationsForAssetGroups(expectations); + this.computeExpectations(expectations); } + } - // -- PRIVATE -- + // -- PRIVATE -- - private void computeExpectations(@NotNull final List expectations) { - List expectationAssets = expectations.stream().toList(); - expectationAssets.forEach((expectation) -> { - // Maximum time for detection - if (isExpired(expectation, this.config.getExpirationTimeInMinute())) { - String result = computeFailedMessage(expectation.getType()); - this.injectExpectationService.computeExpectation( - expectation, - this.config.getId(), - "collector", - PRODUCT_NAME, - result, - false - ); - } - }); - } + private void computeExpectations(@NotNull final List expectations) { + List expectationAssets = expectations.stream().toList(); + expectationAssets.forEach((expectation) -> { + Long userExpirationTime = expectation.getExpirationTime(); + if (userExpirationTime != null) { + // Maximum time for detection + if (isExpired(expectation, Math.toIntExact(userExpirationTime / 60))) { + String result = computeFailedMessage(expectation.getType()); + this.injectExpectationService.computeExpectation( + expectation, + this.config.getId(), + "collector", + PRODUCT_NAME, + result, + false + ); + } + } - private void computeExpectationsForAssets(@NotNull final List expectations) { - List expectationAssets = expectations.stream().filter(e -> e.getAsset() != null).toList(); - expectationAssets.forEach((expectation) -> { - // Maximum time for detection - if (isExpired(expectation, this.config.getAssetExpirationTimeInMinute())) { - String result = computeFailedMessage(expectation.getType()); - this.injectExpectationService.computeExpectation( - expectation, - this.config.getId(), - "collector", - PRODUCT_NAME, - result, - false - ); - } - }); - } + }); + } - private void computeExpectationsForAssetGroups(@NotNull final List expectations) { - List expectationAssetGroups = expectations.stream().filter(e -> e.getAssetGroup() != null).toList(); - expectationAssetGroups.forEach((expectationAssetGroup -> { - List expectationAssets = this.injectExpectationService.expectationsForAssets( - expectationAssetGroup.getInject(), expectationAssetGroup.getAssetGroup(), expectationAssetGroup.getType() - ); - // Every expectation assets are filled - if (expectationAssets.stream().noneMatch(e -> e.getResults().isEmpty())) { - this.injectExpectationService.computeExpectationGroup( - expectationAssetGroup, - expectationAssets, - this.config.getId(), - "collector", - PRODUCT_NAME - ); - } - })); - } + private void computeExpectationsForAssets(@NotNull final List expectations) { + List expectationAssets = expectations.stream().filter(e -> e.getAsset() != null).toList(); + expectationAssets.forEach((expectation) -> { + Long userExpirationTime = expectation.getExpirationTime(); + if (userExpirationTime != null) { + // Maximum time for detection + if (isExpired(expectation, Math.toIntExact(userExpirationTime / 60))) { + String result = computeFailedMessage(expectation.getType()); + this.injectExpectationService.computeExpectation( + expectation, + this.config.getId(), + "collector", + PRODUCT_NAME, + result, + false + ); + } + } + + }); + } + + private void computeExpectationsForAssetGroups(@NotNull final List expectations) { + List expectationAssetGroups = expectations.stream().filter(e -> e.getAssetGroup() != null) + .toList(); + expectationAssetGroups.forEach((expectationAssetGroup -> { + List expectationAssets = this.injectExpectationService.expectationsForAssets( + expectationAssetGroup.getInject(), expectationAssetGroup.getAssetGroup(), expectationAssetGroup.getType() + ); + // Every expectation assets are filled + if (expectationAssets.stream().noneMatch(e -> e.getResults().isEmpty())) { + this.injectExpectationService.computeExpectationGroup( + expectationAssetGroup, + expectationAssets, + this.config.getId(), + "collector", + PRODUCT_NAME + ); + } + })); + } } diff --git a/openbas-api/src/main/java/io/openbas/migration/V3_40__Add_column_expiration_time_expectations.java b/openbas-api/src/main/java/io/openbas/migration/V3_40__Add_column_expiration_time_expectations.java new file mode 100644 index 0000000000..0472813e00 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/migration/V3_40__Add_column_expiration_time_expectations.java @@ -0,0 +1,32 @@ +package io.openbas.migration; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; + +import java.sql.Connection; +import java.sql.Statement; + +public class V3_40__Add_column_expiration_time_expectations extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + Statement select = context.getConnection().createStatement(); + Connection connection = context.getConnection(); + Statement statement = connection.createStatement(); + long technicalMinutesExpirationTime = 60L; + long manualMinutesExpirationTime = 360L; + + select.execute("ALTER TABLE injects_expectations ADD inject_expiration_time bigint;"); + select.execute( + "UPDATE injects_expectations SET inject_expiration_time = " + technicalMinutesExpirationTime + " " + + "WHERE inject_expectation_type = 'DETECTION' OR inject_expectation_type = 'PREVENTION';"); + + select.execute( + "UPDATE injects_expectations SET inject_expiration_time = " + manualMinutesExpirationTime + " " + + "WHERE inject_expectation_type = 'MANUAL' OR inject_expectation_type = 'CHALLENGE' " + + "OR inject_expectation_type = 'ARTICLE' OR inject_expectation_type = 'DOCUMENT' OR inject_expectation_type = 'TEXT';"); + + select.execute( + "ALTER TABLE injects_expectations ALTER COLUMN inject_expiration_time SET NOT NULL;"); + } +} 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 c1a2b677b1..6b72376c13 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/Expectation.ts +++ b/openbas-front/src/admin/components/common/injects/expectations/Expectation.ts @@ -14,6 +14,13 @@ export interface ExpectationInput { expectation_description?: string; expectation_score: number; expectation_expectation_group: boolean; + expectation_expiration_time: number; +} + +export interface ExpectationInputForm extends Omit { + expiration_time_days: number; + expiration_time_hours: number; + expiration_time_minutes: number; } export enum ExpectationType { 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 3c5197d3f3..3d2cf79032 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx +++ b/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormCreate.tsx @@ -1,14 +1,15 @@ import React, { FunctionComponent, SyntheticEvent, useEffect } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { Alert, Button, InputLabel, MenuItem, Select as MUISelect, TextField as MuiTextField, Typography } from '@mui/material'; +import { Alert, Button, InputLabel, MenuItem, Select as MUISelect, TextField, TextField as MuiTextField, Typography } from '@mui/material'; import { makeStyles } from '@mui/styles'; -import type { ExpectationInput } from './Expectation'; +import { ExpectationInput, ExpectationInputForm } from './Expectation'; import { formProps, infoMessage } from './ExpectationFormUtils'; import { useFormatter } from '../../../../../components/i18n'; import type { Theme } from '../../../../../components/Theme'; import ExpectationGroupField from './field/ExpectationGroupField'; import { isTechnicalExpectation } from './ExpectationUtils'; import ScaleBar from '../../../../../components/scalebar/ScaleBar'; +import { splitDuration } from '../../../../../utils/Time'; const useStyles = makeStyles((theme: Theme) => ({ marginTop_2: { @@ -20,11 +21,26 @@ const useStyles = makeStyles((theme: Theme) => ({ gap: theme.spacing(2), marginTop: theme.spacing(2), }, + duration: { + marginTop: 20, + width: '100%', + display: 'flex', + justifyContent: 'space-between', + border: `1px solid ${theme.palette.primary.main}`, + borderRadius: 4, + padding: 15, + }, + trigger: { + fontFamily: 'Consolas, monaco, monospace', + fontSize: 12, + paddingTop: 15, + color: theme.palette.primary.main, + }, })); interface Props { predefinedExpectations: ExpectationInput[]; - onSubmit: SubmitHandler; + onSubmit: SubmitHandler; handleClose: () => void; } @@ -45,6 +61,7 @@ const ExpectationFormCreate: FunctionComponent = ({ expectation_description: predefinedExpectation.expectation_description ?? '', expectation_score: predefinedExpectation.expectation_score > 0 ? predefinedExpectation.expectation_score : 100, expectation_expectation_group: predefinedExpectation.expectation_expectation_group ?? false, + expectation_expiration_time: predefinedExpectation.expectation_expiration_time, }; } return { @@ -53,11 +70,19 @@ const ExpectationFormCreate: FunctionComponent = ({ expectation_description: '', expectation_score: 100, expectation_expectation_group: false, + expectation_expiration_time: 3600, }; }; const predefinedTypes = predefinedExpectations.map((e) => e.expectation_type); - const initialValues = computeValuesFromType(predefinedTypes[0]); + const valuesFromComputedTypes = computeValuesFromType(predefinedTypes[0]); + const expirationTime = splitDuration(valuesFromComputedTypes.expectation_expiration_time || 0); + const initialValues: ExpectationInputForm = { + ...valuesFromComputedTypes, + expiration_time_days: parseInt(expirationTime.days, 10), + expiration_time_hours: parseInt(expirationTime.hours, 10), + expiration_time_minutes: parseInt(expirationTime.minutes, 10), + }; const { register, @@ -66,8 +91,8 @@ const ExpectationFormCreate: FunctionComponent = ({ watch, reset, getValues, - control, - } = useForm(formProps(initialValues, t)); + } = useForm(formProps(initialValues, t)); + const { control } = useForm(); const watchType = watch('expectation_type'); const handleSubmitWithoutPropagation = (e: SyntheticEvent) => { @@ -128,6 +153,32 @@ const ExpectationFormCreate: FunctionComponent = ({ } inputProps={register('expectation_description')} /> +
+
+ {t('Expiration time')} +
+ + + +
{t('Scores')} diff --git a/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormUpdate.tsx b/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormUpdate.tsx index 5b9118844d..728d440dfc 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormUpdate.tsx +++ b/openbas-front/src/admin/components/common/injects/expectations/ExpectationFormUpdate.tsx @@ -1,13 +1,14 @@ import React, { FunctionComponent, SyntheticEvent } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { Alert, Button, InputLabel, MenuItem, Select as MUISelect, TextField as MuiTextField } from '@mui/material'; +import { Alert, Button, InputLabel, MenuItem, Select as MUISelect, TextField, TextField as MuiTextField } from '@mui/material'; import { makeStyles } from '@mui/styles'; import { formProps, infoMessage } from './ExpectationFormUtils'; -import type { ExpectationInput } from './Expectation'; +import { ExpectationInput, ExpectationInputForm } from './Expectation'; import { useFormatter } from '../../../../../components/i18n'; import type { Theme } from '../../../../../components/Theme'; import ExpectationGroupField from './field/ExpectationGroupField'; import { isTechnicalExpectation } from './ExpectationUtils'; +import { splitDuration } from '../../../../../utils/Time'; const useStyles = makeStyles((theme: Theme) => ({ marginTop_2: { @@ -19,10 +20,25 @@ const useStyles = makeStyles((theme: Theme) => ({ gap: theme.spacing(2), marginTop: theme.spacing(2), }, + duration: { + marginTop: 20, + width: '100%', + display: 'flex', + justifyContent: 'space-between', + border: `1px solid ${theme.palette.primary.main}`, + borderRadius: 4, + padding: 15, + }, + trigger: { + fontFamily: 'Consolas, monaco, monospace', + fontSize: 12, + paddingTop: 15, + color: theme.palette.primary.main, + }, })); interface Props { - onSubmit: SubmitHandler; + onSubmit: SubmitHandler; handleClose: () => void; initialValues: ExpectationInput; } @@ -35,13 +51,21 @@ const ExpectationFormUpdate: FunctionComponent = ({ const { t } = useFormatter(); const classes = useStyles(); + const expirationTime = splitDuration(initialValues.expectation_expiration_time || 0); + const formInitialValues: ExpectationInputForm = { + ...initialValues, + expiration_time_days: parseInt(expirationTime.days, 10), + expiration_time_hours: parseInt(expirationTime.hours, 10), + expiration_time_minutes: parseInt(expirationTime.minutes, 10), + }; + const { register, handleSubmit, formState: { errors, isSubmitting, isValid }, getValues, - control, - } = useForm(formProps(initialValues, t)); + } = useForm(formProps(formInitialValues, t)); + const { control } = useForm(); const handleSubmitWithoutPropagation = (e: SyntheticEvent) => { e.preventDefault(); @@ -96,6 +120,32 @@ const ExpectationFormUpdate: FunctionComponent = ({ } inputProps={register('expectation_description')} /> +
+
+ {t('Expiration time')} +
+ + + +
string) => { if (type === 'ARTICLE') { @@ -14,14 +14,17 @@ export const infoMessage = (type: string, t: (key: string) => string) => { return ''; }; -export const formProps = (initialValues: ExpectationInput, t: (key: string) => string): UseFormProps => ({ +export const formProps = (initialValues: ExpectationInputForm, t: (key: string) => string): UseFormProps => ({ mode: 'onTouched', - resolver: zodResolver(zodImplement().with({ + resolver: zodResolver(zodImplement().with({ 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().min(1, 'Score must be greater than 0'), expectation_expectation_group: z.coerce.boolean(), + expiration_time_days: z.coerce.number().min(0), + expiration_time_hours: z.coerce.number().min(0), + expiration_time_minutes: z.coerce.number().min(0), })), defaultValues: initialValues, }); diff --git a/openbas-front/src/admin/components/common/injects/expectations/ExpectationPopover.tsx b/openbas-front/src/admin/components/common/injects/expectations/ExpectationPopover.tsx index 2c8016ac01..057854417c 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/ExpectationPopover.tsx +++ b/openbas-front/src/admin/components/common/injects/expectations/ExpectationPopover.tsx @@ -4,7 +4,7 @@ import { MoreVert } from '@mui/icons-material'; import Transition from '../../../../../components/common/Transition'; import { useFormatter } from '../../../../../components/i18n'; import ExpectationFormUpdate from './ExpectationFormUpdate'; -import type { ExpectationInput } from './Expectation'; +import type { ExpectationInput, ExpectationInputForm } from './Expectation'; import Dialog from '../../../../../components/common/Dialog'; import { PermissionsContext } from '../../Context'; @@ -35,6 +35,7 @@ const ExpectationPopover: FunctionComponent = ({ expectation_description: expectation.expectation_description ?? '', expectation_score: expectation.expectation_score ?? 100, expectation_expectation_group: expectation.expectation_expectation_group ?? false, + expectation_expiration_time: expectation.expectation_expiration_time ?? 21600, }; // Popover @@ -51,8 +52,18 @@ const ExpectationPopover: FunctionComponent = ({ }; const handleCloseEdit = () => setOpenEdit(false); - const onSubmitEdit = (data: ExpectationInput) => { - handleUpdate(data, index); + const onSubmitEdit = (data: ExpectationInputForm) => { + const values: ExpectationInput = { + expectation_type: data.expectation_type, + expectation_name: data.expectation_name, + expectation_description: data.expectation_description, + expectation_score: data.expectation_score, + expectation_expectation_group: data.expectation_expectation_group, + expectation_expiration_time: data.expiration_time_days * 3600 * 24 + + data.expiration_time_hours * 3600 + + data.expiration_time_minutes * 60, + }; + handleUpdate(values, index); handleCloseEdit(); }; diff --git a/openbas-front/src/admin/components/common/injects/expectations/InjectAddExpectation.tsx b/openbas-front/src/admin/components/common/injects/expectations/InjectAddExpectation.tsx index 20713644e2..68c8fe4c79 100644 --- a/openbas-front/src/admin/components/common/injects/expectations/InjectAddExpectation.tsx +++ b/openbas-front/src/admin/components/common/injects/expectations/InjectAddExpectation.tsx @@ -6,7 +6,7 @@ import type { Theme } from '../../../../../components/Theme'; import { useFormatter } from '../../../../../components/i18n'; import Dialog from '../../../../../components/common/Dialog'; import ExpectationFormCreate from './ExpectationFormCreate'; -import type { ExpectationInput } from './Expectation'; +import type { ExpectationInput, ExpectationInputForm } from './Expectation'; import { PermissionsContext } from '../../Context'; const useStyles = makeStyles((theme: Theme) => ({ @@ -41,8 +41,18 @@ const InjectAddExpectation: FunctionComponent = ({ const handleClose = () => setOpenDialog(false); // Form - const onSubmit = (data: ExpectationInput) => { - handleAddExpectation(data); + const onSubmit = (data: ExpectationInputForm) => { + const values: ExpectationInput = { + expectation_type: data.expectation_type, + expectation_name: data.expectation_name, + expectation_description: data.expectation_description, + expectation_score: data.expectation_score, + expectation_expectation_group: data.expectation_expectation_group, + expectation_expiration_time: data.expiration_time_days * 3600 * 24 + + data.expiration_time_hours * 3600 + + data.expiration_time_minutes * 60, + }; + handleAddExpectation(values); handleClose(); }; diff --git a/openbas-front/src/utils/Localization.js b/openbas-front/src/utils/Localization.js index b75db1a34f..d9d2d2faa4 100644 --- a/openbas-front/src/utils/Localization.js +++ b/openbas-front/src/utils/Localization.js @@ -584,6 +584,7 @@ const i18n = { 'Send the questionnaire': 'Envoyer le questionnaire', 'Reset answers': 'Réinitialiser les réponses', hours: 'heures', + 'Expiration time': 'Temps d\'expiration', Apply: 'Appliquer', players: 'joueurs', Explanation: 'Explication', @@ -1590,6 +1591,7 @@ const i18n = { Disabled: '禁用', Days: '天', Hours: '小时', + 'Expiration time': '过期时间', Minutes: '分钟', Seconds: '秒', 'Try the inject': '尝试注入', diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index f2df60a2bc..f4de9c08e0 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -1082,6 +1082,8 @@ export interface Inject { inject_teams?: Team[]; inject_testable?: boolean; inject_title: string; + /** @format date-time */ + inject_trigger_now_date?: string; inject_type?: string; /** @format date-time */ inject_updated_at: string; @@ -1136,6 +1138,8 @@ export interface InjectExpectation { /** @format date-time */ inject_expectation_updated_at?: string; inject_expectation_user?: User; + /** @format int64 */ + inject_expiration_time: number; listened?: boolean; targetId?: string; } @@ -1206,7 +1210,7 @@ export interface InjectInput { inject_country?: string; /** @format int64 */ inject_depends_duration?: number; - inject_depends_from_another?: string; + inject_depends_on?: string; inject_description?: string; inject_documents?: InjectDocumentInput[]; inject_injector_contract?: string; @@ -1395,11 +1399,6 @@ export interface InjectUpdateStatusInput { status?: string; } -export interface InjectUpdateTriggerInput { - /** @format int64 */ - inject_depends_duration?: number; -} - export interface Injector { injector_category?: string; /** @format date-time */ @@ -2339,6 +2338,7 @@ export interface Payload { /** @format date-time */ payload_created_at: string; payload_description?: string; + payload_elevation_required?: boolean; payload_external_id?: string; payload_id: string; payload_name: string; @@ -2416,6 +2416,7 @@ export interface PayloadUpsertInput { payload_cleanup_executor?: string; payload_collector?: string; payload_description?: string; + payload_elevation_required?: boolean; payload_external_id: string; payload_name: string; payload_platforms?: string[]; 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 d32495c1ad..d80297a5f7 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 @@ -117,6 +117,12 @@ public EXPECTATION_STATUS getResponse() { @NotNull private Double expectedScore; + @Setter + @Column(name = "inject_expiration_time") + @JsonProperty("inject_expiration_time") + @NotNull + private Long expirationTime = 60L; + @Setter @Column(name = "inject_expectation_created_at") @JsonProperty("inject_expectation_created_at")