Skip to content

Commit fffbd18

Browse files
committed
[backend] feat: extract ContractOutputType business logic into handler classes (#4302)
1 parent a9f6deb commit fffbd18

26 files changed

Lines changed: 901 additions & 263 deletions

openaev-api/src/main/java/io/openaev/importer/V1_DataImporter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1308,7 +1308,7 @@ private InjectorContract importInjectorContractFromStarterPack(JsonNode importNo
13081308

13091309
public static ContractOutputType formatStringToContractOutputType(String value) {
13101310
for (ContractOutputType type : ContractOutputType.values()) {
1311-
if (type.label.equalsIgnoreCase(value)) {
1311+
if (type.getLabel().equalsIgnoreCase(value)) {
13121312
return type;
13131313
}
13141314
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package io.openaev.output_processor;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.openaev.database.model.ContractOutputField;
5+
import io.openaev.database.model.ContractOutputTechnicalType;
6+
import io.openaev.database.model.ContractOutputType;
7+
import jakarta.validation.constraints.NotBlank;
8+
import jakarta.validation.constraints.NotNull;
9+
import java.util.ArrayList;
10+
import java.util.Collections;
11+
import java.util.List;
12+
import lombok.extern.slf4j.Slf4j;
13+
14+
/** Abstract base class providing common functionality for structured output processor handlers. */
15+
@Slf4j
16+
public abstract class AbstractOutputProcessor implements OutputProcessor {
17+
18+
protected final ContractOutputType type;
19+
protected final ContractOutputTechnicalType technicalType;
20+
protected final List<ContractOutputField> fields;
21+
protected final boolean isFindingCompatible;
22+
23+
protected AbstractOutputProcessor(
24+
ContractOutputType type,
25+
ContractOutputTechnicalType technicalType,
26+
List<ContractOutputField> fields,
27+
boolean isFindingCompatible) {
28+
this.type = type;
29+
this.technicalType = technicalType;
30+
this.fields = fields;
31+
this.isFindingCompatible = isFindingCompatible;
32+
}
33+
34+
@Override
35+
public ContractOutputType getType() {
36+
return type;
37+
}
38+
39+
@Override
40+
public ContractOutputTechnicalType getTechnicalType() {
41+
return technicalType;
42+
}
43+
44+
@Override
45+
public List<ContractOutputField> getFields() {
46+
return fields;
47+
}
48+
49+
@Override
50+
public boolean isFindingCompatible() {
51+
return isFindingCompatible;
52+
}
53+
54+
@Override
55+
public boolean validate(JsonNode jsonNode) {
56+
return jsonNode != null;
57+
}
58+
59+
// FINDING METHODS
60+
// Override these in handlers that support findings
61+
62+
/**
63+
* Convert JSON node to finding value string. Override this method if handler supports findings.
64+
* Default returns empty string with warning log.
65+
*/
66+
@Override
67+
public String toFindingValue(JsonNode jsonNode) {
68+
log.warn("Handler {} does not implement toFindingValue, returning empty string", type);
69+
return "";
70+
}
71+
72+
/**
73+
* Extract asset IDs from JSON node for finding linking. Override to provide custom logic, default
74+
* returns empty list.
75+
*/
76+
public List<String> toFindingAssets(JsonNode jsonNode) {
77+
log.warn("Handler {} does not implement toFindingAssets, returning an empty list", type);
78+
return Collections.emptyList();
79+
}
80+
81+
/**
82+
* Extract user IDs from JSON node for finding linking. Override to provide custom logic, default
83+
* returns empty list.
84+
*/
85+
public List<String> toFindingUsers(JsonNode jsonNode) {
86+
log.warn("Handler {} does not implement toFindingUsers, returning an empty list", type);
87+
return Collections.emptyList();
88+
}
89+
90+
/**
91+
* Extract team IDs from JSON node for finding linking. Override to provide custom logic, default
92+
* returns empty list.
93+
*/
94+
public List<String> toFindingTeams(JsonNode jsonNode) {
95+
log.warn("Handler {} does not implement toFindingTeams, returning an empty list", type);
96+
return Collections.emptyList();
97+
}
98+
99+
// Utility methods
100+
protected String buildString(@NotNull final JsonNode jsonNode) {
101+
if (jsonNode.isArray()) {
102+
List<String> values = new ArrayList<>();
103+
for (JsonNode element : jsonNode) {
104+
values.add(trimQuotes(element.asText()));
105+
}
106+
return String.join(" ", values);
107+
}
108+
return trimQuotes(jsonNode.asText());
109+
}
110+
111+
protected String buildString(@NotNull final JsonNode jsonNode, @NotBlank final String key) {
112+
JsonNode valueNode = jsonNode.get(key);
113+
if (valueNode == null || valueNode.isNull()) {
114+
return "";
115+
}
116+
return buildString(valueNode);
117+
}
118+
119+
protected String trimQuotes(@NotBlank final String value) {
120+
return value.replaceAll("^\"|\"$", "");
121+
}
122+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.openaev.output_processor;
2+
3+
import io.openaev.database.model.ContractOutputTechnicalType;
4+
import io.openaev.database.model.ContractOutputType;
5+
import java.util.List;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class AssetOutputProcessor extends AbstractOutputProcessor {
10+
11+
public AssetOutputProcessor() {
12+
super(ContractOutputType.Asset, ContractOutputTechnicalType.Object, List.of(), false);
13+
}
14+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.openaev.output_processor;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.openaev.database.model.ContractOutputField;
5+
import io.openaev.database.model.ContractOutputTechnicalType;
6+
import io.openaev.database.model.ContractOutputType;
7+
import java.util.ArrayList;
8+
import java.util.Collections;
9+
import java.util.List;
10+
import org.springframework.stereotype.Component;
11+
12+
@Component
13+
public class CVEOutputProcessor extends AbstractOutputProcessor {
14+
15+
private static final String ASSET_ID = "asset_id";
16+
private static final String ID = "id";
17+
private static final String HOST = "host";
18+
private static final String SEVERITY = "severity";
19+
20+
public CVEOutputProcessor() {
21+
super(
22+
ContractOutputType.CVE,
23+
ContractOutputTechnicalType.Object,
24+
List.of(
25+
new ContractOutputField(ASSET_ID, ContractOutputTechnicalType.Text, false),
26+
new ContractOutputField(ID, ContractOutputTechnicalType.Text, true),
27+
new ContractOutputField(HOST, ContractOutputTechnicalType.Text, true),
28+
new ContractOutputField(SEVERITY, ContractOutputTechnicalType.Text, true)),
29+
true);
30+
}
31+
32+
@Override
33+
public boolean validate(JsonNode jsonNode) {
34+
return jsonNode.hasNonNull(ID) && jsonNode.hasNonNull(HOST) && jsonNode.hasNonNull(SEVERITY);
35+
}
36+
37+
// Findings
38+
@Override
39+
public String toFindingValue(JsonNode jsonNode) {
40+
return buildString(jsonNode, ID);
41+
}
42+
43+
@Override
44+
public List<String> toFindingAssets(JsonNode jsonNode) {
45+
JsonNode assetIdNode = jsonNode.get(ASSET_ID);
46+
if (assetIdNode == null) {
47+
return Collections.emptyList();
48+
}
49+
if (assetIdNode.isArray()) {
50+
List<String> result = new ArrayList<>();
51+
for (JsonNode idNode : assetIdNode) {
52+
result.add(idNode.asText());
53+
}
54+
return result;
55+
}
56+
return List.of(assetIdNode.asText());
57+
}
58+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.openaev.output_processor;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.openaev.database.model.ContractOutputField;
5+
import io.openaev.database.model.ContractOutputTechnicalType;
6+
import io.openaev.database.model.ContractOutputType;
7+
import java.util.List;
8+
import org.springframework.stereotype.Component;
9+
10+
@Component
11+
public class CredentialsOutputProcessor extends AbstractOutputProcessor {
12+
13+
private static final String USERNAME = "username";
14+
private static final String PASSWORD = "password";
15+
16+
public CredentialsOutputProcessor() {
17+
super(
18+
ContractOutputType.Credentials,
19+
ContractOutputTechnicalType.Object,
20+
List.of(
21+
new ContractOutputField(USERNAME, ContractOutputTechnicalType.Text, true),
22+
new ContractOutputField(PASSWORD, ContractOutputTechnicalType.Text, true)),
23+
true);
24+
}
25+
26+
@Override
27+
public boolean validate(JsonNode jsonNode) {
28+
return jsonNode.hasNonNull(USERNAME) && jsonNode.hasNonNull(PASSWORD);
29+
}
30+
31+
@Override
32+
public String toFindingValue(JsonNode jsonNode) {
33+
String username = buildString(jsonNode, USERNAME);
34+
String password = buildString(jsonNode, PASSWORD);
35+
return username + ":" + password;
36+
}
37+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.openaev.output_processor;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.openaev.database.model.ContractOutputTechnicalType;
5+
import io.openaev.database.model.ContractOutputType;
6+
import java.util.List;
7+
import org.apache.commons.validator.routines.InetAddressValidator;
8+
import org.springframework.stereotype.Component;
9+
10+
@Component
11+
public class IPv4OutputProcessor extends AbstractOutputProcessor {
12+
13+
private static final InetAddressValidator VALIDATOR = InetAddressValidator.getInstance();
14+
15+
public IPv4OutputProcessor() {
16+
super(ContractOutputType.IPv4, ContractOutputTechnicalType.Text, List.of(), true);
17+
}
18+
19+
@Override
20+
public boolean validate(JsonNode jsonNode) {
21+
return VALIDATOR.isValidInet4Address(jsonNode.asText());
22+
}
23+
24+
@Override
25+
public String toFindingValue(JsonNode jsonNode) {
26+
return buildString(jsonNode);
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.openaev.output_processor;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.openaev.database.model.ContractOutputTechnicalType;
5+
import io.openaev.database.model.ContractOutputType;
6+
import java.util.List;
7+
import org.apache.commons.validator.routines.InetAddressValidator;
8+
import org.springframework.stereotype.Component;
9+
10+
@Component
11+
public class IPv6OutputProcessor extends AbstractOutputProcessor {
12+
13+
private static final InetAddressValidator VALIDATOR = InetAddressValidator.getInstance();
14+
15+
public IPv6OutputProcessor() {
16+
super(ContractOutputType.IPv6, ContractOutputTechnicalType.Text, List.of(), true);
17+
}
18+
19+
@Override
20+
public boolean validate(JsonNode jsonNode) {
21+
return VALIDATOR.isValidInet6Address(jsonNode.asText());
22+
}
23+
24+
@Override
25+
public String toFindingValue(JsonNode jsonNode) {
26+
return buildString(jsonNode);
27+
}
28+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.openaev.output_processor;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.openaev.database.model.ContractOutputTechnicalType;
5+
import io.openaev.database.model.ContractOutputType;
6+
import java.util.List;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
public class NumberOutputProcessor extends AbstractOutputProcessor {
11+
12+
public NumberOutputProcessor() {
13+
super(ContractOutputType.Number, ContractOutputTechnicalType.Number, List.of(), true);
14+
}
15+
16+
@Override
17+
public String toFindingValue(JsonNode jsonNode) {
18+
return buildString(jsonNode);
19+
}
20+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.openaev.output_processor;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.openaev.database.model.ContractOutputField;
5+
import io.openaev.database.model.ContractOutputTechnicalType;
6+
import io.openaev.database.model.ContractOutputType;
7+
import java.util.List;
8+
9+
/**
10+
* Handler interface for processing structured outputs in different contexts. Implementations of
11+
* this interface will define how to validate and process structured outputs based on their type and
12+
* technical type, as well as the contexts they support.
13+
*/
14+
public interface OutputProcessor {
15+
16+
/** Get the type (matches ContractOutputType enum) */
17+
ContractOutputType getType();
18+
19+
/** Get the technical type (matches ContractOutputTechnicalType enum) */
20+
ContractOutputTechnicalType getTechnicalType();
21+
22+
/** Get fields */
23+
List<ContractOutputField> getFields();
24+
25+
/** Is finding compatible */
26+
boolean isFindingCompatible();
27+
28+
/** Validate that the JSON node is correctly formatted for this type */
29+
boolean validate(JsonNode jsonNode);
30+
31+
// FINDING methods
32+
String toFindingValue(JsonNode jsonNode);
33+
34+
List<String> toFindingAssets(JsonNode jsonNode);
35+
36+
List<String> toFindingUsers(JsonNode jsonNode);
37+
38+
List<String> toFindingTeams(JsonNode jsonNode);
39+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.openaev.output_processor;
2+
3+
import io.openaev.database.model.ContractOutputType;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Optional;
7+
import java.util.function.Function;
8+
import java.util.stream.Collectors;
9+
import org.springframework.stereotype.Component;
10+
11+
@Component
12+
public class OutputProcessorFactory {
13+
14+
private final Map<ContractOutputType, OutputProcessor> outputProcessorHandlerMap;
15+
16+
public OutputProcessorFactory(List<OutputProcessor> handlers) {
17+
this.outputProcessorHandlerMap =
18+
handlers.stream().collect(Collectors.toMap(OutputProcessor::getType, Function.identity()));
19+
}
20+
21+
public OutputProcessor getHandler(ContractOutputType type) {
22+
return Optional.ofNullable(outputProcessorHandlerMap.get(type))
23+
.orElseThrow(
24+
() ->
25+
new IllegalArgumentException(
26+
"No handler found for type: "
27+
+ type
28+
+ ". Available types: "
29+
+ outputProcessorHandlerMap.keySet()));
30+
}
31+
}

0 commit comments

Comments
 (0)