Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1981d8c
Implement domains
GaetanSantucci Oct 29, 2025
ebca9ae
:construction: Implement Domains
GaetanSantucci Nov 18, 2025
153932b
:construction: Add tests
GaetanSantucci Nov 18, 2025
33c4c8a
:construction: Add relation InjectorContract Domain
GaetanSantucci Nov 18, 2025
1fbe775
:construction: Add relation InjectorContract Domain
GaetanSantucci Nov 18, 2025
b0c60fe
:fire: Set api types
GaetanSantucci Nov 18, 2025
261c4f1
:art: Add Domain to store
GaetanSantucci Nov 18, 2025
742f88f
:bug: fix linter
GaetanSantucci Nov 18, 2025
aac8773
:art: Set domain to payloads component
GaetanSantucci Nov 18, 2025
4687356
Domains translation key
GaetanSantucci Nov 18, 2025
43d6131
:art: Update payload model and create service for domains
GaetanSantucci Nov 20, 2025
d044c58
:art: Update payload model and create service for domains
GaetanSantucci Nov 20, 2025
ad1de12
:art: Add display domains to payload
GaetanSantucci Nov 20, 2025
ffe6945
:art: Add display domains to payload
GaetanSantucci Nov 21, 2025
671a39f
fix linter
GaetanSantucci Nov 21, 2025
61cb866
:art: Add domains to duplicate payload
GaetanSantucci Nov 21, 2025
221f827
:art: Add domains column to injectorContract
GaetanSantucci Nov 21, 2025
dba2cbc
fix lint
GaetanSantucci Nov 21, 2025
4f07645
:bug: fix translation
GaetanSantucci Nov 24, 2025
884451d
:bug: fix test
GaetanSantucci Nov 24, 2025
31fe04b
:bug: fix migration
GaetanSantucci Nov 24, 2025
5c289ea
[backend] feat(SCV): start to fix tests (#4119)
gabriel-peze Nov 24, 2025
b396179
:bug: Fix test
GaetanSantucci Nov 25, 2025
e36a678
Refactors domain handling to use Sets
GaetanSantucci Nov 26, 2025
3781545
:bug: Fix domain - update Unclassified to To classify
GaetanSantucci Nov 26, 2025
6d505bc
:bug: Fix test
GaetanSantucci Nov 26, 2025
3f5ff5a
Fix migration dif
GaetanSantucci Nov 26, 2025
bff2e27
Fix Stix test
GaetanSantucci Nov 27, 2025
1e9921f
:art: Add domain to scenario view
GaetanSantucci Nov 27, 2025
88bda40
:lipstick: Fix linter
GaetanSantucci Nov 27, 2025
a9f5cf8
translate key
GaetanSantucci Nov 27, 2025
fc9eaa0
Update domains migration
GaetanSantucci Nov 27, 2025
08e46c3
:art: Improve code
GaetanSantucci Nov 28, 2025
f39abd1
Improve Domains methods
GaetanSantucci Dec 1, 2025
77c3f9c
WIP
GaetanSantucci Dec 1, 2025
d466ff2
wip on domains
GaetanSantucci Dec 1, 2025
2646bbd
Fix some bugs
GaetanSantucci Dec 1, 2025
329fb35
Fix linter warning
GaetanSantucci Dec 1, 2025
e0443dd
Merge branch 'release/current' into issue/4119
GaetanSantucci Dec 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions openaev-api/src/main/java/io/openaev/importer/V1_DataImporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.openaev.database.repository.*;
import io.openaev.injectors.challenge.model.ChallengeContent;
import io.openaev.injectors.channel.model.ChannelContent;
import io.openaev.rest.domain.DomainService;
import io.openaev.rest.exercise.exports.VariableWithValueMixin;
import io.openaev.rest.inject.form.InjectDependencyInput;
import io.openaev.rest.injector_contract.InjectorContractContentUtils;
Expand Down Expand Up @@ -80,6 +81,7 @@ public class V1_DataImporter implements Importer {
private final InjectDependenciesRepository injectDependenciesRepository;
private final PayloadCreationService payloadCreationService;
private final CollectorRepository collectorRepository;
private final DomainService domainService;

private final InjectorContractContentUtils injectorContractContentUtils;

Expand Down Expand Up @@ -220,6 +222,28 @@ private Tag createTag(JsonNode jsonNode) {
return tag;
}

// -- DOMAINS PATTERN --
private Set<Domain> importDomains(JsonNode importNode, String prefix) {
Set<Domain> domains = new HashSet<>();
resolveJsonElements(importNode, prefix + "domains")
.forEach(
nodeDomain -> {
JsonNode nameNode = nodeDomain.get("domain_name");
if (nameNode == null) {
return;
}

Domain domainCreated =
this.domainService.upsert(
nameNode.textValue(), nodeDomain.get("domain_color").textValue());
domains.add(domainCreated);
});
if (domains.isEmpty()) {
return Set.of(new Domain(null, "To classify", "#FFFFFF", Instant.now(), null));
}
return domains;
}

// -- ATTACK PATTERN --
private List<String> importAttackPattern(
JsonNode importNode, String prefix, Map<String, Base> baseIds) {
Expand Down Expand Up @@ -1353,8 +1377,10 @@ private String importPayloadAsMain(
PayloadCreateInput payloadCreateInput = buildPayload(payloadNode);
payloadCreateInput.setOutputParsers(
buildOutputParsersFromPayloadJsonNode(payloadNode, baseIds));
payloadCreateInput.setDomains(importDomains(payloadNode, "payload_"));

List<String> attackPatternIds = importAttackPattern(payloadNode, "payload_", baseIds);

payloadCreateInput.setAttackPatternsIds(attackPatternIds);
payloadCreateInput.setDetectionRemediations(buildDetectionRemediationsJsonNode(payloadNode));
Payload payload = this.payloadCreationService.createPayload(payloadCreateInput);
Expand Down Expand Up @@ -1392,6 +1418,8 @@ private Optional<InjectorContract> importPayload(
payloadCreateInput.setOutputParsers(
buildOutputParsersFromPayloadJsonNode(payloadNode, baseIds));

payloadCreateInput.setDomains(importDomains(payloadNode, "payload_"));

List<String> attackPatternIds = importAttackPattern(payloadNode, "payload_", baseIds);
payloadCreateInput.setAttackPatternsIds(attackPatternIds);
payloadCreateInput.setDetectionRemediations(buildDetectionRemediationsJsonNode(payloadNode));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.openaev.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_52__Implement_Domains_notion extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
try (Statement stmt = context.getConnection().createStatement()) {
stmt.execute(
"""
CREATE TABLE domains (
domain_id VARCHAR(255) NOT NULL CONSTRAINT domains_pkey PRIMARY KEY,
domain_name VARCHAR(255) NOT NULL UNIQUE,
domain_color VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
domain_created_at TIMESTAMPTZ DEFAULT now(),
domain_updated_at TIMESTAMPTZ DEFAULT now()
);
""");

stmt.execute(
"""
CREATE INDEX idx_domains_domain_name
ON domains(domain_name);
""");

stmt.execute(
"""
CREATE TABLE payloads_domains (
payload_id VARCHAR(255) NOT NULL,
domain_id VARCHAR(255) NOT NULL,
PRIMARY KEY (payload_id, domain_id),
CONSTRAINT fk_payloads_domains_domain FOREIGN KEY (domain_id) REFERENCES domains(domain_id) ON DELETE CASCADE,
CONSTRAINT fk_payloads_domains_payload FOREIGN KEY (payload_id) REFERENCES payloads(payload_id) ON DELETE CASCADE
);
""");

stmt.execute("CREATE INDEX idx_payloads_domains_domain_id ON payloads_domains(domain_id);");
stmt.execute("CREATE INDEX idx_payloads_domains_payload_id ON payloads_domains(payload_id);");

stmt.execute(
"""
CREATE TABLE injectors_contracts_domains (
injector_contract_id VARCHAR(255) NOT NULL,
domain_id VARCHAR(255) NOT NULL,
PRIMARY KEY (injector_contract_id, domain_id),

CONSTRAINT fk_icd_injector_contract
FOREIGN KEY (injector_contract_id)
REFERENCES injectors_contracts(injector_contract_id)
ON DELETE CASCADE,

CONSTRAINT fk_icd_domain
FOREIGN KEY (domain_id)
REFERENCES domains(domain_id)
ON DELETE CASCADE
);
""");

stmt.execute(
"CREATE INDEX idx_icd_injector_contract_id ON injectors_contracts_domains(injector_contract_id);");
stmt.execute("CREATE INDEX idx_icd_domain_id ON injectors_contracts_domains(domain_id);");

stmt.execute(
"INSERT INTO domains (domain_id, domain_name, domain_color) VALUES "
+ " (gen_random_uuid(), 'Endpoint', '#389CFF'),"
+ " (gen_random_uuid(), 'Network', '#009933'),"
+ " (gen_random_uuid(), 'Web App', '#FF9933'),"
+ " (gen_random_uuid(), 'E-mail Infiltration', '#FF6666'),"
+ " (gen_random_uuid(), 'Data Exfiltration', '#9933CC'),"
+ " (gen_random_uuid(), 'URL Filtering', '#66CCFF'),"
+ " (gen_random_uuid(), 'Cloud', '#9999CC'),"
+ " (gen_random_uuid(), 'Table-Top', '#FFCC33'),"
+ " (gen_random_uuid(), 'To classify', '#FFFFFF');");
}
}
}

// Rollback script

// DROP TABLE IF EXISTS domains;
// DROP INDEX IF EXISTS idx_payloads_domains_domain_id;
// DROP INDEX IF EXISTS idx_payloads_domains_payload_id;
// DROP TABLE IF EXISTS payloads_domains;
// DROP INDEX IF EXISTS idx_injectors_contracts_domains_domain_id;
// DROP INDEX IF EXISTS idx_injectors_contracts_domains_injector_contract_id;
// DROP TABLE IF EXISTS injectors_contracts_domains;
54 changes: 54 additions & 0 deletions openaev-api/src/main/java/io/openaev/rest/domain/DomainApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.openaev.rest.domain;

import io.openaev.aop.LogExecutionTime;
import io.openaev.aop.RBAC;
import io.openaev.database.model.Action;
import io.openaev.database.model.Domain;
import io.openaev.database.model.ResourceType;
import io.openaev.rest.domain.form.DomainBaseInput;
import io.openaev.rest.helper.RestBehavior;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@Tag(name = "Domain API", description = "Operations related to Domain")
public class DomainApi extends RestBehavior {

public static final String DOMAIN_URI = "/api/domains";
private final DomainService domainService;

@LogExecutionTime
@Operation(summary = "Search Domains")
@GetMapping(DOMAIN_URI)
@RBAC(actionPerformed = Action.READ, resourceType = ResourceType.PAYLOAD)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not link the RBAC to the Payloads, we should create a new RessourceType DOMAIN.

To do so :
Into ResourceType.java, add the value DOMAIN.
Into PermissionService.java, on the RESOURCES_OPEN EnumSet, add the ResourceType.DOMAIN value.
Into Capability.java, on the MANAGE_PLATFORM_SETTINGS enum value, add those lines :

pair(ResourceType.DOMAIN, Action.CREATE),
pair(ResourceType.DOMAIN, Action.DUPLICATE),
pair(ResourceType.DOMAIN, Action.DELETE),
pair(ResourceType.DOMAIN, Action.WRITE),

And finally on DomainApi.java, change the RBAC to use ResourceType.DOMAIN

public List<Domain> domains() {
return domainService.searchDomains();
}

@Operation(summary = "Get a Domain by ID", description = "Fetches detailed Domain info by ID")
@GetMapping(DOMAIN_URI + "/{domainId}")
@RBAC(
resourceId = "#domainId",
actionPerformed = Action.READ,
resourceType = ResourceType.PAYLOAD)
public Domain getDomain(@PathVariable String domainId) {
return domainService.findById(domainId);
}

@PostMapping(DOMAIN_URI + "/{domainId}/upsert")
@RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.PAYLOAD)
@Transactional(rollbackOn = Exception.class)
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The upserted domain")})
@Operation(description = "Upsert a domain", summary = "Upsert domain")
public Domain upsertDomain(@Valid @RequestBody DomainBaseInput input) {
return domainService.upsertDomain(input);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.openaev.rest.domain;

import io.openaev.database.model.Domain;
import io.openaev.database.repository.DomainRepository;
import io.openaev.rest.domain.form.DomainBaseInput;
import io.openaev.rest.exception.ElementNotFoundException;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import static io.openaev.helper.StreamHelper.fromIterable;
import static io.openaev.utils.StringUtils.generateRandomColor;

@Slf4j
@Service
@RequiredArgsConstructor
public class DomainService {

private static final String DOMAIN_ID_NOT_FOUND_MSG = "Domain not found with id";

private final DomainRepository domainRepository;

public List<Domain> searchDomains() {
return fromIterable(domainRepository.findAll());
}

private Optional<Domain> findByName(final String name) {
return domainRepository.findByName(name);
}

public Domain findById(final String domainId) {
return domainRepository
.findById(domainId)
.orElseThrow(() -> new ElementNotFoundException((String.format("%s: %s", DOMAIN_ID_NOT_FOUND_MSG, domainId))));
}

public Domain upsertDomain(final DomainBaseInput input) {
return this.upsert(input.getName(), input.getColor());
}

public Domain upsert(final Domain domainToUpsert) {
return this.upsert(domainToUpsert.getName(), domainToUpsert.getColor());
}

public Domain upsert(final String name, final String color) {
Optional<Domain> existingDomain = this.findByName(name);
return existingDomain.orElseGet(
() ->
domainRepository.save(
new Domain(
null, name, color != null ? color : generateRandomColor() , Instant.now(), null)));
}

public Set<Domain> upserts(final Set<Domain> domains) {
return domains.stream().map(this::upsert).collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.openaev.rest.domain.form;

import static io.openaev.config.AppConfig.MANDATORY_MESSAGE;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use the @Data annotation instead of @Getter and @Setter for a DTO.
@DaTa is a concatenation of @Getter, @Setter, @equals and @Hashcode

public class DomainBaseInput {
@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("domain_name")
@Schema(description = "Name of the domain")
private String name;

@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("domain_color")
@Schema(description = "Color of the domain")
private String color;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import static io.openaev.database.model.InjectorContract.*;
import static io.openaev.helper.DatabaseHelper.updateRelation;
import static io.openaev.helper.StreamHelper.fromIterable;
import static io.openaev.utils.JpaUtils.createJoinArrayAggOnId;
import static io.openaev.utils.JpaUtils.createLeftJoin;
import static io.openaev.utils.JpaUtils.*;
import static io.openaev.utils.pagination.SortUtilsCriteriaBuilder.toSortCriteriaBuilder;

import com.fasterxml.jackson.databind.JsonNode;
Expand Down Expand Up @@ -319,6 +318,12 @@ private void selectForInjectorContractFull(
Expression<String[]> attackPatternIdsExpression =
createJoinArrayAggOnId(cb, injectorContractRoot, "attackPatterns");

Expression<String[]> domainsIdsExpression =
createJoinArrayAggOnId(cb, injectorContractRoot, "domains");

Expression<String[]> payloadDomainsIdsExpression =
createJoinArrayAggOnIdForJoin(cb, injectorContractPayloadJoin, "domains");

// SELECT
cq.multiselect(
injectorContractRoot.get("id").alias("injector_contract_id"),
Expand All @@ -331,6 +336,8 @@ private void selectForInjectorContractFull(
injectorContractInjectorJoin.get("type").alias("injector_contract_injector_type"),
injectorContractInjectorJoin.get("name").alias("injector_contract_injector_name"),
attackPatternIdsExpression.alias("injector_contract_attack_patterns"),
payloadDomainsIdsExpression.alias("payload_domains"),
domainsIdsExpression.alias("injector_contract_domains"),
injectorContractRoot.get("updatedAt").alias("injector_contract_updated_at"),
injectorContractPayloadJoin.get("executionArch").alias("payload_execution_arch"))
.distinct(true);
Expand Down Expand Up @@ -359,11 +366,23 @@ private List<InjectorContractFullOutput> execInjectorFullContract(TypedQuery<Tup
tuple.get("collector_type", String.class),
tuple.get("injector_contract_injector_type", String.class),
tuple.get("injector_contract_attack_patterns", String[].class),
resolveEffectiveDomains(
tuple.get("injector_contract_domains", String[].class),
tuple.get("payload_domains", String[].class)),
tuple.get("injector_contract_updated_at", Instant.class),
tuple.get("payload_execution_arch", Payload.PAYLOAD_EXECUTION_ARCH.class)))
.toList();
}

private List<String> resolveEffectiveDomains(String[] injectorDomains, String[] payloadDomains) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is a duplication of code from InjectorContractFullOutput, it should be moved to an utils class, with @component annotation

String[] effectiveDomains =
(payloadDomains != null && payloadDomains.length > 0) ? payloadDomains : injectorDomains;
if (effectiveDomains == null) {
return List.of();
}
return Arrays.stream(effectiveDomains).filter(Objects::nonNull).distinct().toList();
}

private void selectForInjectorContractBase(
@NotNull final CriteriaBuilder cb,
@NotNull final CriteriaQuery<Tuple> cq,
Expand Down
Loading