Repository: OpenAEV-Platform/openaev (java)

Issue:
Partial delete + error when deleting a file associated to a payload
## Description

Partial delete + error when deleting a file associated to a payload.

1. Create a payload of type "file drop"
2. Upload a new document as part of this payload
3. Try to delete the document

=> An error occured.

But, actually, the document was deleted in Minio (S3) but not the database entry. Which lead to an very inconsistant and not usable behavior.

What we should do:

1. A document associated to at least 1 payload cannot be deleted
2. When it is not associated to any payload it can be deleted
3. We should have a popover + dialog to display "where the file is used"

Like this:

![Image](https://github.com/user-attachments/assets/e266d1d9-538f-45e5-81e7-167fa5c64024)

When clicking on number:

![Image](https://github.com/user-attachments/assets/03a8e1aa-254a-4614-975d-bf36d80834ee)


Discussion:
We can correct the database link entry to fix the bug, but we’ll need to create a new improvement issue to address the remaining concerns.
To check:
<img width="960" height="813" alt="Image" src="https://github.com/user-attachments/assets/e24f0789-cdda-4fad-9aa4-40c65c091657" />

Patch:
diff --git a/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorService.java b/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorService.java
--- a/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorService.java
+++ b/openbas-api/src/main/java/io/openbas/executors/caldera/service/CalderaExecutorService.java
@@ -16,7 +16,7 @@
 import io.openbas.service.AgentService;
 import io.openbas.service.EndpointService;
 import io.openbas.service.PlatformSettingsService;
-import io.openbas.utils.EndpointMapper;
+import io.openbas.utils.mapper.EndpointMapper;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import java.util.*;
diff --git a/openbas-api/src/main/java/io/openbas/executors/model/AgentRegisterInput.java b/openbas-api/src/main/java/io/openbas/executors/model/AgentRegisterInput.java
--- a/openbas-api/src/main/java/io/openbas/executors/model/AgentRegisterInput.java
+++ b/openbas-api/src/main/java/io/openbas/executors/model/AgentRegisterInput.java
@@ -3,7 +3,7 @@
 import io.openbas.database.model.Endpoint;
 import io.openbas.database.model.Executor;
 import io.openbas.helper.AgentHelper;
-import io.openbas.utils.EndpointMapper;
+import io.openbas.utils.mapper.EndpointMapper;
 import java.time.Instant;
 import lombok.Data;
 
diff --git a/openbas-api/src/main/java/io/openbas/migration/V4_11__Update_contrainst_documents.java b/openbas-api/src/main/java/io/openbas/migration/V4_11__Update_contrainst_documents.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/migration/V4_11__Update_contrainst_documents.java
@@ -0,0 +1,34 @@
+package io.openbas.migration;
+
+import java.sql.Statement;
+import org.flywaydb.core.api.migration.BaseJavaMigration;
+import org.flywaydb.core.api.migration.Context;
+import org.springframework.stereotype.Component;
+
+@Component
+public class V4_11__Update_contrainst_documents extends BaseJavaMigration {
+
+  @Override
+  public void migrate(Context context) throws Exception {
+    try (Statement stmt = context.getConnection().createStatement()) {
+
+      // Drop and recreate foreign key in scenarios_documents
+      stmt.execute(
+          """
+                ALTER TABLE scenarios_documents
+                DROP CONSTRAINT document_id_fk,
+                ADD CONSTRAINT document_id_fk
+                FOREIGN KEY (document_id) REFERENCES documents(document_id) ON DELETE CASCADE
+            """);
+
+      // Drop and recreate foreign key in challenges_documents
+      stmt.execute(
+          """
+                ALTER TABLE challenges_documents
+                DROP CONSTRAINT document_id_fk,
+                ADD CONSTRAINT document_id_fk
+                FOREIGN KEY (document_id) REFERENCES documents(document_id) ON DELETE CASCADE
+            """);
+    }
+  }
+}
diff --git a/openbas-api/src/main/java/io/openbas/rest/asset/endpoint/EndpointApi.java b/openbas-api/src/main/java/io/openbas/rest/asset/endpoint/EndpointApi.java
--- a/openbas-api/src/main/java/io/openbas/rest/asset/endpoint/EndpointApi.java
+++ b/openbas-api/src/main/java/io/openbas/rest/asset/endpoint/EndpointApi.java
@@ -15,9 +15,9 @@
 import io.openbas.rest.asset.endpoint.form.*;
 import io.openbas.rest.helper.RestBehavior;
 import io.openbas.service.EndpointService;
-import io.openbas.utils.EndpointMapper;
 import io.openbas.utils.FilterUtilsJpa;
 import io.openbas.utils.HttpReqRespUtils;
+import io.openbas.utils.mapper.EndpointMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotBlank;
diff --git a/openbas-api/src/main/java/io/openbas/rest/asset_group/AssetGroupApi.java b/openbas-api/src/main/java/io/openbas/rest/asset_group/AssetGroupApi.java
--- a/openbas-api/src/main/java/io/openbas/rest/asset_group/AssetGroupApi.java
+++ b/openbas-api/src/main/java/io/openbas/rest/asset_group/AssetGroupApi.java
@@ -18,8 +18,8 @@
 import io.openbas.rest.helper.RestBehavior;
 import io.openbas.service.AssetGroupService;
 import io.openbas.service.EndpointService;
-import io.openbas.utils.EndpointMapper;
 import io.openbas.utils.FilterUtilsJpa;
+import io.openbas.utils.mapper.EndpointMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotBlank;
diff --git a/openbas-api/src/main/java/io/openbas/rest/custom_dashboard/CustomDashboardService.java b/openbas-api/src/main/java/io/openbas/rest/custom_dashboard/CustomDashboardService.java
--- a/openbas-api/src/main/java/io/openbas/rest/custom_dashboard/CustomDashboardService.java
+++ b/openbas-api/src/main/java/io/openbas/rest/custom_dashboard/CustomDashboardService.java
@@ -8,8 +8,8 @@
 import io.openbas.database.raw.RawCustomDashboard;
 import io.openbas.database.repository.CustomDashboardRepository;
 import io.openbas.rest.custom_dashboard.form.CustomDashboardOutput;
-import io.openbas.utils.CustomDashboardMapper;
 import io.openbas.utils.FilterUtilsJpa;
+import io.openbas.utils.mapper.CustomDashboardMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.persistence.EntityNotFoundException;
 import java.time.Instant;
diff --git a/openbas-api/src/main/java/io/openbas/rest/cve/CveApi.java b/openbas-api/src/main/java/io/openbas/rest/cve/CveApi.java
--- a/openbas-api/src/main/java/io/openbas/rest/cve/CveApi.java
+++ b/openbas-api/src/main/java/io/openbas/rest/cve/CveApi.java
@@ -10,7 +10,7 @@
 import io.openbas.rest.cve.form.CveUpdateInput;
 import io.openbas.rest.cve.service.CveService;
 import io.openbas.rest.helper.RestBehavior;
-import io.openbas.utils.CveMapper;
+import io.openbas.utils.mapper.CveMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
diff --git a/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java b/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java
--- a/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java
+++ b/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java
@@ -5,14 +5,17 @@
 import static io.openbas.database.specification.DocumentSpecification.findGrantedFor;
 import static io.openbas.helper.StreamHelper.fromIterable;
 import static io.openbas.helper.StreamHelper.iterableToSet;
+import static io.openbas.utils.mapper.DocumentMapper.toDocumentRelationsOutput;
 import static io.openbas.utils.pagination.PaginationUtils.buildPaginationJPA;
 
+import io.openbas.aop.LogExecutionTime;
 import io.openbas.config.OpenBASPrincipal;
 import io.openbas.database.model.*;
 import io.openbas.database.raw.RawDocument;
 import io.openbas.database.raw.RawPaginationDocument;
 import io.openbas.database.repository.*;
 import io.openbas.rest.document.form.DocumentCreateInput;
+import io.openbas.rest.document.form.DocumentRelationsOutput;
 import io.openbas.rest.document.form.DocumentTagUpdateInput;
 import io.openbas.rest.document.form.DocumentUpdateInput;
 import io.openbas.rest.exception.ElementNotFoundException;
@@ -20,12 +23,16 @@
 import io.openbas.rest.inject.service.InjectService;
 import io.openbas.service.FileService;
 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 java.io.IOException;
 import java.io.InputStream;
-import java.util.*;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 import lombok.RequiredArgsConstructor;
@@ -46,11 +53,11 @@
 @RequiredArgsConstructor
 public class DocumentApi extends RestBehavior {
 
+  public static final String DOCUMENT_API = "/api/documents";
   private final TagRepository tagRepository;
   private final DocumentRepository documentRepository;
   private final ExerciseRepository exerciseRepository;
   private final ScenarioRepository scenarioRepository;
-  private final InjectDocumentRepository injectDocumentRepository;
   private final UserRepository userRepository;
   private final InjectorRepository injectorRepository;
   private final CollectorRepository collectorRepository;
@@ -69,7 +76,7 @@ private Optional<Document> resolveDocument(String documentId) {
     }
   }
 
-  @PostMapping("/api/documents")
+  @PostMapping(DOCUMENT_API)
   @Transactional(rollbackOn = Exception.class)
   public Document uploadDocument(
       @Valid @RequestPart("input") DocumentCreateInput input,
@@ -122,7 +129,7 @@ public Document uploadDocument(
     }
   }
 
-  @PostMapping("/api/documents/upsert")
+  @PostMapping(DOCUMENT_API + "/upsert")
   @Transactional(rollbackOn = Exception.class)
   public Document upsertDocument(
       @Valid @RequestPart("input") DocumentCreateInput input,
@@ -218,7 +225,7 @@ public List<RawDocument> documents() {
     }
   }
 
-  @PostMapping("/api/documents/search")
+  @PostMapping(DOCUMENT_API + "/search")
   public Page<RawPaginationDocument> searchDocuments(
       @RequestBody @Valid final SearchPaginationInput searchPaginationInput) {
     OpenBASPrincipal user = currentUser();
@@ -253,13 +260,13 @@ public Page<RawPaginationDocument> searchDocuments(
     }
   }
 
-  @GetMapping("/api/documents/{documentId}")
+  @GetMapping(DOCUMENT_API + "/{documentId}")
   public Document document(@PathVariable String documentId) {
     return resolveDocument(documentId)
         .orElseThrow(() -> new ElementNotFoundException("Document not found"));
   }
 
-  @GetMapping("/api/documents/{documentId}/tags")
+  @GetMapping(DOCUMENT_API + "/{documentId}/tags")
   public Set<Tag> documentTags(@PathVariable String documentId) {
     Document document =
         resolveDocument(documentId)
@@ -267,7 +274,7 @@ public Set<Tag> documentTags(@PathVariable String documentId) {
     return document.getTags();
   }
 
-  @PutMapping("/api/documents/{documentId}/tags")
+  @PutMapping(DOCUMENT_API + "/{documentId}/tags")
   public Document documentTags(
       @PathVariable String documentId, @RequestBody DocumentTagUpdateInput input) {
     Document document =
@@ -278,7 +285,7 @@ public Document documentTags(
   }
 
   @Transactional(rollbackOn = Exception.class)
-  @PutMapping("/api/documents/{documentId}")
+  @PutMapping(DOCUMENT_API + "/{documentId}")
   public Document updateDocumentInformation(
       @PathVariable String documentId, @Valid @RequestBody DocumentUpdateInput input) {
     Document document =
@@ -335,7 +342,7 @@ public Document updateDocumentInformation(
     return documentRepository.save(document);
   }
 
-  @GetMapping("/api/documents/{documentId}/file")
+  @GetMapping(DOCUMENT_API + "/{documentId}/file")
   public void downloadDocument(@PathVariable String documentId, HttpServletResponse response)
       throws IOException {
     Document document =
@@ -484,19 +491,17 @@ private List<Document> getScenarioPlayerDocuments(Scenario scenario) {
     return documentService.getPlayerDocuments(articles, injects);
   }
 
+  @LogExecutionTime
+  @Operation(summary = "Fetch the entities related to this document id")
+  @GetMapping(DOCUMENT_API + "/{documentId}/relations")
+  public DocumentRelationsOutput getDocumentRelations(@PathVariable String documentId) {
+    return toDocumentRelationsOutput(documentService.document(documentId));
+  }
+
   @Transactional(rollbackOn = Exception.class)
-  @DeleteMapping("/api/documents/{documentId}")
+  @DeleteMapping(DOCUMENT_API + "/{documentId}")
   public void deleteDocument(@PathVariable String documentId) {
-    injectDocumentRepository.deleteDocumentFromAllReferences(documentId);
-    List<Document> documents = documentRepository.removeById(documentId);
-    documents.forEach(
-        document -> {
-          try {
-            fileService.deleteFile(document.getTarget());
-          } catch (Exception e) {
-            // Fail no longer available in the storage.
-          }
-        });
+    documentService.deleteDocument(documentId);
   }
 
   // -- EXERCISE & SENARIO--
diff --git a/openbas-api/src/main/java/io/openbas/rest/document/DocumentService.java b/openbas-api/src/main/java/io/openbas/rest/document/DocumentService.java
--- a/openbas-api/src/main/java/io/openbas/rest/document/DocumentService.java
+++ b/openbas-api/src/main/java/io/openbas/rest/document/DocumentService.java
@@ -12,21 +12,25 @@
 import io.openbas.database.repository.DocumentRepository;
 import io.openbas.injectors.challenge.model.ChallengeContent;
 import io.openbas.rest.exception.ElementNotFoundException;
+import io.openbas.service.FileService;
 import jakarta.annotation.Resource;
 import jakarta.validation.constraints.NotBlank;
 import java.util.List;
 import java.util.stream.Stream;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 @RequiredArgsConstructor
 @Service
+@Slf4j
 public class DocumentService {
 
+  @Resource private ObjectMapper mapper;
+
   private final DocumentRepository documentRepository;
   private final ChallengeRepository challengeRepository;
-
-  @Resource private ObjectMapper mapper;
+  private final FileService fileService;
 
   // -- CRUD --
 
@@ -69,4 +73,18 @@ public List<Document> getPlayerDocuments(List<Article> articles, List<Inject> in
         .distinct()
         .toList();
   }
+
+  public void deleteDocument(String documentId) {
+    List<Document> documents = documentRepository.removeById(documentId);
+
+    // Remove document from minio
+    documents.forEach(
+        document -> {
+          try {
+            fileService.deleteFile(document.getTarget());
+          } catch (Exception e) {
+            log.warn("File already removed or not found in minio: {}", document.getTarget(), e);
+          }
+        });
+  }
 }
diff --git a/openbas-api/src/main/java/io/openbas/rest/document/form/DocumentRelationsOutput.java b/openbas-api/src/main/java/io/openbas/rest/document/form/DocumentRelationsOutput.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/rest/document/form/DocumentRelationsOutput.java
@@ -0,0 +1,34 @@
+package io.openbas.rest.document.form;
+
+import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import java.util.Set;
+import lombok.Builder;
+import lombok.Data;
+
+@Builder
+@Data
+@JsonInclude(NON_EMPTY)
+public class DocumentRelationsOutput {
+
+  private Set<RelatedEntityOutput> simulations;
+
+  private Set<RelatedEntityOutput> securityPlatforms;
+
+  private Set<RelatedEntityOutput> channels;
+
+  private Set<RelatedEntityOutput> payloads;
+
+  private Set<RelatedEntityOutput> scenarioArticles;
+
+  private Set<RelatedEntityOutput> simulationArticles;
+
+  private Set<RelatedEntityOutput> atomicTestings;
+
+  private Set<RelatedEntityOutput> scenarioInjects;
+
+  private Set<RelatedEntityOutput> simulationInjects;
+
+  private Set<RelatedEntityOutput> challenges;
+}
diff --git a/openbas-api/src/main/java/io/openbas/rest/document/form/RelatedEntityOutput.java b/openbas-api/src/main/java/io/openbas/rest/document/form/RelatedEntityOutput.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/rest/document/form/RelatedEntityOutput.java
@@ -0,0 +1,10 @@
+package io.openbas.rest.document.form;
+
+import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Builder;
+
+@Builder
+@JsonInclude(NON_NULL)
+public record RelatedEntityOutput(String id, String name, String context) {}
diff --git a/openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java b/openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java
--- a/openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java
+++ b/openbas-api/src/main/java/io/openbas/rest/exercise/service/ExerciseService.java
@@ -37,6 +37,8 @@
 import io.openbas.telemetry.metric_collectors.ActionMetricCollector;
 import io.openbas.utils.*;
 import io.openbas.utils.AtomicTestingUtils.ExpectationResultsByType;
+import io.openbas.utils.mapper.ExerciseMapper;
+import io.openbas.utils.mapper.InjectMapper;
 import jakarta.annotation.Resource;
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.PersistenceContext;
diff --git a/openbas-api/src/main/java/io/openbas/rest/finding/FindingDistinctSearchService.java b/openbas-api/src/main/java/io/openbas/rest/finding/FindingDistinctSearchService.java
--- a/openbas-api/src/main/java/io/openbas/rest/finding/FindingDistinctSearchService.java
+++ b/openbas-api/src/main/java/io/openbas/rest/finding/FindingDistinctSearchService.java
@@ -9,7 +9,7 @@
 import io.openbas.database.repository.FindingRepository;
 import io.openbas.database.specification.FindingSpecification;
 import io.openbas.rest.finding.form.AggregatedFindingOutput;
-import io.openbas.utils.FindingMapper;
+import io.openbas.utils.mapper.FindingMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import java.util.List;
 import java.util.Map;
diff --git a/openbas-api/src/main/java/io/openbas/rest/finding/FindingSearchApi.java b/openbas-api/src/main/java/io/openbas/rest/finding/FindingSearchApi.java
--- a/openbas-api/src/main/java/io/openbas/rest/finding/FindingSearchApi.java
+++ b/openbas-api/src/main/java/io/openbas/rest/finding/FindingSearchApi.java
@@ -10,7 +10,7 @@
 import io.openbas.rest.finding.form.PageAggregatedFindingOutput;
 import io.openbas.rest.finding.form.PageRelatedFindingOutput;
 import io.openbas.rest.helper.RestBehavior;
-import io.openbas.utils.FindingMapper;
+import io.openbas.utils.mapper.FindingMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import io.swagger.v3.oas.annotations.media.Content;
 import io.swagger.v3.oas.annotations.media.Schema;
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
--- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java
+++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java
@@ -23,8 +23,8 @@
 import io.openbas.service.ImportService;
 import io.openbas.service.targets.TargetService;
 import io.openbas.utils.FilterUtilsJpa;
-import io.openbas.utils.PayloadMapper;
 import io.openbas.utils.TargetType;
+import io.openbas.utils.mapper.PayloadMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java b/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java
--- a/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java
+++ b/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java
@@ -37,6 +37,8 @@
 import io.openbas.rest.tag.TagService;
 import io.openbas.service.*;
 import io.openbas.utils.*;
+import io.openbas.utils.mapper.InjectMapper;
+import io.openbas.utils.mapper.InjectStatusMapper;
 import jakarta.annotation.Nullable;
 import jakarta.annotation.Resource;
 import jakarta.transaction.Transactional;
diff --git a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java
--- a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java
+++ b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java
@@ -17,8 +17,8 @@
 import io.openbas.rest.exception.ElementNotFoundException;
 import io.openbas.rest.inject.service.InjectService;
 import io.openbas.telemetry.metric_collectors.ActionMetricCollector;
-import io.openbas.utils.InjectMapper;
-import io.openbas.utils.PayloadMapper;
+import io.openbas.utils.mapper.InjectMapper;
+import io.openbas.utils.mapper.PayloadMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.annotation.Resource;
 import jakarta.persistence.criteria.Join;
diff --git a/openbas-api/src/main/java/io/openbas/service/EndpointService.java b/openbas-api/src/main/java/io/openbas/service/EndpointService.java
--- a/openbas-api/src/main/java/io/openbas/service/EndpointService.java
+++ b/openbas-api/src/main/java/io/openbas/service/EndpointService.java
@@ -19,8 +19,8 @@
 import io.openbas.rest.asset.endpoint.form.EndpointInput;
 import io.openbas.rest.asset.endpoint.form.EndpointRegisterInput;
 import io.openbas.rest.exception.ElementNotFoundException;
-import io.openbas.utils.EndpointMapper;
 import io.openbas.utils.FilterUtilsJpa;
+import io.openbas.utils.mapper.EndpointMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.annotation.Resource;
 import jakarta.validation.constraints.NotBlank;
diff --git a/openbas-api/src/main/java/io/openbas/service/InjectSearchService.java b/openbas-api/src/main/java/io/openbas/service/InjectSearchService.java
--- a/openbas-api/src/main/java/io/openbas/service/InjectSearchService.java
+++ b/openbas-api/src/main/java/io/openbas/service/InjectSearchService.java
@@ -18,8 +18,8 @@
 import io.openbas.rest.inject.output.InjectOutput;
 import io.openbas.rest.payload.output.PayloadSimple;
 import io.openbas.utils.AtomicTestingUtils;
-import io.openbas.utils.InjectMapper;
 import io.openbas.utils.TargetType;
+import io.openbas.utils.mapper.InjectMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.PersistenceContext;
diff --git a/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java b/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java
--- a/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java
+++ b/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java
@@ -14,7 +14,7 @@
 import io.openbas.executors.Injector;
 import io.openbas.rest.exception.BadRequestException;
 import io.openbas.rest.inject.output.InjectTestStatusOutput;
-import io.openbas.utils.InjectStatusMapper;
+import io.openbas.utils.mapper.InjectStatusMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.persistence.EntityNotFoundException;
 import java.util.ArrayList;
diff --git a/openbas-api/src/main/java/io/openbas/service/MapperService.java b/openbas-api/src/main/java/io/openbas/service/MapperService.java
--- a/openbas-api/src/main/java/io/openbas/service/MapperService.java
+++ b/openbas-api/src/main/java/io/openbas/service/MapperService.java
@@ -38,8 +38,8 @@
 import io.openbas.service.utils.CustomColumnPositionStrategy;
 import io.openbas.utils.Constants;
 import io.openbas.utils.CopyObjectListUtils;
-import io.openbas.utils.EndpointMapper;
 import io.openbas.utils.TargetType;
+import io.openbas.utils.mapper.EndpointMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.constraints.NotBlank;
diff --git a/openbas-api/src/main/java/io/openbas/service/ScenarioService.java b/openbas-api/src/main/java/io/openbas/service/ScenarioService.java
--- a/openbas-api/src/main/java/io/openbas/service/ScenarioService.java
+++ b/openbas-api/src/main/java/io/openbas/service/ScenarioService.java
@@ -41,7 +41,7 @@
 import io.openbas.rest.scenario.form.ScenarioSimple;
 import io.openbas.rest.team.output.TeamOutput;
 import io.openbas.telemetry.metric_collectors.ActionMetricCollector;
-import io.openbas.utils.ExerciseMapper;
+import io.openbas.utils.mapper.ExerciseMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import jakarta.annotation.Resource;
 import jakarta.persistence.EntityManager;
diff --git a/openbas-api/src/main/java/io/openbas/utils/ScenarioMapper.java b/openbas-api/src/main/java/io/openbas/utils/ScenarioMapper.java
deleted file mode 100644
--- a/openbas-api/src/main/java/io/openbas/utils/ScenarioMapper.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package io.openbas.utils;
-
-import io.openbas.database.model.Scenario;
-import io.openbas.rest.scenario.form.ScenarioSimple;
-import jakarta.validation.constraints.NotNull;
-import lombok.RequiredArgsConstructor;
-import org.springframework.beans.BeanUtils;
-import org.springframework.stereotype.Component;
-
-@RequiredArgsConstructor
-@Component
-public class ScenarioMapper {
-
-  public ScenarioSimple toScenarioSimple(@NotNull final Scenario scenario) {
-    ScenarioSimple simple = new ScenarioSimple();
-    BeanUtils.copyProperties(scenario, simple);
-    return simple;
-  }
-}
diff --git a/openbas-api/src/main/java/io/openbas/utils/AgentMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/AgentMapper.java
similarity index 97%
rename from openbas-api/src/main/java/io/openbas/utils/AgentMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/AgentMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/AgentMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/AgentMapper.java
@@ -1,4 +1,4 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import static java.util.Collections.emptyList;
 
diff --git a/openbas-api/src/main/java/io/openbas/utils/AssetGroupMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/AssetGroupMapper.java
similarity index 92%
rename from openbas-api/src/main/java/io/openbas/utils/AssetGroupMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/AssetGroupMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/AssetGroupMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/AssetGroupMapper.java
@@ -1,4 +1,4 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import io.openbas.database.model.AssetGroup;
 import io.openbas.rest.asset_group.form.AssetGroupSimple;
diff --git a/openbas-api/src/main/java/io/openbas/utils/mapper/ChallengeMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/ChallengeMapper.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/ChallengeMapper.java
@@ -0,0 +1,19 @@
+package io.openbas.utils.mapper;
+
+import io.openbas.database.model.Challenge;
+import io.openbas.rest.document.form.RelatedEntityOutput;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ChallengeMapper {
+
+  public static Set<RelatedEntityOutput> toRelatedEntityOutputs(Set<Challenge> challenges) {
+    return challenges.stream()
+        .map(challenge -> toRelatedEntityOutput(challenge))
+        .collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toRelatedEntityOutput(Challenge challenge) {
+    return RelatedEntityOutput.builder().id(challenge.getId()).name(challenge.getName()).build();
+  }
+}
diff --git a/openbas-api/src/main/java/io/openbas/utils/mapper/ChannelMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/ChannelMapper.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/ChannelMapper.java
@@ -0,0 +1,19 @@
+package io.openbas.utils.mapper;
+
+import io.openbas.database.model.Channel;
+import io.openbas.rest.document.form.RelatedEntityOutput;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ChannelMapper {
+
+  public static Set<RelatedEntityOutput> toRelatedEntityOutputs(Set<Channel> channels) {
+    return channels.stream()
+        .map(channel -> toRelatedEntityOutput(channel))
+        .collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toRelatedEntityOutput(Channel channel) {
+    return RelatedEntityOutput.builder().id(channel.getId()).name(channel.getName()).build();
+  }
+}
diff --git a/openbas-api/src/main/java/io/openbas/utils/CustomDashboardMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/CustomDashboardMapper.java
similarity index 97%
rename from openbas-api/src/main/java/io/openbas/utils/CustomDashboardMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/CustomDashboardMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/CustomDashboardMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/CustomDashboardMapper.java
@@ -1,4 +1,4 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import io.openbas.database.raw.RawCustomDashboard;
 import io.openbas.rest.custom_dashboard.form.CustomDashboardOutput;
diff --git a/openbas-api/src/main/java/io/openbas/utils/CveMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/CveMapper.java
similarity index 98%
rename from openbas-api/src/main/java/io/openbas/utils/CveMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/CveMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/CveMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/CveMapper.java
@@ -1,4 +1,4 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import io.openbas.config.cache.LicenseCacheManager;
 import io.openbas.database.model.Cve;
diff --git a/openbas-api/src/main/java/io/openbas/utils/mapper/DocumentMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/DocumentMapper.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/DocumentMapper.java
@@ -0,0 +1,91 @@
+package io.openbas.utils.mapper;
+
+import static io.openbas.utils.mapper.ChallengeMapper.toRelatedEntityOutputs;
+import static io.openbas.utils.mapper.ChannelMapper.toRelatedEntityOutputs;
+import static io.openbas.utils.mapper.ExerciseMapper.toRelatedEntityOutputs;
+import static io.openbas.utils.mapper.ExerciseMapper.toSimulationArticles;
+import static io.openbas.utils.mapper.ExerciseMapper.toSimulationInjects;
+import static io.openbas.utils.mapper.InjectMapper.toRelatedEntityOutputs;
+import static io.openbas.utils.mapper.PayloadMapper.toRelatedEntityOutputs;
+import static io.openbas.utils.mapper.ScenarioMapper.toScenarioArticles;
+import static io.openbas.utils.mapper.ScenarioMapper.toScenarioInjects;
+import static io.openbas.utils.mapper.SecurityPlatformMapper.toRelatedEntityOutputs;
+
+import io.openbas.database.model.*;
+import io.openbas.rest.document.form.DocumentRelationsOutput;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@RequiredArgsConstructor
+@Slf4j
+public class DocumentMapper {
+
+  public static DocumentRelationsOutput toDocumentRelationsOutput(Document document) {
+    Set<Article> articles = document.getArticles();
+    Set<Inject> injects =
+        document.getInjectDocuments().stream()
+            .map(InjectDocument::getInject)
+            .collect(Collectors.toSet());
+
+    Set<Article> scenarioArticles =
+        articles.stream()
+            .filter(article -> article.getScenario() != null)
+            .collect(Collectors.toSet());
+
+    Set<Article> simulationArticles =
+        articles.stream()
+            .filter(article -> article.getExercise() != null)
+            .collect(Collectors.toSet());
+
+    Set<Inject> atomics =
+        injects.stream()
+            .filter(inject -> inject.getScenario() == null && inject.getExercise() == null)
+            .collect(Collectors.toSet());
+
+    Set<Inject> scenarioInjects =
+        injects.stream().filter(inject -> inject.getScenario() != null).collect(Collectors.toSet());
+
+    Set<Inject> simulationInjects =
+        injects.stream().filter(inject -> inject.getExercise() != null).collect(Collectors.toSet());
+
+    Set<Exercise> simulations =
+        Stream.concat(
+                document.getSimulationsByLogoDark().stream(),
+                document.getSimulationsByLogoLight().stream())
+            .collect(Collectors.toSet());
+
+    Set<SecurityPlatform> securityPlatforms =
+        Stream.concat(
+                document.getSecurityPlatformsByLogoDark().stream(),
+                document.getSecurityPlatformsByLogoLight().stream())
+            .collect(Collectors.toSet());
+
+    Set<Channel> channels =
+        Stream.concat(
+                document.getChannelsByLogoDark().stream(),
+                document.getChannelsByLogoLight().stream())
+            .collect(Collectors.toSet());
+
+    Set<Payload> payloads =
+        Stream.concat(
+                document.getPayloadsByFileDrop().stream(),
+                document.getPayloadsByExecutableFile().stream())
+            .collect(Collectors.toSet());
+
+    return DocumentRelationsOutput.builder()
+        .simulations(toRelatedEntityOutputs(simulations))
+        .securityPlatforms(toRelatedEntityOutputs(securityPlatforms))
+        .channels(toRelatedEntityOutputs(channels))
+        .payloads(toRelatedEntityOutputs(payloads))
+        .scenarioArticles(toScenarioArticles(scenarioArticles))
+        .simulationArticles(toSimulationArticles(simulationArticles))
+        .atomicTestings(toRelatedEntityOutputs(atomics))
+        .scenarioInjects(toScenarioInjects(scenarioInjects))
+        .simulationInjects(toSimulationInjects(simulationInjects))
+        .challenges(toRelatedEntityOutputs(document.getChallenges()))
+        .build();
+  }
+}
diff --git a/openbas-api/src/main/java/io/openbas/utils/EndpointMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/EndpointMapper.java
similarity index 99%
rename from openbas-api/src/main/java/io/openbas/utils/EndpointMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/EndpointMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/EndpointMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/EndpointMapper.java
@@ -1,4 +1,4 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import static io.openbas.database.model.Endpoint.*;
 import static io.openbas.utils.AgentUtils.getPrimaryAgents;
diff --git a/openbas-api/src/main/java/io/openbas/utils/ExerciseMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/ExerciseMapper.java
similarity index 79%
rename from openbas-api/src/main/java/io/openbas/utils/ExerciseMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/ExerciseMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/ExerciseMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/ExerciseMapper.java
@@ -1,9 +1,11 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import static java.util.Collections.emptyList;
 
+import io.openbas.database.model.Article;
 import io.openbas.database.model.Exercise;
 import io.openbas.database.model.ExerciseStatus;
+import io.openbas.database.model.Inject;
 import io.openbas.database.raw.RawExerciseSimple;
 import io.openbas.database.raw.RawInjectExpectation;
 import io.openbas.database.repository.AssetGroupRepository;
@@ -11,7 +13,11 @@
 import io.openbas.database.repository.InjectExpectationRepository;
 import io.openbas.database.repository.TeamRepository;
 import io.openbas.rest.atomic_testing.form.TargetSimple;
+import io.openbas.rest.document.form.RelatedEntityOutput;
 import io.openbas.rest.exercise.form.ExerciseSimple;
+import io.openbas.utils.AtomicTestingUtils;
+import io.openbas.utils.ResultUtils;
+import io.openbas.utils.TargetType;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -159,4 +165,40 @@ public ExerciseSimple toExerciseSimple(Exercise exercise) {
 
     return simple;
   }
+
+  public static Set<RelatedEntityOutput> toRelatedEntityOutputs(Set<Exercise> exercises) {
+    return exercises.stream()
+        .map(exercise -> toRelatedEntityOutput(exercise))
+        .collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toRelatedEntityOutput(Exercise exercise) {
+    return RelatedEntityOutput.builder().id(exercise.getId()).name(exercise.getName()).build();
+  }
+
+  public static Set<RelatedEntityOutput> toSimulationArticles(Set<Article> articles) {
+    return articles.stream()
+        .map(article -> toSimulationArticle(article))
+        .collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toSimulationArticle(Article article) {
+    return RelatedEntityOutput.builder()
+        .id(article.getId())
+        .name(article.getName())
+        .context(article.getExercise().getId())
+        .build();
+  }
+
+  public static Set<RelatedEntityOutput> toSimulationInjects(Set<Inject> injects) {
+    return injects.stream().map(inject -> toSimulationInject(inject)).collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toSimulationInject(Inject inject) {
+    return RelatedEntityOutput.builder()
+        .id(inject.getId())
+        .name(inject.getTitle())
+        .context(inject.getExercise().getId())
+        .build();
+  }
 }
diff --git a/openbas-api/src/main/java/io/openbas/utils/FindingMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/FindingMapper.java
similarity index 98%
rename from openbas-api/src/main/java/io/openbas/utils/FindingMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/FindingMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/FindingMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/FindingMapper.java
@@ -1,4 +1,4 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import io.openbas.database.model.*;
 import io.openbas.database.repository.FindingRepository;
diff --git a/openbas-api/src/main/java/io/openbas/utils/InjectMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/InjectMapper.java
similarity index 86%
rename from openbas-api/src/main/java/io/openbas/utils/InjectMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/InjectMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/InjectMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/InjectMapper.java
@@ -1,10 +1,18 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import io.openbas.database.model.*;
 import io.openbas.rest.atomic_testing.form.*;
+import io.openbas.rest.document.form.RelatedEntityOutput;
 import io.openbas.rest.inject.output.InjectSimple;
 import io.openbas.rest.payload.output.PayloadSimple;
-import java.util.*;
+import io.openbas.utils.AtomicTestingUtils;
+import io.openbas.utils.InjectUtils;
+import io.openbas.utils.ResultUtils;
+import io.openbas.utils.TargetType;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
@@ -124,4 +132,14 @@ private KillChainPhaseSimple toKillChainPhasesSimple(KillChainPhase killChainPha
   public InjectSimple toInjectSimple(Inject inject) {
     return InjectSimple.builder().id(inject.getId()).title(inject.getTitle()).build();
   }
+
+  public static Set<RelatedEntityOutput> toRelatedEntityOutputs(Set<Inject> injects) {
+    return injects.stream()
+        .map(inject -> toRelatedEntityOutput(inject))
+        .collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toRelatedEntityOutput(Inject inject) {
+    return RelatedEntityOutput.builder().id(inject.getId()).name(inject.getTitle()).build();
+  }
 }
diff --git a/openbas-api/src/main/java/io/openbas/utils/InjectStatusMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/InjectStatusMapper.java
similarity index 98%
rename from openbas-api/src/main/java/io/openbas/utils/InjectStatusMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/InjectStatusMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/InjectStatusMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/InjectStatusMapper.java
@@ -1,4 +1,4 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import io.openbas.database.model.*;
 import io.openbas.rest.atomic_testing.form.*;
diff --git a/openbas-api/src/main/java/io/openbas/utils/PayloadMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/PayloadMapper.java
similarity index 95%
rename from openbas-api/src/main/java/io/openbas/utils/PayloadMapper.java
rename to openbas-api/src/main/java/io/openbas/utils/mapper/PayloadMapper.java
--- a/openbas-api/src/main/java/io/openbas/utils/PayloadMapper.java
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/PayloadMapper.java
@@ -1,4 +1,4 @@
-package io.openbas.utils;
+package io.openbas.utils.mapper;
 
 import static io.openbas.database.model.Command.COMMAND_TYPE;
 import static io.openbas.database.model.DnsResolution.DNS_RESOLUTION_TYPE;
@@ -12,6 +12,7 @@
 import io.openbas.ee.Ee;
 import io.openbas.rest.atomic_testing.form.AttackPatternSimple;
 import io.openbas.rest.atomic_testing.form.StatusPayloadOutput;
+import io.openbas.rest.document.form.RelatedEntityOutput;
 import io.openbas.rest.payload.form.DetectionRemediationOutput;
 import io.openbas.rest.payload.output.output_parser.ContractOutputElementSimple;
 import io.openbas.rest.payload.output.output_parser.OutputParserSimple;
@@ -260,4 +261,14 @@ private DetectionRemediationOutput toDetectionRemediationOutput(
             .build();
     return output;
   }
+
+  public static Set<RelatedEntityOutput> toRelatedEntityOutputs(Set<Payload> payloads) {
+    return payloads.stream()
+        .map(payload -> toRelatedEntityOutput(payload))
+        .collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toRelatedEntityOutput(Payload payload) {
+    return RelatedEntityOutput.builder().id(payload.getId()).name(payload.getName()).build();
+  }
 }
diff --git a/openbas-api/src/main/java/io/openbas/utils/mapper/ScenarioMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/ScenarioMapper.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/ScenarioMapper.java
@@ -0,0 +1,48 @@
+package io.openbas.utils.mapper;
+
+import io.openbas.database.model.Article;
+import io.openbas.database.model.Inject;
+import io.openbas.database.model.Scenario;
+import io.openbas.rest.document.form.RelatedEntityOutput;
+import io.openbas.rest.scenario.form.ScenarioSimple;
+import jakarta.validation.constraints.NotNull;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Component;
+
+@RequiredArgsConstructor
+@Component
+public class ScenarioMapper {
+
+  public ScenarioSimple toScenarioSimple(@NotNull final Scenario scenario) {
+    ScenarioSimple simple = new ScenarioSimple();
+    BeanUtils.copyProperties(scenario, simple);
+    return simple;
+  }
+
+  public static Set<RelatedEntityOutput> toScenarioArticles(Set<Article> articles) {
+    return articles.stream().map(article -> toScenarioArticle(article)).collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toScenarioArticle(Article article) {
+    return RelatedEntityOutput.builder()
+        .id(article.getId())
+        .name(article.getName())
+        .context(article.getScenario().getId())
+        .build();
+  }
+
+  public static Set<RelatedEntityOutput> toScenarioInjects(Set<Inject> injects) {
+    return injects.stream().map(inject -> toScenarioInject(inject)).collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toScenarioInject(Inject inject) {
+    return RelatedEntityOutput.builder()
+        .id(inject.getId())
+        .name(inject.getTitle())
+        .context(inject.getScenario().getId())
+        .build();
+  }
+}
diff --git a/openbas-api/src/main/java/io/openbas/utils/mapper/SecurityPlatformMapper.java b/openbas-api/src/main/java/io/openbas/utils/mapper/SecurityPlatformMapper.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/main/java/io/openbas/utils/mapper/SecurityPlatformMapper.java
@@ -0,0 +1,23 @@
+package io.openbas.utils.mapper;
+
+import io.openbas.database.model.SecurityPlatform;
+import io.openbas.rest.document.form.RelatedEntityOutput;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class SecurityPlatformMapper {
+
+  public static Set<RelatedEntityOutput> toRelatedEntityOutputs(
+      Set<SecurityPlatform> securityPlatforms) {
+    return securityPlatforms.stream()
+        .map(securityPlatform -> toRelatedEntityOutput(securityPlatform))
+        .collect(Collectors.toSet());
+  }
+
+  private static RelatedEntityOutput toRelatedEntityOutput(SecurityPlatform securityPlatform) {
+    return RelatedEntityOutput.builder()
+        .id(securityPlatform.getId())
+        .name(securityPlatform.getName())
+        .build();
+  }
+}
diff --git a/openbas-model/src/main/java/io/openbas/database/model/Document.java b/openbas-model/src/main/java/io/openbas/database/model/Document.java
--- a/openbas-model/src/main/java/io/openbas/database/model/Document.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/Document.java
@@ -10,7 +10,9 @@
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.persistence.*;
 import jakarta.validation.constraints.NotBlank;
-import java.util.*;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
 import lombok.Getter;
 import lombok.Setter;
 import org.hibernate.annotations.UuidGenerator;
@@ -91,9 +93,57 @@ public class Document implements Base {
   @JsonProperty("document_scenarios")
   private Set<Scenario> scenarios = new HashSet<>();
 
+  @ManyToMany(fetch = FetchType.LAZY)
+  @JoinTable(
+      name = "articles_documents",
+      joinColumns = @JoinColumn(name = "document_id"),
+      inverseJoinColumns = @JoinColumn(name = "article_id"))
+  @JsonIgnore
+  private Set<Article> articles = new HashSet<>();
+
+  @ManyToMany(fetch = FetchType.LAZY)
+  @JoinTable(
+      name = "challenges_documents",
+      joinColumns = @JoinColumn(name = "document_id"),
+      inverseJoinColumns = @JoinColumn(name = "challenge_id"))
+  @JsonIgnore
+  private Set<Challenge> challenges = new HashSet<>();
+
   @OneToMany(mappedBy = "document", fetch = FetchType.LAZY)
   @JsonIgnore
-  private List<InjectDocument> injectDocuments = new ArrayList<>();
+  private Set<InjectDocument> injectDocuments = new HashSet<>();
+
+  @OneToMany(mappedBy = "fileDropFile", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+  @JsonIgnore
+  private Set<FileDrop> payloadsByFileDrop = new HashSet<>();
+
+  @OneToMany(mappedBy = "executableFile", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+  @JsonIgnore
+  private Set<Executable> payloadsByExecutableFile = new HashSet<>();
+
+  @OneToMany(mappedBy = "logoDark", fetch = FetchType.LAZY)
+  @JsonIgnore
+  private Set<Channel> channelsByLogoDark = new HashSet<>();
+
+  @OneToMany(mappedBy = "logoLight", fetch = FetchType.LAZY)
+  @JsonIgnore
+  private Set<Channel> channelsByLogoLight = new HashSet<>();
+
+  @OneToMany(mappedBy = "logoDark", fetch = FetchType.LAZY)
+  @JsonIgnore
+  private Set<SecurityPlatform> securityPlatformsByLogoDark = new HashSet<>();
+
+  @OneToMany(mappedBy = "logoLight", fetch = FetchType.LAZY)
+  @JsonIgnore
+  private Set<SecurityPlatform> securityPlatformsByLogoLight = new HashSet<>();
+
+  @OneToMany(mappedBy = "logoDark", fetch = FetchType.LAZY)
+  @JsonIgnore
+  private Set<Exercise> simulationsByLogoDark = new HashSet<>();
+
+  @OneToMany(mappedBy = "logoLight", fetch = FetchType.LAZY)
+  @JsonIgnore
+  private Set<Exercise> simulationsByLogoLight = new HashSet<>();
 
   @Override
   public boolean isUserHasAccess(User user) {
@@ -102,8 +152,12 @@ public boolean isUserHasAccess(User user) {
 
   @Override
   public boolean equals(Object o) {
-    if (this == o) return true;
-    if (o == null || !Base.class.isAssignableFrom(o.getClass())) return false;
+    if (this == o) {
+      return true;
+    }
+    if (o == null || !Base.class.isAssignableFrom(o.getClass())) {
+      return false;
+    }
     Base base = (Base) o;
     return id.equals(base.getId());
   }
diff --git a/openbas-model/src/main/java/io/openbas/database/repository/InjectDocumentRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/InjectDocumentRepository.java
--- a/openbas-model/src/main/java/io/openbas/database/repository/InjectDocumentRepository.java
+++ b/openbas-model/src/main/java/io/openbas/database/repository/InjectDocumentRepository.java
@@ -19,12 +19,6 @@ public interface InjectDocumentRepository
   @NotNull
   Optional<InjectDocument> findById(@NotNull InjectDocumentId id);
 
-  @Modifying
-  @Query(
-      value = "delete from injects_documents i where i.document_id = :documentId",
-      nativeQuery = true)
-  void deleteDocumentFromAllReferences(@Param("documentId") String docId);
-
   @Modifying
   @Query(
       value = "delete from injects_documents i where i.inject_id = :injectId",


Test patch:
diff --git a/openbas-api/src/test/java/io/openbas/executors/caldera/service/CalderaExecutorServiceTest.java b/openbas-api/src/test/java/io/openbas/executors/caldera/service/CalderaExecutorServiceTest.java
--- a/openbas-api/src/test/java/io/openbas/executors/caldera/service/CalderaExecutorServiceTest.java
+++ b/openbas-api/src/test/java/io/openbas/executors/caldera/service/CalderaExecutorServiceTest.java
@@ -17,7 +17,7 @@
 import io.openbas.service.AgentService;
 import io.openbas.service.EndpointService;
 import io.openbas.service.PlatformSettingsService;
-import io.openbas.utils.EndpointMapper;
+import io.openbas.utils.mapper.EndpointMapper;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
diff --git a/openbas-api/src/test/java/io/openbas/rest/DocumentApiTest.java b/openbas-api/src/test/java/io/openbas/rest/DocumentApiTest.java
new file mode 100644
--- /dev/null
+++ b/openbas-api/src/test/java/io/openbas/rest/DocumentApiTest.java
@@ -0,0 +1,119 @@
+package io.openbas.rest;
+
+import static io.openbas.rest.document.DocumentApi.DOCUMENT_API;
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.openbas.IntegrationTest;
+import io.openbas.database.model.Challenge;
+import io.openbas.database.model.Document;
+import io.openbas.database.repository.ChallengeRepository;
+import io.openbas.database.repository.DocumentRepository;
+import io.openbas.rest.document.form.DocumentRelationsOutput;
+import io.openbas.rest.document.form.RelatedEntityOutput;
+import io.openbas.utils.fixtures.ChallengeFixture;
+import io.openbas.utils.fixtures.DocumentFixture;
+import io.openbas.utils.fixtures.FileFixture;
+import io.openbas.utils.fixtures.composers.ChallengeComposer;
+import io.openbas.utils.fixtures.composers.DocumentComposer;
+import io.openbas.utils.fixtures.files.BinaryFile;
+import io.openbas.utils.mockUser.WithMockAdminUser;
+import jakarta.annotation.Resource;
+import java.util.Set;
+import org.junit.jupiter.api.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.transaction.annotation.Transactional;
+
+@TestInstance(PER_CLASS)
+@Transactional
+@DisplayName("Document API Integration Tests")
+class DocumentApiTest extends IntegrationTest {
+
+  @Resource protected ObjectMapper mapper;
+
+  @Autowired private MockMvc mvc;
+
+  @Autowired DocumentComposer documentComposer;
+  @Autowired ChallengeComposer challengeComposer;
+  @Autowired private DocumentRepository documentRepository;
+  @Autowired private ChallengeRepository challengeRepository;
+
+  @BeforeAll
+  void beforeAll() {
+    challengeComposer.reset();
+    documentComposer.reset();
+  }
+
+  @AfterAll
+  void afterAll() {
+    challengeComposer.reset();
+    documentComposer.reset();
+  }
+
+  @Nested
+  @DisplayName("Documents CRUD")
+  @WithMockAdminUser
+  class CRUD {
+
+    @Test
+    @DisplayName("Should delete a document")
+    void shouldDeleteDocument() throws Exception {
+      Document document = getDocumentWithChallenge();
+      Challenge challenge = document.getChallenges().stream().findFirst().get();
+
+      mvc.perform(delete(DOCUMENT_API + "/" + document.getId())).andExpect(status().isOk());
+
+      assertFalse(documentRepository.findById(document.getId()).isPresent());
+      assertTrue(challengeRepository.findById(challenge.getId()).isPresent());
+    }
+
+    @Test
+    @DisplayName("Should fetch related entities for a document")
+    void shouldFetchRelatedEntities() throws Exception {
+      Document document = getDocumentWithChallenge();
+      Challenge challenge = document.getChallenges().stream().findFirst().get();
+
+      String response =
+          mvc.perform(get(DOCUMENT_API + "/" + document.getId() + "/relations"))
+              .andExpect(status().isOk())
+              .andReturn()
+              .getResponse()
+              .getContentAsString();
+
+      assertNotNull(response);
+
+      DocumentRelationsOutput output =
+          DocumentRelationsOutput.builder()
+              .challenges(
+                  Set.of(new RelatedEntityOutput(challenge.getId(), challenge.getName(), null)))
+              .build();
+
+      String relationJson = mapper.writeValueAsString(output);
+
+      assertThatJson(response).when(IGNORING_ARRAY_ORDER).isEqualTo(relationJson);
+    }
+  }
+
+  private Document getDocumentWithChallenge() {
+
+    ChallengeComposer.Composer challenge =
+        challengeComposer.forChallenge(ChallengeFixture.createDefaultChallenge());
+
+    BinaryFile badCoffeeFileContent = FileFixture.getBadCoffeeFileContent();
+    return documentComposer
+        .forDocument(DocumentFixture.getDocument(badCoffeeFileContent))
+        .withInMemoryFile(badCoffeeFileContent)
+        .withChallenge(challenge)
+        .persist()
+        .get();
+  }
+}
diff --git a/openbas-api/src/test/java/io/openbas/rest/EndpointApiTest.java b/openbas-api/src/test/java/io/openbas/rest/EndpointApiTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/EndpointApiTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/EndpointApiTest.java
@@ -21,8 +21,8 @@
 import io.openbas.rest.asset.endpoint.form.EndpointRegisterInput;
 import io.openbas.rest.exercise.service.ExerciseService;
 import io.openbas.service.EndpointService;
-import io.openbas.utils.EndpointMapper;
 import io.openbas.utils.fixtures.ExerciseFixture;
+import io.openbas.utils.mapper.EndpointMapper;
 import io.openbas.utils.mockUser.WithMockAdminUser;
 import java.util.ArrayList;
 import java.util.List;
diff --git a/openbas-api/src/test/java/io/openbas/rest/exercise/service/ExerciseServiceTest.java b/openbas-api/src/test/java/io/openbas/rest/exercise/service/ExerciseServiceTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/exercise/service/ExerciseServiceTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/exercise/service/ExerciseServiceTest.java
@@ -19,10 +19,10 @@
 import io.openbas.service.VariableService;
 import io.openbas.telemetry.metric_collectors.ActionMetricCollector;
 import io.openbas.utils.AtomicTestingUtils;
-import io.openbas.utils.ExerciseMapper;
-import io.openbas.utils.InjectMapper;
 import io.openbas.utils.ResultUtils;
 import io.openbas.utils.fixtures.*;
+import io.openbas.utils.mapper.ExerciseMapper;
+import io.openbas.utils.mapper.InjectMapper;
 import java.util.*;
 import org.junit.jupiter.api.*;
 import org.junit.jupiter.api.extension.ExtendWith;
diff --git a/openbas-api/src/test/java/io/openbas/rest/finding/FindingApiTest.java b/openbas-api/src/test/java/io/openbas/rest/finding/FindingApiTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/finding/FindingApiTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/finding/FindingApiTest.java
@@ -22,9 +22,9 @@
 import io.openbas.database.repository.FindingRepository;
 import io.openbas.database.specification.FindingSpecification;
 import io.openbas.rest.finding.form.RelatedFindingOutput;
-import io.openbas.utils.FindingMapper;
 import io.openbas.utils.fixtures.*;
 import io.openbas.utils.fixtures.composers.*;
+import io.openbas.utils.mapper.FindingMapper;
 import io.openbas.utils.mockUser.WithMockAdminUser;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import io.openbas.utils.pagination.SortField;
diff --git a/openbas-api/src/test/java/io/openbas/rest/inject/service/InjectServiceTest.java b/openbas-api/src/test/java/io/openbas/rest/inject/service/InjectServiceTest.java
--- a/openbas-api/src/test/java/io/openbas/rest/inject/service/InjectServiceTest.java
+++ b/openbas-api/src/test/java/io/openbas/rest/inject/service/InjectServiceTest.java
@@ -21,9 +21,9 @@
 import io.openbas.service.AssetGroupService;
 import io.openbas.service.AssetService;
 import io.openbas.service.UserService;
-import io.openbas.utils.InjectMapper;
 import io.openbas.utils.InjectUtils;
 import io.openbas.utils.fixtures.AssetGroupFixture;
+import io.openbas.utils.mapper.InjectMapper;
 import io.openbas.utils.pagination.SearchPaginationInput;
 import java.util.ArrayList;
 import java.util.Collections;
diff --git a/openbas-api/src/test/java/io/openbas/service/ExerciseServiceIntegrationTest.java b/openbas-api/src/test/java/io/openbas/service/ExerciseServiceIntegrationTest.java
--- a/openbas-api/src/test/java/io/openbas/service/ExerciseServiceIntegrationTest.java
+++ b/openbas-api/src/test/java/io/openbas/service/ExerciseServiceIntegrationTest.java
@@ -18,10 +18,10 @@
 import io.openbas.rest.inject.service.InjectDuplicateService;
 import io.openbas.rest.inject.service.InjectService;
 import io.openbas.telemetry.metric_collectors.ActionMetricCollector;
-import io.openbas.utils.ExerciseMapper;
-import io.openbas.utils.InjectMapper;
 import io.openbas.utils.ResultUtils;
 import io.openbas.utils.fixtures.ExerciseFixture;
+import io.openbas.utils.mapper.ExerciseMapper;
+import io.openbas.utils.mapper.InjectMapper;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.jupiter.api.*;
diff --git a/openbas-api/src/test/java/io/openbas/service/ScenarioServiceTest.java b/openbas-api/src/test/java/io/openbas/service/ScenarioServiceTest.java
--- a/openbas-api/src/test/java/io/openbas/service/ScenarioServiceTest.java
+++ b/openbas-api/src/test/java/io/openbas/service/ScenarioServiceTest.java
@@ -17,10 +17,10 @@
 import io.openbas.rest.inject.service.InjectDuplicateService;
 import io.openbas.rest.inject.service.InjectService;
 import io.openbas.telemetry.metric_collectors.ActionMetricCollector;
-import io.openbas.utils.ExerciseMapper;
 import io.openbas.utils.fixtures.AssetGroupFixture;
 import io.openbas.utils.fixtures.ScenarioFixture;
 import io.openbas.utils.fixtures.TagFixture;
+import io.openbas.utils.mapper.ExerciseMapper;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/EndpointFixture.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/EndpointFixture.java
--- a/openbas-api/src/test/java/io/openbas/utils/fixtures/EndpointFixture.java
+++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/EndpointFixture.java
@@ -3,7 +3,7 @@
 import io.openbas.database.model.Endpoint;
 import io.openbas.rest.asset.endpoint.form.EndpointInput;
 import io.openbas.rest.asset.endpoint.form.EndpointRegisterInput;
-import io.openbas.utils.EndpointMapper;
+import io.openbas.utils.mapper.EndpointMapper;
 import java.time.Instant;
 import java.util.List;
 
diff --git a/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/DocumentComposer.java b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/DocumentComposer.java
--- a/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/DocumentComposer.java
+++ b/openbas-api/src/test/java/io/openbas/utils/fixtures/composers/DocumentComposer.java
@@ -1,5 +1,6 @@
 package io.openbas.utils.fixtures.composers;
 
+import io.openbas.database.model.Challenge;
 import io.openbas.database.model.Document;
 import io.openbas.database.model.Tag;
 import io.openbas.database.repository.DocumentRepository;
@@ -17,13 +18,16 @@
 
 @Component
 public class DocumentComposer extends ComposerBase<Document> {
+
   @Autowired private DocumentRepository documentRepository;
   @Autowired private FileService fileService;
 
   public class Composer extends InnerComposerBase<Document> {
+
     private final Document document;
     private BaseFile<?> companionFile = null;
     private final List<TagComposer.Composer> tagComposers = new ArrayList<>();
+    private final List<ChallengeComposer.Composer> challengeComposers = new ArrayList<>();
 
     public Composer(Document document) {
       this.document = document;
@@ -37,6 +41,14 @@ public Composer withTag(TagComposer.Composer tagComposer) {
       return this;
     }
 
+    public Composer withChallenge(ChallengeComposer.Composer challengeComposer) {
+      challengeComposers.add(challengeComposer);
+      Set<Challenge> tempChallenges = this.document.getChallenges();
+      tempChallenges.add(challengeComposer.get());
+      this.document.setChallenges(tempChallenges);
+      return this;
+    }
+
     public Composer withId(String id) {
       this.document.setId(id);
       return this;
@@ -52,6 +64,7 @@ public Composer withInMemoryFile(BaseFile<?> file) {
     @Override
     public Composer persist() {
       this.tagComposers.forEach(TagComposer.Composer::persist);
+      this.challengeComposers.forEach(ChallengeComposer.Composer::persist);
       if (companionFile != null) {
         try (ByteArrayInputStream bais =
             new ByteArrayInputStream(companionFile.getContentBytes())) {
@@ -68,6 +81,7 @@ public Composer persist() {
     public Composer delete() {
       documentRepository.delete(document);
       this.tagComposers.forEach(TagComposer.Composer::delete);
+      this.challengeComposers.forEach(ChallengeComposer.Composer::delete);
       return this;
     }