Skip to content
Open
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 @@ -37,11 +37,11 @@
import com.fortify.cli.aviator.grpc.CorrelationResult;
import com.fortify.cli.aviator.grpc.CorrelationStreamConfig;
import com.fortify.cli.aviator.grpc.CorrelationStreamProcessor;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCAttributeHelper;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelateDownloadHelper;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelateFprParser;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelateFprParser.ParseResult;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelateHelper;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelationAttributeHelper;
import com.fortify.cli.aviator.ssc.helper.CategoryBucket;
import com.fortify.cli.aviator.ssc.helper.CategoryGrouper;
import com.fortify.cli.aviator.ssc.helper.DastFprCorrelationEnricher;
Expand Down Expand Up @@ -260,7 +260,7 @@ private void uploadTaggedSastFpr(Path sastPath, List<CorrelatedPair> confirmed,

private void writeLastCorrelationTimestamp() {
logger.progress("Status: Writing last_correlation timestamp to app version...");
AviatorSSCCorrelationAttributeHelper.writeLastCorrelationTimestamp(unirest, av.getVersionId());
AviatorSSCAttributeHelper.writeLastCorrelationTimestamp(unirest, av.getVersionId());
logger.progress("Status: last_correlation timestamp written successfully.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
import com.fortify.cli.aviator._common.session.user.helper.AviatorUserSessionDescriptor;
import com.fortify.cli.aviator.audit.DastAuditFPR;
import com.fortify.cli.aviator.audit.DastAuditFprResult;
import com.fortify.cli.aviator.audit.DastAuditFprStatus;
import com.fortify.cli.aviator.config.AviatorLoggerImpl;
import com.fortify.cli.aviator.config.IAviatorLogger;
import com.fortify.cli.aviator.config.TagMappingConfig;
import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper;
import com.fortify.cli.aviator.grpc.DastAuditStreamConfig;
import com.fortify.cli.aviator.grpc.DastAuditStreamProcessor;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCAttributeHelper;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCAuditHelper;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCFprTransferHelper;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCTagValidator;
Expand Down Expand Up @@ -97,6 +99,11 @@ public JsonNode getJsonNode(UnirestInstance unirest) {
artifactId = AviatorSSCFprTransferHelper.uploadDastFpr(
unirest, appVersion, downloadedFpr, progressWriter);
}
if (result.status() == DastAuditFprStatus.AUDITED
|| result.status() == DastAuditFprStatus.SKIPPED) {
AviatorSSCAttributeHelper.writeLastDastAuditTimestamp(
unirest, appVersion.getVersionId());
}
return buildOutput(appVersion, result, artifactId);
} catch (RuntimeException e) {
actionResult = "FAILED";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,26 @@
package com.fortify.cli.aviator.ssc.helper;

/**
* Attribute definitions used by the SAST-DAST correlation feature.
* SSC application-version attribute definitions used by Aviator workflows.
*
* <p>These are SSC application-version attributes (not per-issue custom tags).
* The definition is created by the {@code aviator ssc prepare} command and
* the value is written by {@code aviator ssc correlate-sast-dast}.
* the values are written by {@code aviator ssc correlate-sast-dast} and
* {@code aviator ssc audit-dast}.
*/
public final class AviatorSSCCorrelationAttributeDefs {
public final class AviatorSSCAttributeDefinitions {

private AviatorSSCCorrelationAttributeDefs() {}
private AviatorSSCAttributeDefinitions() {}

/**
* Descriptor for a custom SSC attribute definition managed by the Aviator module.
*
* @param guid Fixed GUID — must never change once deployed to an SSC instance.
* @param name Attribute name as it appears in SSC (used for lookup and write).
* @param category SSC attribute category (e.g. {@code "TECHNICAL"}).
* @param type SSC attribute type string (e.g. {@code "TEXT"}, {@code "DATE"}).
* @param description Human-readable description stored in SSC.
*/
public record AttributeDefinition(
String guid,
String name,
String category,
String type,
Expand All @@ -53,10 +52,23 @@ public record AttributeDefinition(
* comparison with artifact {@code lastScanDate} values.
*/
public static final AttributeDefinition LAST_CORRELATION_ATTR = new AttributeDefinition(
"B2C3D4E5-F6A7-8901-BCDE-F12345678901",
"last_correlation",
"TECHNICAL",
"TEXT",
"Timestamp of the last successful SAST-DAST correlation run (ISO-8601 UTC). Written by fcli aviator ssc correlate-sast-dast."
);

/**
* Free-text attribute written after a DAST audit evaluation completes successfully.
*
* <p>The value is an ISO-8601 UTC timestamp. A successful evaluation includes a
* run that finds no eligible findings, allowing bulk DAST audit selection to avoid
* repeating a completed no-op evaluation until a newer DAST scan is available.
*/
public static final AttributeDefinition LAST_DAST_AUDIT_ATTR = new AttributeDefinition(
"last_dast_audit",
"TECHNICAL",
"TEXT",
"Timestamp of the last successful DAST audit evaluation (ISO-8601 UTC). Written by fcli aviator ssc audit-dast."
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,33 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelationAttributeDefs.AttributeDefinition;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCAttributeDefinitions.AttributeDefinition;
import com.fortify.cli.common.exception.FcliSimpleException;
import com.fortify.cli.common.json.JsonHelper;
import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException;
import com.fortify.cli.ssc._common.rest.ssc.SSCUrls;
import com.fortify.cli.ssc.attribute.helper.SSCAttributeUpdateBuilder;

import kong.unirest.UnirestException;
import kong.unirest.UnirestInstance;
import lombok.RequiredArgsConstructor;

/**
* Manages the SSC attribute definitions used by the SAST-DAST correlation feature.
* Manages SSC application-version attributes used by Aviator workflows.
*
* <p>The attribute definition is created by {@code aviator ssc prepare} (admin-only).
* The attribute value is written per application version by
* {@code aviator ssc correlate-sast-dast} (non-admin).
* {@code aviator ssc correlate-sast-dast} and {@code aviator ssc audit-dast} (non-admin).
*
* <p>This is distinct from the generic SSC attribute helpers in the SSC module
* ({@code SSCAttributeHelper}, {@code SSCAttributeDefinitionHelper}) which
* handle reading/updating existing attributes. This class also handles
* <em>creating</em> attribute definitions specific to correlation.
* <em>creating</em> attribute definitions specific to Aviator workflows.
*/
@RequiredArgsConstructor
public class AviatorSSCCorrelationAttributeHelper {
public class AviatorSSCAttributeHelper {

private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCCorrelationAttributeHelper.class);
private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCAttributeHelper.class);
private final UnirestInstance unirest;
private final AttributeDefinition attrDef;

Expand All @@ -63,7 +64,7 @@ public class AviatorSSCCorrelationAttributeHelper {
*/
public void synchronize(AviatorSSCPrepareHelper.PrepareResult result) {
try {
LOG.debug("Searching for attribute definition '{}' (GUID: {})", attrDef.name(), attrDef.guid());
LOG.debug("Searching for SSC attribute definition '{}'", attrDef.name());
if (findDefinition() != null) {
LOG.info("Attribute definition '{}' is already present.", attrDef.name());
result.addEntry("Attribute Definition", "VERIFIED",
Expand All @@ -82,9 +83,18 @@ public void synchronize(AviatorSSCPrepareHelper.PrepareResult result) {
}
}

/** Writes the current UTC timestamp to the {@code last_correlation} attribute. */
public static void writeLastCorrelationTimestamp(UnirestInstance unirest, String versionId) {
writeTimestamp(unirest, versionId, AviatorSSCAttributeDefinitions.LAST_CORRELATION_ATTR);
}

/** Writes the current UTC timestamp to the {@code last_dast_audit} attribute. */
public static void writeLastDastAuditTimestamp(UnirestInstance unirest, String versionId) {
writeTimestamp(unirest, versionId, AviatorSSCAttributeDefinitions.LAST_DAST_AUDIT_ATTR);
}

/**
* Writes the current UTC timestamp to the {@code last_correlation} attribute on
* the given application version.
* Writes the current UTC timestamp to the given attribute on the application version.
*
* <p>This method assumes the attribute definition already exists — it must have
* been created by a prior {@code aviator ssc prepare} run. If the definition
Expand All @@ -93,19 +103,21 @@ public void synchronize(AviatorSSCPrepareHelper.PrepareResult result) {
* @param unirest active SSC session
* @param versionId SSC project version ID
*/
public static void writeLastCorrelationTimestamp(UnirestInstance unirest, String versionId) {
private static void writeTimestamp(
UnirestInstance unirest, String versionId, AttributeDefinition attributeDefinition) {
String timestamp = Instant.now().toString();
LOG.debug("Writing last_correlation timestamp '{}' to app version {}", timestamp, versionId);
LOG.debug("Writing {} timestamp to app version {}", attributeDefinition.name(), versionId);

try {
new SSCAttributeUpdateBuilder(unirest)
.add(Map.of(AviatorSSCCorrelationAttributeDefs.LAST_CORRELATION_ATTR.name(), timestamp))
.add(Map.of(attributeDefinition.category() + ":" + attributeDefinition.name(), timestamp))
.buildRequest(versionId)
.asObject(JsonNode.class);

LOG.info("last_correlation timestamp '{}' written to app version {}", timestamp, versionId);
} catch (FcliSimpleException e) {
LOG.warn("WARN: Could not write last_correlation timestamp. Run 'fcli aviator ssc prepare' to create the attribute definition.");
LOG.info("{} timestamp written to app version {}", attributeDefinition.name(), versionId);
} catch (FcliSimpleException | UnirestException e) {
LOG.warn("Could not write {} timestamp; the audit result remains successful but bulk selection may retry this version. "
+ "Run 'fcli aviator ssc prepare' if the attribute definition is missing.", attributeDefinition.name());
}
}

Expand All @@ -122,7 +134,9 @@ private JsonNode findDefinition() {
JsonNode data = responseBody.get("data");
if (data == null || !data.isArray()) return null;
return JsonHelper.stream((ArrayNode) data)
.filter(n -> attrDef.name().equals(n.path("name").asText()))
.filter(n -> attrDef.name().equals(n.path("name").asText())
&& attrDef.category().equals(n.path("category").asText())
&& attrDef.type().equals(n.path("type").asText()))
.findFirst().orElse(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ private void addOptionalTagWarnings(TagSynchronizationResults tagResults, Prepar
/** Synchronizes Aviator custom attributes. */
private void synchronizeAttributes(PrepareResult result, IProgressWriter progress) {
progress.writeProgress("Synchronizing Aviator custom attributes...");
new AviatorSSCCorrelationAttributeHelper(unirest, AviatorSSCCorrelationAttributeDefs.LAST_CORRELATION_ATTR)
new AviatorSSCAttributeHelper(unirest, AviatorSSCAttributeDefinitions.LAST_CORRELATION_ATTR)
.synchronize(result);
new AviatorSSCAttributeHelper(unirest, AviatorSSCAttributeDefinitions.LAST_DAST_AUDIT_ATTR)
.synchronize(result);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.aviator.ssc.helper;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.databind.JsonNode;
import com.fortify.cli.common.json.JsonHelper;
import com.fortify.cli.common.rest.unirest.UnirestHelper;
import com.fortify.cli.common.rest.unirest.config.UnirestJsonHeaderConfigurer;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;

import kong.unirest.UnirestInstance;

class AviatorSSCAttributeHelperTest {
@Test
void writesLastDastAuditTimestampAsTextAttribute() throws Exception {
try (var server = new TestSscServer(); var unirest = newUnirest(server)) {
AviatorSSCAttributeHelper.writeLastDastAuditTimestamp(unirest, "42");

JsonNode update = server.getLastUpdate();
assertEquals("42", update.get(0).path("attributeDefinitionId").asText());
assertTrue(update.get(0).path("value").asText().matches("\\d{4}-\\d{2}-\\d{2}T.*Z"));
}
}

@Test
void exposesStableDastAuditAttributeDefinition() {
var definition = AviatorSSCAttributeDefinitions.LAST_DAST_AUDIT_ATTR;

assertEquals("last_dast_audit", definition.name());
assertEquals("TECHNICAL", definition.category());
assertEquals("TEXT", definition.type());
}

@Test
void markerWriteFailureIsReportedWithoutThrowing() throws Exception {
try (var server = new TestSscServer().withUpdateStatus(500); var unirest = newUnirest(server)) {
assertDoesNotThrow(() -> AviatorSSCAttributeHelper.writeLastDastAuditTimestamp(unirest, "42"));
}
}

private static UnirestInstance newUnirest(TestSscServer server) {
return UnirestHelper.createUnirestInstance(unirest -> {
UnirestJsonHeaderConfigurer.configure(unirest);
unirest.config().defaultBaseUrl(server.getBaseUrl());
});
}

private static final class TestSscServer implements AutoCloseable {
private final HttpServer server;
private JsonNode lastUpdate;
private int updateStatus = 200;

private TestSscServer() throws IOException {
server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/api/v1/attributeDefinitions", this::handleDefinitions);
server.createContext("/api/v1/projectVersions/42/attributes", this::handleAttributes);
server.start();
}

private String getBaseUrl() {
return "http://localhost:" + server.getAddress().getPort();
}

private JsonNode getLastUpdate() {
return lastUpdate;
}

private TestSscServer withUpdateStatus(int status) {
updateStatus = status;
return this;
}

private void handleDefinitions(HttpExchange exchange) throws IOException {
respond(exchange, 200, """
{"data":[{"id":"42","guid":"C3D4E5F6-A7B8-9012-BCDE-F12345678902","name":"last_dast_audit","category":"TECHNICAL","type":"TEXT","required":false,"hasDefault":false,"options":[]}]}
""");
}

private void handleAttributes(HttpExchange exchange) throws IOException {
if (!"PUT".equals(exchange.getRequestMethod())) {
respond(exchange, 405, "{}");
return;
}
lastUpdate = JsonHelper.getObjectMapper().readTree(exchange.getRequestBody());
respond(exchange, updateStatus, "{}");
}

private static void respond(HttpExchange exchange, int status, String body) throws IOException {
byte[] response = body.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(status, response.length);
try (var output = exchange.getResponseBody()) {
output.write(response);
}
}

@Override
public void close() {
server.stop(0);
}
}
}
Loading
Loading