Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,22 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.extern.slf4j.Slf4j;

/** Abstract base class providing common functionality for structured output processor handlers. */
@Slf4j
public abstract class AbstractOutputProcessor implements OutputProcessor {

protected final ContractOutputType type;
protected final ContractOutputTechnicalType technicalType;
protected final List<ContractOutputField> fields;
protected final boolean isFindingCompatible;

protected AbstractOutputProcessor(
ContractOutputType type,
ContractOutputTechnicalType technicalType,
List<ContractOutputField> fields,
boolean isFindingCompatible) {
List<ContractOutputField> fields) {
this.type = type;
this.technicalType = technicalType;
this.fields = fields;
this.isFindingCompatible = isFindingCompatible;
}

@Override
Expand All @@ -46,57 +40,22 @@ public List<ContractOutputField> getFields() {
return fields;
}

@Override
public boolean isFindingCompatible() {
return isFindingCompatible;
}

@Override
public boolean validate(JsonNode jsonNode) {
return jsonNode != null;
}

// FINDING METHODS
// Override these in handlers that support findings

/**
* Convert JSON node to finding value string. Override this method if handler supports findings.
* Default returns empty string with warning log.
*/
@Override
public String toFindingValue(JsonNode jsonNode) {
log.warn("Handler {} does not implement toFindingValue, returning empty string", type);
return "";
}

/**
* Extract asset IDs from JSON node for finding linking. Override to provide custom logic, default
* returns empty list.
*/
public List<String> toFindingAssets(JsonNode jsonNode) {
log.warn("Handler {} does not implement toFindingAssets, returning an empty list", type);
return Collections.emptyList();
}

/**
* Extract user IDs from JSON node for finding linking. Override to provide custom logic, default
* returns empty list.
*/
public List<String> toFindingUsers(JsonNode jsonNode) {
log.warn("Handler {} does not implement toFindingUsers, returning an empty list", type);
return Collections.emptyList();
}
// UTILITY methods

/**
* Extract team IDs from JSON node for finding linking. Override to provide custom logic, default
* returns empty list.
* Builds a string representation from a JSON node.
*
* <p>If the node is an array, concatenates all elements (with quotes trimmed) separated by
* spaces. Otherwise, returns the node's text value with quotes trimmed.
*
* @param jsonNode the JSON node to process
* @return a string representation of the node's value(s)
*/
public List<String> toFindingTeams(JsonNode jsonNode) {
log.warn("Handler {} does not implement toFindingTeams, returning an empty list", type);
return Collections.emptyList();
}

// Utility methods
protected String buildString(@NotNull final JsonNode jsonNode) {
if (jsonNode.isArray()) {
List<String> values = new ArrayList<>();
Expand All @@ -108,6 +67,16 @@ protected String buildString(@NotNull final JsonNode jsonNode) {
return trimQuotes(jsonNode.asText());
}

/**
* Builds a string representation from a specific key in a JSON node.
*
* <p>If the key is missing or null, returns an empty string. Otherwise, delegates to {@link
* #buildString(JsonNode)}.
*
* @param jsonNode the JSON node to process
* @param key the key to extract
* @return a string representation of the value at the given key, or empty string if not present
*/
protected String buildString(@NotNull final JsonNode jsonNode, @NotBlank final String key) {
JsonNode valueNode = jsonNode.get(key);
if (valueNode == null || valueNode.isNull()) {
Expand All @@ -116,6 +85,12 @@ protected String buildString(@NotNull final JsonNode jsonNode, @NotBlank final S
return buildString(valueNode);
}

/**
* Removes leading and trailing quotes from a string value.
*
* @param value the string to trim
* @return the string without leading/trailing quotes
*/
protected String trimQuotes(@NotBlank final String value) {
return value.replaceAll("^\"|\"$", "");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,170 @@
package io.openaev.output_processor;

import io.openaev.database.model.ContractOutputTechnicalType;
import io.openaev.database.model.ContractOutputType;
import java.util.List;
import static io.openaev.database.model.AssetType.Values.ENDPOINT_TYPE;
import static io.openaev.database.model.AssetType.Values.SECURITY_PLATFORM_TYPE;

import com.fasterxml.jackson.databind.JsonNode;
import io.openaev.database.model.*;
import io.openaev.rest.asset.endpoint.form.EndpointInput;
import io.openaev.rest.inject.service.ContractOutputContext;
import io.openaev.rest.inject.service.ExecutionProcessingContext;
import io.openaev.rest.tag.TagService;
import io.openaev.service.EndpointService;
import java.util.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class AssetOutputProcessor extends AbstractOutputProcessor {

public AssetOutputProcessor() {
super(ContractOutputType.Asset, ContractOutputTechnicalType.Object, List.of(), false);
private static final String NAME = "name";
private static final String TYPE = "type";
private static final String DESCRIPTION = "description";
private static final String EXTERNAL_REFERENCE = "external_reference";
private static final String TAGS = "tags";
private static final String EXTENDED_ATTRIBUTES = "extended_attributes";
private static final String PLATFORM = "platform";
private static final String ARCH = "arch";
private static final String IP_ADDRESSES = "ip_addresses";
private static final String HOSTNAME = "hostname";
private static final String MAC_ADDRESSES = "mac_addresses";
private static final String END_OF_LIFE = "end_of_life";
private final EndpointService endpointService;
private final TagService tagService;

public AssetOutputProcessor(EndpointService endpointService, TagService tagService) {
super(
ContractOutputType.Asset,
ContractOutputTechnicalType.Object,
List.of(
new ContractOutputField(NAME, ContractOutputTechnicalType.Text, true),
new ContractOutputField(TYPE, ContractOutputTechnicalType.Text, true),
new ContractOutputField(DESCRIPTION, ContractOutputTechnicalType.Text, false),
new ContractOutputField(EXTERNAL_REFERENCE, ContractOutputTechnicalType.Text, true),
new ContractOutputField(TAGS, ContractOutputTechnicalType.Text, true),
new ContractOutputField(
EXTENDED_ATTRIBUTES, ContractOutputTechnicalType.Object, true)));
this.endpointService = endpointService;
this.tagService = tagService;
}

@Override
public boolean validate(JsonNode jsonNode) {
if (!jsonNode.hasNonNull(NAME)
|| !jsonNode.hasNonNull(TYPE)
|| !jsonNode.hasNonNull(EXTERNAL_REFERENCE)) {
return false;
}
String type = jsonNode.path(TYPE).asText();
return switch (type) {
case ENDPOINT_TYPE -> validateEndpoint(jsonNode.path(EXTENDED_ATTRIBUTES));
case SECURITY_PLATFORM_TYPE -> true;
default -> false;
};
}

private boolean validateEndpoint(JsonNode extended) {
return extended.hasNonNull(PLATFORM) && extended.hasNonNull(ARCH);
}

@Override
public void process(
ExecutionProcessingContext executionContext,
ContractOutputContext contractOutputContext,
JsonNode structuredOutputNode) {
if (!structuredOutputNode.isArray()) {
log.warn(
"Expected an array node for asset output, got: {}", structuredOutputNode.getNodeType());
return;
}
for (JsonNode assetNode : structuredOutputNode) {
if (!validate(assetNode)) {
log.warn("Invalid asset node, skipping: {}", assetNode);
continue;
}
String type = assetNode.path(TYPE).asText();
switch (type) {
case ENDPOINT_TYPE -> processEndpoint(executionContext, assetNode);
case SECURITY_PLATFORM_TYPE -> log.info("SecurityPlatform not yet supported, skipping");
}
}
}

/**
* Processes a single endpoint asset node. Creates the endpoint if it does not already exist.
*
* @param assetNode The JSON node representing the endpoint asset
*/
private void processEndpoint(ExecutionProcessingContext executionContext, JsonNode assetNode) {
EndpointInput input = buildEndpointInput(assetNode);
Optional<Endpoint> existing = endpointService.findExistingEndpoint(input);
if (existing.isPresent()) {
log.info("Endpoint already exists: {} (id={})", input.getName(), existing.get().getId());
return;
}
Endpoint created = endpointService.createEndpoint(input);
log.info("Created endpoint: {} (id={})", input.getName(), created.getId());
}

/**
* Builds an EndpointInput object from a JSON asset node, extracting all relevant fields.
*
* @param assetNode The JSON node representing the endpoint asset
* @return EndpointInput populated with asset data
*/
private EndpointInput buildEndpointInput(JsonNode assetNode) {
JsonNode extended = assetNode.path(EXTENDED_ATTRIBUTES);

EndpointInput input = new EndpointInput();

// AssetInput fields
input.setName(assetNode.path(NAME).asText());
input.setDescription(assetNode.path(DESCRIPTION).asText());
String external = assetNode.path(EXTERNAL_REFERENCE).asText();
input.setExternalReference(external.isEmpty() ? null : external);

// Tags
Set<String> tagNames = new HashSet<>();
for (JsonNode tag : assetNode.path(TAGS)) {
tagNames.add(tag.asText());
}
input.setTagIds(
tagService.findOrCreateTagsFromNames(tagNames).stream().map(Tag::getId).toList());

// Platform and arch
input.setPlatform(Endpoint.PLATFORM_TYPE.fromString(extended.path(PLATFORM).asText()));
input.setArch(Endpoint.PLATFORM_ARCH.fromString(extended.path(ARCH).asText()));

// IPs
JsonNode ipNode = extended.path(IP_ADDRESSES);
input.setIps(toStringArray(ipNode));

// Hostname
String hostname = extended.path(HOSTNAME).asText();
input.setHostname(hostname);

// MAC addresses
JsonNode macNode = extended.path(MAC_ADDRESSES);
input.setMacAddresses(toStringArray(macNode));

// EoL
input.setEol(extended.path(END_OF_LIFE).asBoolean(false));

return input;
}

private String[] toStringArray(JsonNode node) {
if (node.isArray()) {
List<String> values = new ArrayList<>();
node.forEach(n -> values.add(n.asText()));
return values.toArray(new String[0]);
}

if (!node.isMissingNode() && !node.isNull()) {
return new String[] {node.asText()};
}

return new String[0];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@
import io.openaev.database.model.ContractOutputField;
import io.openaev.database.model.ContractOutputTechnicalType;
import io.openaev.database.model.ContractOutputType;
import io.openaev.rest.finding.FindingService;
import io.openaev.rest.inject.service.ExecutionProcessingContext;
import io.openaev.service.InjectExpectationService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.stereotype.Component;

@Component
public class CVEOutputProcessor extends AbstractOutputProcessor {
public class CVEOutputProcessor extends FindingCapableOutputProcessor {

private static final String ASSET_ID = "asset_id";
private static final String ID = "id";
private static final String HOST = "host";
private static final String SEVERITY = "severity";

public CVEOutputProcessor() {
private final InjectExpectationService injectExpectationService;

public CVEOutputProcessor(
FindingService findingService, InjectExpectationService injectExpectationService) {
super(
ContractOutputType.CVE,
ContractOutputTechnicalType.Object,
Expand All @@ -26,15 +32,23 @@ public CVEOutputProcessor() {
new ContractOutputField(ID, ContractOutputTechnicalType.Text, true),
new ContractOutputField(HOST, ContractOutputTechnicalType.Text, true),
new ContractOutputField(SEVERITY, ContractOutputTechnicalType.Text, true)),
true);
findingService);
this.injectExpectationService = injectExpectationService;
}

@Override
public boolean validate(JsonNode jsonNode) {
return jsonNode.hasNonNull(ID) && jsonNode.hasNonNull(HOST) && jsonNode.hasNonNull(SEVERITY);
}

// Findings
/** Matches vulnerability expectations after findings are generated. */
@Override
protected void afterFindings(
ExecutionProcessingContext executionContext, JsonNode structuredOutputNode) {
injectExpectationService.matchesVulnerabilityExpectations(
executionContext, structuredOutputNode);
}

@Override
public String toFindingValue(JsonNode jsonNode) {
return buildString(jsonNode, ID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,24 @@
import io.openaev.database.model.ContractOutputField;
import io.openaev.database.model.ContractOutputTechnicalType;
import io.openaev.database.model.ContractOutputType;
import io.openaev.rest.finding.FindingService;
import java.util.List;
import org.springframework.stereotype.Component;

@Component
public class CredentialsOutputProcessor extends AbstractOutputProcessor {
public class CredentialsOutputProcessor extends FindingCapableOutputProcessor {

private static final String USERNAME = "username";
private static final String PASSWORD = "password";

public CredentialsOutputProcessor() {
public CredentialsOutputProcessor(FindingService findingService) {
super(
ContractOutputType.Credentials,
ContractOutputTechnicalType.Object,
List.of(
new ContractOutputField(USERNAME, ContractOutputTechnicalType.Text, true),
new ContractOutputField(PASSWORD, ContractOutputTechnicalType.Text, true)),
true);
findingService);
}

@Override
Expand Down
Loading
Loading