From ac81b88022ec9622f1e9d3e7b0d81de1803fcaa1 Mon Sep 17 00:00:00 2001 From: Samuel Hassine Date: Fri, 31 May 2024 12:31:16 +0200 Subject: [PATCH] [backend/frontend] Introduce inject readiness (#1041) --- .../injectors/caldera/CalderaContract.java | 16 +++- .../injectors/caldera/CalderaExecutor.java | 2 +- .../caldera/client/model/Executor.java | 1 + .../atomic_testing/form/InjectResultDTO.java | 3 + .../scheduler/jobs/InjectsExecutionJob.java | 18 +++++ .../io/openbas/utils/AtomicTestingMapper.java | 79 ++++++++++--------- .../atomic_testing/AtomicTestingHeader.tsx | 37 ++++++--- .../components/common/injects/Injects.js | 10 +-- openbas-front/src/utils/api-types.d.ts | 39 ++++++++- .../io/openbas/database/model/Inject.java | 31 +++++++- .../database/model/InjectorContract.java | 6 ++ 11 files changed, 175 insertions(+), 67 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 41cd48122e..3525c41e60 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 @@ -1,10 +1,7 @@ package io.openbas.injectors.caldera; import io.openbas.injector_contract.*; -import io.openbas.injector_contract.fields.ContractAsset; -import io.openbas.injector_contract.fields.ContractAssetGroup; -import io.openbas.injector_contract.fields.ContractExpectations; -import io.openbas.injector_contract.fields.ContractSelect; +import io.openbas.injector_contract.fields.*; import io.openbas.database.model.Endpoint; import io.openbas.helper.SupportedLanguage; import io.openbas.injectors.caldera.client.model.Ability; @@ -20,6 +17,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static io.openbas.executors.caldera.service.CalderaExecutorService.toPlatform; @@ -119,6 +118,15 @@ private List abilityContracts(@NotNull final ContractConfig contractCo builder.optional(expectationsField); List platforms = new ArrayList<>(); ability.getExecutors().forEach(executor -> { + String command = executor.getCommand(); + if (command != null && !command.isEmpty()) { + Matcher matcher = Pattern.compile("#\\{(.*?)\\}").matcher(command); + while (matcher.find()) { + if (!matcher.group(1).isEmpty()) { + builder.mandatory(ContractText.textField(matcher.group(1), matcher.group(1))); + } + } + } if (!executor.getPlatform().equals("unknown")) { String platform = toPlatform(executor.getPlatform()).name(); if (!platforms.contains(platform)) { diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaExecutor.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaExecutor.java index 0d15c018be..6efcf2d652 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaExecutor.java +++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/CalderaExecutor.java @@ -59,10 +59,10 @@ public ExecutionProcess process(@NotNull final Execution execution, @NotNull fin List asyncIds = new ArrayList<>(); List expectations = new ArrayList<>(); // Execute inject for all assets - String contract = inject.getInjectorContract().getId(); if (assets.isEmpty()) { execution.addTrace(traceError("Found 0 asset to execute the ability on (likely this inject does not have any target or the targeted asset is inactive and has been purged)")); } + String contract = inject.getInjectorContract().getId(); assets.forEach((asset, aBoolean) -> { try { Endpoint executionEndpoint = this.findAndRegisterAssetForExecution(injection.getInjection().getInject(), asset); diff --git a/openbas-api/src/main/java/io/openbas/injectors/caldera/client/model/Executor.java b/openbas-api/src/main/java/io/openbas/injectors/caldera/client/model/Executor.java index acc9477c34..c4bd00acee 100644 --- a/openbas-api/src/main/java/io/openbas/injectors/caldera/client/model/Executor.java +++ b/openbas-api/src/main/java/io/openbas/injectors/caldera/client/model/Executor.java @@ -9,4 +9,5 @@ public class Executor { private String name; private String platform; + private String command; } diff --git a/openbas-api/src/main/java/io/openbas/rest/atomic_testing/form/InjectResultDTO.java b/openbas-api/src/main/java/io/openbas/rest/atomic_testing/form/InjectResultDTO.java index ca879651b0..4f910ee429 100644 --- a/openbas-api/src/main/java/io/openbas/rest/atomic_testing/form/InjectResultDTO.java +++ b/openbas-api/src/main/java/io/openbas/rest/atomic_testing/form/InjectResultDTO.java @@ -83,6 +83,9 @@ public class InjectResultDTO { @JsonProperty("injects_documents") private List documents; + @JsonProperty("inject_ready") + private Boolean isReady; + @JsonProperty("inject_updated_at") private Instant updatedAt = now(); } diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index 5b427dce31..5edc272487 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -185,6 +185,24 @@ private void executeInternal(ExecutableInject executableInject) { private void executeInject(ExecutableInject executableInject) { // Depending on injector type (internal or external) execution must be done differently Inject inject = executableInject.getInjection().getInject(); + if( !inject.isReady() ) { + // Status + if( inject.getStatus().isEmpty() ) { + InjectStatus status = new InjectStatus(); + status.getTraces().add(InjectStatusExecution.traceError("The inject is not ready to be executed (missing mandatory fields)")); + status.setName(ExecutionStatus.ERROR); + status.setTrackingSentDate(Instant.now()); + status.setInject(inject); + injectStatusRepository.save(status); + } else { + InjectStatus status = inject.getStatus().get(); + status.getTraces().add(InjectStatusExecution.traceError("The inject is not ready to be executed (missing mandatory fields)")); + status.setName(ExecutionStatus.ERROR); + status.setTrackingSentDate(Instant.now()); + injectStatusRepository.save(status); + } + return; + } Injector externalInjector = injectorRepository.findByType(inject.getInjectorContract().getInjector().getType()).orElseThrow(); LOGGER.log(Level.INFO, "Executing inject " + inject.getInject().getTitle()); // Executor logics diff --git a/openbas-api/src/main/java/io/openbas/utils/AtomicTestingMapper.java b/openbas-api/src/main/java/io/openbas/utils/AtomicTestingMapper.java index 8b6e04b5fe..907619678e 100644 --- a/openbas-api/src/main/java/io/openbas/utils/AtomicTestingMapper.java +++ b/openbas-api/src/main/java/io/openbas/utils/AtomicTestingMapper.java @@ -14,55 +14,56 @@ public class AtomicTestingMapper { - public static InjectResultDTO toDtoWithTargetResults(Inject inject) { - List targets = AtomicTestingUtils.getTargetsWithResults(inject); - List targetIds = targets.stream().map(InjectTargetWithResult::getId).toList(); + public static InjectResultDTO toDtoWithTargetResults(Inject inject) { + List targets = AtomicTestingUtils.getTargetsWithResults(inject); + List targetIds = targets.stream().map(InjectTargetWithResult::getId).toList(); - return getAtomicTestingOutputBuilder(inject) - .targets(targets) - .expectationResultByTypes(AtomicTestingUtils.getExpectationResultByTypes( - getRefinedExpectations(inject, targetIds) - )) - .build(); - } + return getAtomicTestingOutputBuilder(inject) + .targets(targets) + .expectationResultByTypes(AtomicTestingUtils.getExpectationResultByTypes( + getRefinedExpectations(inject, targetIds) + )) + .build(); + } - public static InjectResultDTO toDto(Inject inject, List targets) { - List targetIds = targets.stream().map(InjectTargetWithResult::getId).toList(); + public static InjectResultDTO toDto(Inject inject, List targets) { + List targetIds = targets.stream().map(InjectTargetWithResult::getId).toList(); - return getAtomicTestingOutputBuilder(inject) - .targets(targets) - .expectationResultByTypes(AtomicTestingUtils.getExpectationResultByTypes( - getRefinedExpectations(inject, targetIds) - )) - .build(); - } + return getAtomicTestingOutputBuilder(inject) + .targets(targets) + .expectationResultByTypes(AtomicTestingUtils.getExpectationResultByTypes( + getRefinedExpectations(inject, targetIds) + )) + .build(); + } - private static InjectResultDTOBuilder getAtomicTestingOutputBuilder(Inject inject) { - return InjectResultDTO - .builder() - .id(inject.getId()) - .title(inject.getTitle()) - .description(inject.getDescription()) + private static InjectResultDTOBuilder getAtomicTestingOutputBuilder(Inject inject) { + return InjectResultDTO + .builder() + .id(inject.getId()) + .title(inject.getTitle()) + .description(inject.getDescription()) .content(inject.getContent()) .expectations(inject.getExpectations()) - .type(inject.getInjectorContract().getInjector().getType()) - .tagIds(inject.getTags().stream().map(Tag::getId).toList()) + .type(inject.getInjectorContract().getInjector().getType()) + .tagIds(inject.getTags().stream().map(Tag::getId).toList()) .documents(inject.getDocuments().stream().map(InjectDocument::getDocument).map(Document::getId).toList()) - .injectorContract(inject.getInjectorContract()) - .status(inject.getStatus().orElse(draftInjectStatus())) - .killChainPhases(inject.getKillChainPhases()) - .attackPatterns(inject.getAttackPatterns()) - .updatedAt(inject.getUpdatedAt()); - } + .injectorContract(inject.getInjectorContract()) + .status(inject.getStatus().orElse(draftInjectStatus())) + .killChainPhases(inject.getKillChainPhases()) + .attackPatterns(inject.getAttackPatterns()) + .isReady(inject.isReady()) + .updatedAt(inject.getUpdatedAt()); + } - public record ExpectationResultsByType(@NotNull ExpectationType type, - @NotNull InjectExpectation.ExpectationStatus avgResult, - @NotNull List distribution) { + public record ExpectationResultsByType(@NotNull ExpectationType type, + @NotNull InjectExpectation.ExpectationStatus avgResult, + @NotNull List distribution) { - } + } - public record ResultDistribution(@NotNull String label, @NotNull Integer value) { + public record ResultDistribution(@NotNull String label, @NotNull Integer value) { - } + } } diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingHeader.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingHeader.tsx index 74393cf7f0..9bc4e9510c 100644 --- a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingHeader.tsx +++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingHeader.tsx @@ -1,6 +1,6 @@ import React, { useContext, useState } from 'react'; import { Alert, Button, Dialog, DialogActions, DialogContent, DialogContentText, Tooltip, Typography } from '@mui/material'; -import { PlayArrowOutlined } from '@mui/icons-material'; +import { PlayArrowOutlined, SettingsOutlined } from '@mui/icons-material'; import { makeStyles } from '@mui/styles'; import { fetchInjectResultDto, tryAtomicTesting } from '../../../../actions/atomic_testings/atomic-testing-actions'; import AtomicTestingPopover from './AtomicTestingPopover'; @@ -62,17 +62,30 @@ const AtomicTestingHeader = () => {
- + {!injectResultDto.inject_ready || !injectResultDto.inject_targets || injectResultDto.inject_targets.length === 0 ? ( + + ) : ( + + )}
{ let injectStatus = inject.inject_enabled ? t('Enabled') : t('Disabled'); - if (inject.inject_content === null) { - injectStatus = t('To fill'); + if (!inject.inject_ready) { + injectStatus = t('Missing content'); } return ( { style={inlineStyles.inject_enabled} > diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index a9e0094b3b..feabd49ce2 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -491,7 +491,17 @@ export interface DryInject { export interface DryInjectStatus { status_id?: string; - status_name?: "DRAFT" | "INFO" | "QUEUING" | "EXECUTING" | "PENDING" | "PARTIAL" | "ERROR" | "SUCCESS"; + 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; @@ -879,6 +889,7 @@ export interface Inject { inject_injector_contract?: InjectorContract; inject_kill_chain_phases?: KillChainPhase[]; inject_payloads?: Asset[]; + inject_ready?: boolean; inject_scenario?: Scenario; /** @format date-time */ inject_sent_at?: string; @@ -1010,6 +1021,7 @@ export interface InjectResultDTO { inject_injector_contract: InjectorContract; /** Kill Chain Phases */ inject_kill_chain_phases: KillChainPhase[]; + inject_ready?: boolean; inject_status?: InjectStatus; /** * Specifies the categories of targetResults for atomic testing. @@ -1027,7 +1039,17 @@ export interface InjectResultDTO { export interface InjectStatus { status_id?: string; - status_name?: "DRAFT" | "INFO" | "QUEUING" | "EXECUTING" | "PENDING" | "PARTIAL" | "ERROR" | "SUCCESS"; + 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; @@ -1052,7 +1074,17 @@ export interface InjectStatusExecution { /** @format int32 */ execution_duration?: number; execution_message?: string; - execution_status?: "DRAFT" | "INFO" | "QUEUING" | "EXECUTING" | "PENDING" | "PARTIAL" | "ERROR" | "SUCCESS"; + execution_status?: + | "DRAFT" + | "INFO" + | "QUEUING" + | "EXECUTING" + | "PENDING" + | "PARTIAL" + | "ERROR" + | "MAYBE_PARTIAL_PREVENTED" + | "MAYBE_PREVENTED" + | "SUCCESS"; /** @format date-time */ execution_time?: string; } @@ -1115,6 +1147,7 @@ export interface InjectorConnection { } export interface InjectorContract { + convertedContent?: object; injector_contract_atomic_testing?: boolean; injector_contract_attack_patterns?: AttackPattern[]; injector_contract_content: string; 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 dfc2c37551..16cbd1177f 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 @@ -2,6 +2,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.node.ObjectNode; import io.openbas.annotation.Queryable; @@ -12,6 +15,7 @@ import io.openbas.helper.MultiIdListDeserializer; import io.openbas.helper.MultiIdSetDeserializer; import io.openbas.helper.MultiModelDeserializer; +import jakarta.annotation.Resource; import jakarta.persistence.*; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; @@ -25,6 +29,8 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.StreamSupport; import static io.openbas.database.model.Endpoint.ENDPOINT_TYPE; import static java.time.Duration.between; @@ -37,7 +43,6 @@ @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) -> { @@ -255,6 +260,30 @@ public long getNumberOfTargetUsers() { .reduce(Long::sum).orElse(0L); } + @JsonProperty("inject_ready") + public boolean isReady() { + InjectorContract injectorContract = getInjectorContract(); + ObjectNode content = getContent(); + if( getContent() == null ) { + return false; + } + AtomicBoolean ready = new AtomicBoolean(true); + ObjectNode contractContent = injectorContract.getConvertedContent(); + List contractMandatoryFields = StreamSupport.stream(contractContent.get("fields").spliterator(), false).filter(contractElement -> contractElement.get("mandatory").asBoolean()).toList(); + if (!contractMandatoryFields.isEmpty()) { + contractMandatoryFields.forEach(jsonField -> { + String key = jsonField.get("key").asText(); + if( content.get(key) == null ) { + ready.set(false); + } + if(Objects.equals(jsonField.get("type").asText(), "String") && (content.get(key).asText().isEmpty())) { + ready.set(false); + } + }); + } + return ready.get(); + } + @JsonIgnore public Instant computeInjectDate(Instant source, int speed) { // Compute origin execution date diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java b/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java index db3480258a..58f2f08f25 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectorContract.java @@ -3,10 +3,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.hypersistence.utils.hibernate.type.array.StringArrayType; import io.hypersistence.utils.hibernate.type.basic.PostgreSQLHStoreType; import io.openbas.annotation.Queryable; import io.openbas.database.audit.ModelBaseListener; +import io.openbas.database.converter.ContentConverter; import io.openbas.helper.MonoIdDeserializer; import io.openbas.helper.MultiIdListDeserializer; import jakarta.persistence.*; @@ -48,6 +50,10 @@ public class InjectorContract implements Base { @NotBlank private String content; + @Column(name = "injector_contract_content", insertable=false, updatable=false) + @Convert(converter = ContentConverter.class) + private ObjectNode convertedContent; + @Column(name = "injector_contract_custom") @JsonProperty("injector_contract_custom") private Boolean custom = false;