From 78f5b4a1c0cb518de85c441d1a31dafb09f96058 Mon Sep 17 00:00:00 2001
From: Ankit Rathod
Date: Fri, 21 Aug 2026 15:03:52 +0530
Subject: [PATCH] Add DAST audit command and related functionality
- Implemented AviatorSSCDastAuditCommand for auditing DAST findings in SSC applications.
- Created DastAuditRequestMapper and DastAuditResponseMapper for handling DAST audit requests and responses.
- Added unit tests for DAST audit request and response mappers, stream processor, and command.
- Introduced AviatorSSCFprTransferHelper for managing DAST FPR downloads and uploads.
- Updated AviatorSSCAuditHelper to include DAST-specific audit statistics.
- Enhanced AviatorSSCCommands to include the new DAST audit command.
- Updated internationalization properties for DAST audit command messages.
---
.../fcli-app/aviator/resource-config.json | 3 +
.../_common/config/AviatorConfigManager.java | 11 +
.../audit/DastAuditDecisionMapper.java | 73 +++
.../cli/aviator/audit/DastAuditFPR.java | 195 +++++++
.../cli/aviator/audit/DastAuditFprResult.java | 38 ++
.../cli/aviator/config/TagMappingConfig.java | 48 ++
.../fortify/cli/aviator/dast/DastIssue.java | 5 +-
.../cli/aviator/dast/DastReproStep.java | 25 +
.../dast/StreamingWebInspectParser.java | 44 +-
.../aviator/fpr/processor/AuditProcessor.java | 90 ++++
.../cli/aviator/grpc/AviatorGrpcClient.java | 11 +
.../aviator/grpc/DastAuditRequestMapper.java | 66 +++
.../aviator/grpc/DastAuditResponseMapper.java | 50 ++
.../cli/aviator/grpc/DastAuditResult.java | 50 ++
.../aviator/grpc/DastAuditStreamConfig.java | 35 ++
.../grpc/DastAuditStreamProcessor.java | 480 ++++++++++++++++++
.../aviator/grpc/DastAuditStreamResult.java | 27 +
.../cli/aviator/grpc/DastAuditWorkItem.java | 21 +
.../cli/aviator/grpc/RequestHandler.java | 16 +-
.../src/main/proto/dast_audit.proto | 136 +++++
.../resources/default_dast_tag_mapping.yaml | 24 +
.../audit/DastAuditDecisionMapperTest.java | 44 ++
.../cli/aviator/audit/DastAuditFPRTest.java | 161 ++++++
.../aviator/config/TagMappingConfigTest.java | 22 +
.../dast/StreamingWebInspectParserTest.java | 97 ++++
.../grpc/DastAuditRequestMapperTest.java | 50 ++
.../grpc/DastAuditResponseMapperTest.java | 71 +++
.../grpc/DastAuditStreamConfigTest.java | 33 ++
.../grpc/DastAuditStreamProcessorTest.java | 75 +++
.../cli/aviator/grpc/RequestHandlerTest.java | 47 ++
.../ssc/cli/cmd/AviatorSSCAuditCommand.java | 19 +-
.../ssc/cli/cmd/AviatorSSCCommands.java | 1 +
.../AviatorSSCCorrelateSastDastCommand.java | 4 +-
.../cli/cmd/AviatorSSCDastAuditCommand.java | 172 +++++++
.../ssc/helper/AviatorSSCAuditHelper.java | 32 ++
.../AviatorSSCCorrelateDownloadHelper.java | 23 +-
.../helper/AviatorSSCFprTransferHelper.java | 73 +++
.../aviator/i18n/AviatorMessages.properties | 8 +
.../cmd/AviatorSSCDastAuditCommandTest.java | 82 +++
39 files changed, 2406 insertions(+), 56 deletions(-)
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditDecisionMapper.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFPR.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFprResult.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/DastReproStep.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditRequestMapper.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditResponseMapper.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditResult.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamConfig.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamResult.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditWorkItem.java
create mode 100644 fcli-core/fcli-aviator-common/src/main/proto/dast_audit.proto
create mode 100644 fcli-core/fcli-aviator-common/src/main/resources/default_dast_tag_mapping.yaml
create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/DastAuditDecisionMapperTest.java
create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/DastAuditFPRTest.java
create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/dast/StreamingWebInspectParserTest.java
create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditRequestMapperTest.java
create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditResponseMapperTest.java
create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamConfigTest.java
create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessorTest.java
create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/RequestHandlerTest.java
create mode 100644 fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java
create mode 100644 fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCFprTransferHelper.java
create mode 100644 fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommandTest.java
diff --git a/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/aviator/resource-config.json b/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/aviator/resource-config.json
index 5d73d4c3157..9db67cb8605 100644
--- a/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/aviator/resource-config.json
+++ b/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/aviator/resource-config.json
@@ -10,6 +10,9 @@
{
"pattern": "default_tag_mapping.yaml"
},
+ {
+ "pattern": "default_dast_tag_mapping.yaml"
+ },
{
"pattern": "remediations.xsd"
},
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/config/AviatorConfigManager.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/config/AviatorConfigManager.java
index 6787f8a4d0f..236a42b18f4 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/config/AviatorConfigManager.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/_common/config/AviatorConfigManager.java
@@ -28,6 +28,7 @@ public class AviatorConfigManager {
private static final String EXTENSIONS_CONFIG_RESOURCE = "extensions_config.yaml";
private static final String LANGUAGES_COMMENT_CONFIG_RESOURCE = "languages_comment_config.yaml";
private static final String DEFAULT_TAG_MAPPING_RESOURCE = "default_tag_mapping.yaml";
+ private static final String DEFAULT_DAST_TAG_MAPPING_RESOURCE = "default_dast_tag_mapping.yaml";
private static volatile AviatorConfigManager instance;
private static final Object lock = new Object();
@@ -35,12 +36,14 @@ public class AviatorConfigManager {
private final ExtensionsConfig extensionsConfig;
private final LanguagesCommentConfig languagesCommentConfig;
private final TagMappingConfig defaultTagMappingConfig;
+ private final TagMappingConfig defaultDastTagMappingConfig;
private AviatorConfigManager() {
LOG.debug("Initializing AviatorConfigManager...");
this.extensionsConfig = ResourceUtil.loadYamlResource(EXTENSIONS_CONFIG_RESOURCE, ExtensionsConfig.class);
this.languagesCommentConfig = ResourceUtil.loadYamlResource(LANGUAGES_COMMENT_CONFIG_RESOURCE, LanguagesCommentConfig.class);
this.defaultTagMappingConfig = ResourceUtil.loadYamlResource(DEFAULT_TAG_MAPPING_RESOURCE, TagMappingConfig.class);
+ this.defaultDastTagMappingConfig = ResourceUtil.loadYamlResource(DEFAULT_DAST_TAG_MAPPING_RESOURCE, TagMappingConfig.class);
if (this.extensionsConfig != null) {
FileTypeLanguageMapperUtil.initializeConfig(this.extensionsConfig);
@@ -91,4 +94,12 @@ public TagMappingConfig getDefaultTagMappingConfig() {
}
return defaultTagMappingConfig;
}
+
+ public TagMappingConfig getDefaultDastTagMappingConfig() {
+ if (defaultDastTagMappingConfig == null) {
+ LOG.error("DefaultDastTagMappingConfig was not loaded. This indicates a bug.");
+ throw new AviatorBugException("Critical: DefaultDastTagMappingConfig not loaded.");
+ }
+ return defaultDastTagMappingConfig;
+ }
}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditDecisionMapper.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditDecisionMapper.java
new file mode 100644
index 00000000000..60ddfb92b13
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditDecisionMapper.java
@@ -0,0 +1,73 @@
+/*
+ * 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.audit;
+
+import com.fortify.cli.aviator.audit.model.AuditResponse;
+import com.fortify.cli.aviator.audit.model.AuditResult;
+import com.fortify.cli.aviator.grpc.DastAuditResult;
+import com.fortify.cli.aviator.util.Constants;
+
+/**
+ * Converts structured DAST decisions to conservative FCLI audit results.
+ */
+public final class DastAuditDecisionMapper {
+ private DastAuditDecisionMapper() {}
+
+ public static AuditResponse toAuditResponse(DastAuditResult result) {
+ if (!(result instanceof DastAuditResult.Success success)) {
+ return AuditResponse.builder()
+ .issueId(result.issueId())
+ .status(result.status())
+ .statusMessage(result.statusMessage())
+ .build();
+ }
+
+ String confidence = normalizedConfidence(success.confidence());
+ String tagValue;
+ String prediction;
+ String tier;
+ if (success.truePositive()) {
+ tagValue = Constants.EXPLOITABLE;
+ prediction = Constants.AVIATOR_REMEDIATION_REQUIRED;
+ tier = "GOLD";
+ } else if ("HIGH".equals(confidence)) {
+ tagValue = Constants.NOT_AN_ISSUE;
+ prediction = Constants.AVIATOR_NOT_AN_ISSUE;
+ tier = "GOLD";
+ } else {
+ tagValue = Constants.NOT_AN_ISSUE;
+ prediction = Constants.AVIATOR_LIKELY_FP;
+ tier = "SILVER";
+ }
+
+ String comment = success.finalComment() != null && !success.finalComment().isBlank()
+ ? success.finalComment()
+ : success.reasoning();
+ return AuditResponse.builder()
+ .issueId(success.issueId())
+ .status("SUCCESS")
+ .tier(tier)
+ .aviatorPredictionTag(prediction)
+ .isAviatorProcessed(true)
+ .auditResult(AuditResult.builder().tagValue(tagValue).comment(comment).build())
+ .build();
+ }
+
+ private static String normalizedConfidence(String confidence) {
+ if (confidence == null) return "LOW";
+ return switch (confidence.toUpperCase(java.util.Locale.ROOT)) {
+ case "HIGH", "MEDIUM", "LOW" -> confidence.toUpperCase(java.util.Locale.ROOT);
+ default -> "LOW";
+ };
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFPR.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFPR.java
new file mode 100644
index 00000000000..69dcbe49e38
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFPR.java
@@ -0,0 +1,195 @@
+/*
+ * 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.audit;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fortify.cli.aviator.audit.model.AuditResponse;
+import com.fortify.cli.aviator.config.TagMappingConfig;
+import com.fortify.cli.aviator.dast.StreamingWebInspectParser;
+import com.fortify.cli.aviator.fpr.model.AuditIssue;
+import com.fortify.cli.aviator.fpr.processor.AuditProcessor;
+import com.fortify.cli.aviator.grpc.DastAuditStreamConfig;
+import com.fortify.cli.aviator.grpc.DastAuditStreamResult;
+import com.fortify.cli.aviator.grpc.DastAuditWorkItem;
+import com.fortify.cli.aviator.util.Constants;
+import com.fortify.cli.aviator.util.FprHandle;
+
+/**
+ * Coordinates parsing, filtering, server auditing, and DAST audit.xml updates.
+ */
+public final class DastAuditFPR {
+ private static final Logger LOG = LoggerFactory.getLogger(DastAuditFPR.class);
+
+ private DastAuditFPR() {}
+
+ private record EligibilityResult(
+ List workItems,
+ int missingId,
+ int duplicate,
+ int suppressed,
+ int processed) {}
+
+ @FunctionalInterface
+ public interface StreamRunner {
+ CompletableFuture run(
+ DastAuditStreamConfig config, List workItems, int totalReportedIssues);
+ }
+
+ public static DastAuditFprResult audit(
+ FprHandle fprHandle,
+ DastAuditStreamConfig config,
+ TagMappingConfig tagMappingConfig,
+ StreamRunner streamRunner) {
+ tagMappingConfig.validateForDast();
+ var auditProcessor = new AuditProcessor(fprHandle);
+ Map auditIssues = auditProcessor.processAuditXML();
+ var sessions = new StreamingWebInspectParser(fprHandle).parseSessions();
+ EligibilityResult eligibility = eligibleWorkItems(sessions, auditIssues);
+ List workItems = eligibility.workItems();
+ int totalReported = sessions.stream().mapToInt(session -> session.getIssues().size()).sum();
+ int locallySkipped = totalReported - workItems.size();
+ LOG.info("DAST audit eligibility: reported={}, eligible={}, skipped={} "
+ + "(missingId={}, duplicate={}, suppressed={}, alreadyProcessed={})",
+ totalReported, workItems.size(), locallySkipped, eligibility.missingId(),
+ eligibility.duplicate(), eligibility.suppressed(), eligibility.processed());
+
+ if (workItems.isEmpty()) {
+ LOG.info("DAST audit skipped because no eligible findings remain");
+ return emptyResult(totalReported, locallySkipped);
+ }
+
+ DastAuditStreamResult streamResult = streamRunner.run(config, workItems, totalReported).join();
+ Map successfulResponses = new LinkedHashMap<>();
+ int truePositives = 0;
+ int falsePositivesSuppressed = 0;
+ int likelyFalsePositives = 0;
+ int failed = 0;
+ int serverSkipped = 0;
+ Set respondedIssueIds = new java.util.HashSet<>();
+
+ for (var result : streamResult.results()) {
+ respondedIssueIds.add(result.issueId());
+ AuditResponse response = DastAuditDecisionMapper.toAuditResponse(result);
+ if ("SUCCESS".equalsIgnoreCase(response.getStatus()) && response.getAuditResult() != null) {
+ successfulResponses.put(result.issueId(), response);
+ var success = (com.fortify.cli.aviator.grpc.DastAuditResult.Success) result;
+ LOG.debug("DAST issue {} audited successfully: confidence={}, tier={}, result={}",
+ result.issueId(), success.confidence(), response.getTier(), response.getAuditResult().getTagValue());
+ if (Constants.EXPLOITABLE.equals(response.getAuditResult().getTagValue())) {
+ truePositives++;
+ } else if (isSuppressedFalsePositive(response, tagMappingConfig)) {
+ falsePositivesSuppressed++;
+ } else {
+ likelyFalsePositives++;
+ }
+ } else if ("SKIPPED".equalsIgnoreCase(result.status())) {
+ serverSkipped++;
+ LOG.debug("DAST issue {} skipped by server: statusMessage={}",
+ result.issueId(), result.statusMessage());
+ } else {
+ failed++;
+ LOG.warn("DAST issue {} failed: status={}, statusMessage={}",
+ result.issueId(), result.status(), result.statusMessage());
+ }
+ }
+ int missingResponses = 0;
+ for (DastAuditWorkItem workItem : workItems) {
+ if (!respondedIssueIds.contains(workItem.issue().getId())) {
+ missingResponses++;
+ LOG.warn("DAST issue {} received no terminal server response", workItem.issue().getId());
+ }
+ }
+ failed += missingResponses;
+
+ var updatedFile = successfulResponses.isEmpty()
+ ? null
+ : auditProcessor.updateAndSaveDastAuditXml(successfulResponses, tagMappingConfig);
+ int succeeded = successfulResponses.size();
+ LOG.info("DAST audit responses: submitted={}, succeeded={}, serverSkipped={}, failed={}, missingResponses={}",
+ workItems.size(), succeeded, serverSkipped, failed, missingResponses);
+ String status = succeeded == workItems.size() ? "AUDITED"
+ : succeeded > 0 ? "PARTIALLY_AUDITED" : "FAILED";
+ String message = succeeded == 0 ? "No DAST audit responses were successfully processed" : null;
+ return new DastAuditFprResult(
+ updatedFile, status, message, totalReported, workItems.size(), workItems.size(), succeeded,
+ truePositives, falsePositivesSuppressed, likelyFalsePositives,
+ locallySkipped + serverSkipped, failed,
+ streamResult.reservedQuota(), streamResult.exceededCount(), streamResult.unlimitedQuota(),
+ streamResult.quotaLastUpdated(), streamResult.nextQuotaUpdateMessage());
+ }
+
+ private static boolean isSuppressedFalsePositive(AuditResponse response, TagMappingConfig tagMappingConfig) {
+ boolean tierOne = "GOLD".equalsIgnoreCase(response.getTier());
+ return Boolean.TRUE.equals(tagMappingConfig.getResult(
+ tierOne, TagMappingConfig.ResultType.FP).getSuppress());
+ }
+
+ private static EligibilityResult eligibleWorkItems(
+ List sessions,
+ Map auditIssues) {
+ var workItems = new ArrayList();
+ var seenIssueIds = new java.util.HashSet();
+ int missingId = 0;
+ int duplicate = 0;
+ int suppressed = 0;
+ int processed = 0;
+ for (var session : sessions) {
+ for (var issue : session.getIssues()) {
+ String issueId = issue.getId();
+ if (issueId == null || issueId.isBlank()) {
+ missingId++;
+ LOG.debug("Skipping DAST finding without an issue ID in session {}", session.getRequestId());
+ continue;
+ }
+ if (!seenIssueIds.add(issueId)) {
+ duplicate++;
+ LOG.debug("Skipping duplicate DAST issue {} in session {}", issueId, session.getRequestId());
+ continue;
+ }
+ AuditIssue auditIssue = auditIssues.get(issueId);
+ if (auditIssue != null && auditIssue.isSuppressed()) {
+ suppressed++;
+ LOG.debug("Skipping DAST issue {} because it is already suppressed", issueId);
+ continue;
+ }
+ if (auditIssue != null && isProcessedByAviator(auditIssue)) {
+ processed++;
+ LOG.debug("Skipping DAST issue {} because it is already processed by Aviator", issueId);
+ continue;
+ }
+ workItems.add(new DastAuditWorkItem(session, issue));
+ }
+ }
+ return new EligibilityResult(List.copyOf(workItems), missingId, duplicate, suppressed, processed);
+ }
+
+ private static boolean isProcessedByAviator(AuditIssue auditIssue) {
+ return Constants.PROCESSED_BY_AVIATOR.equalsIgnoreCase(
+ auditIssue.getTags().get(Constants.AVIATOR_STATUS_TAG_ID));
+ }
+
+ private static DastAuditFprResult emptyResult(int totalReported, int skipped) {
+ return new DastAuditFprResult(
+ null, "SKIPPED", "No eligible DAST findings to audit", totalReported, 0, 0, 0,
+ 0, 0, 0, skipped, 0, 0, 0, false, null, null);
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFprResult.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFprResult.java
new file mode 100644
index 00000000000..5f7ab46709b
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFprResult.java
@@ -0,0 +1,38 @@
+/*
+ * 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.audit;
+
+import java.io.File;
+
+/**
+ * Summary of processing one DAST FPR.
+ */
+public record DastAuditFprResult(
+ File updatedFile,
+ String status,
+ String message,
+ int totalReported,
+ int eligible,
+ int submitted,
+ int succeeded,
+ int truePositives,
+ int falsePositivesSuppressed,
+ int likelyFalsePositives,
+ int skipped,
+ int failed,
+ int reservedQuota,
+ int exceededCount,
+ boolean unlimitedQuota,
+ String quotaLastUpdated,
+ String nextQuotaUpdateMessage
+) {}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/config/TagMappingConfig.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/config/TagMappingConfig.java
index 2f21bf4c364..b6b34b329a9 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/config/TagMappingConfig.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/config/TagMappingConfig.java
@@ -61,6 +61,14 @@ public void validate() {
}
}
+ public void validateForDast() {
+ validate();
+ if (hasSuppressionExclusions()) {
+ throw new AviatorSimpleException(
+ "Invalid DAST tag mapping configuration: suppression_exclusions are not supported");
+ }
+ }
+
public boolean hasSuppressionExclusions() {
return suppression_exclusions != null
&& suppression_exclusions.stream()
@@ -84,6 +92,40 @@ public boolean isSuppressionExcluded(SuppressionExclusionContext context) {
.anyMatch(exclusion -> exclusion.matches(context));
}
+ public Result getResult(boolean tierOne, ResultType resultType) {
+ Tier tier = tierOne ? mapping.getTier_1() : mapping.getTier_2();
+ return switch (resultType) {
+ case FP -> tier.getFp();
+ case TP -> tier.getTp();
+ case UNSURE -> tier.getUnsure();
+ };
+ }
+
+ public Set getMappedValues() {
+ if (mapping == null) {
+ return Collections.emptySet();
+ }
+ LinkedHashSet values = new LinkedHashSet<>();
+ addMappedValues(values, mapping.getTier_1());
+ addMappedValues(values, mapping.getTier_2());
+ return Collections.unmodifiableSet(values);
+ }
+
+ private void addMappedValues(Set values, Tier tier) {
+ if (tier == null) {
+ return;
+ }
+ addMappedValue(values, tier.getFp());
+ addMappedValue(values, tier.getTp());
+ addMappedValue(values, tier.getUnsure());
+ }
+
+ private void addMappedValue(Set values, Result result) {
+ if (result != null && result.getValue() != null && !result.getValue().isBlank()) {
+ values.add(result.getValue());
+ }
+ }
+
private void validateTier(List errors, String path, Tier tier) {
validateRequired(errors, path, tier);
if (tier != null) {
@@ -259,6 +301,12 @@ public static class Mapping {
private Tier tier_2;
}
+ public enum ResultType {
+ FP,
+ TP,
+ UNSURE
+ }
+
@Data @Reflectable
public static class Tier {
private Result fp;
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/DastIssue.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/DastIssue.java
index cdb564c258f..5b174eb209f 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/DastIssue.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/DastIssue.java
@@ -13,8 +13,8 @@
package com.fortify.cli.aviator.dast;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -37,6 +37,7 @@ public class DastIssue {
private String cweDescription; // Full CWE description text
private String sessionUrl; // URL of the session containing this issue
private List reproStepUrls = new ArrayList<>();
+ private List reproSteps = new ArrayList<>();
// ReportSections for audit context
private String summary; // Summary from ReportSection
@@ -46,7 +47,7 @@ public class DastIssue {
private String referenceInfo; // Reference Info from ReportSection
// Additional classifications
- private Map classifications = new HashMap<>(); // kind -> value
+ private Map classifications = new LinkedHashMap<>(); // kind -> value
// Audit status
private boolean suppressed = false;
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/DastReproStep.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/DastReproStep.java
new file mode 100644
index 00000000000..bcf4898d25a
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/DastReproStep.java
@@ -0,0 +1,25 @@
+/*
+ * 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.dast;
+
+import lombok.Data;
+
+/**
+ * Represents one ordered navigation or attack step reported by WebInspect.
+ */
+@Data
+public class DastReproStep {
+ private String source;
+ private String url;
+ private String postParams;
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/StreamingWebInspectParser.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/StreamingWebInspectParser.java
index c3fd3994dd2..f6ac5ec2cf3 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/StreamingWebInspectParser.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/dast/StreamingWebInspectParser.java
@@ -32,6 +32,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.fortify.cli.aviator._common.exception.AviatorTechnicalException;
import com.fortify.cli.aviator.util.FprHandle;
/**
@@ -64,7 +65,7 @@ public List parse() {
Path webInspectPath = fprHandle.getPath("/webinspect.xml");
if (!Files.exists(webInspectPath)) {
- throw new RuntimeException("webinspect.xml not found in DAST FPR");
+ throw new AviatorTechnicalException("webinspect.xml not found in DAST FPR");
}
try (InputStream inputStream = Files.newInputStream(webInspectPath)) {
@@ -86,9 +87,9 @@ public List parse() {
logger.info("Parsed {} DAST issues from webinspect.xml (streaming)", issues.size());
} catch (XMLStreamException e) {
- throw new RuntimeException("Failed to parse webinspect.xml: " + e.getMessage(), e);
+ throw new AviatorTechnicalException("Failed to parse webinspect.xml", e);
} catch (IOException e) {
- throw new RuntimeException("Failed to read webinspect.xml: " + e.getMessage(), e);
+ throw new AviatorTechnicalException("Failed to read webinspect.xml", e);
}
return issues;
@@ -106,7 +107,7 @@ public List parseSessions() {
Path webInspectPath = fprHandle.getPath("/webinspect.xml");
if (!Files.exists(webInspectPath)) {
- throw new RuntimeException("webinspect.xml not found in DAST FPR");
+ throw new AviatorTechnicalException("webinspect.xml not found in DAST FPR");
}
try (InputStream inputStream = Files.newInputStream(webInspectPath)) {
@@ -133,9 +134,9 @@ public List parseSessions() {
sessions.size(), totalIssues);
} catch (XMLStreamException e) {
- throw new RuntimeException("Failed to parse webinspect.xml: " + e.getMessage(), e);
+ throw new AviatorTechnicalException("Failed to parse webinspect.xml", e);
} catch (IOException e) {
- throw new RuntimeException("Failed to read webinspect.xml: " + e.getMessage(), e);
+ throw new AviatorTechnicalException("Failed to read webinspect.xml", e);
}
return sessions;
@@ -429,12 +430,12 @@ private void parseReproSteps(XMLStreamReader reader, DastIssue issue)
while (reader.hasNext()) {
int event = reader.next();
- if (event == XMLStreamConstants.START_ELEMENT) {
- if ("Url".equals(reader.getLocalName())) {
- String url = readElementText(reader);
- if (url != null && !url.isEmpty()) {
- issue.getReproStepUrls().add(url);
- }
+ if (event == XMLStreamConstants.START_ELEMENT
+ && "ReproStep".equals(reader.getLocalName())) {
+ DastReproStep step = parseReproStep(reader);
+ if (step.getUrl() != null && !step.getUrl().isEmpty()) {
+ issue.getReproStepUrls().add(step.getUrl());
+ issue.getReproSteps().add(step);
}
} else if (event == XMLStreamConstants.END_ELEMENT
&& "ReproSteps".equals(reader.getLocalName())) {
@@ -443,6 +444,25 @@ private void parseReproSteps(XMLStreamReader reader, DastIssue issue)
}
}
+ private DastReproStep parseReproStep(XMLStreamReader reader) throws XMLStreamException {
+ var step = new DastReproStep();
+ while (reader.hasNext()) {
+ int event = reader.next();
+ if (event == XMLStreamConstants.START_ELEMENT) {
+ switch (reader.getLocalName()) {
+ case "Source" -> step.setSource(readElementText(reader));
+ case "Url" -> step.setUrl(readElementText(reader));
+ case "PostParams" -> step.setPostParams(readElementText(reader));
+ default -> { }
+ }
+ } else if (event == XMLStreamConstants.END_ELEMENT
+ && "ReproStep".equals(reader.getLocalName())) {
+ return step;
+ }
+ }
+ return step;
+ }
+
// =========================================================================
// ReportSection parsing
// =========================================================================
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuditProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuditProcessor.java
index 602367fbf03..dcf29e450b2 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuditProcessor.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/AuditProcessor.java
@@ -763,6 +763,96 @@ public File updateAndSaveAuditAndRemediationsXml(Map audi
return fprHandle.getFprPath().toFile();
}
+ /**
+ * Applies DAST audit decisions to audit.xml without creating SAST remediation artifacts.
+ */
+ public File updateAndSaveDastAuditXml(Map auditResponses,
+ TagMappingConfig tagMappingConfig) {
+ Set writtenInstanceIds = new HashSet<>();
+ for (Map.Entry entry : auditResponses.entrySet()) {
+ AuditResponse response = entry.getValue();
+ if (response == null || !"SUCCESS".equalsIgnoreCase(response.getStatus())
+ || response.getAuditResult() == null) {
+ continue;
+ }
+ Element issueElement = findIssueElement(entry.getKey());
+ if (issueElement == null) {
+ issueElement = createDastIssueElement(entry.getKey());
+ } else {
+ int revision = Optional.ofNullable(issueElement.getAttribute("revision"))
+ .filter(value -> !value.isBlank())
+ .map(value -> {
+ try { return Integer.parseInt(value); } catch (NumberFormatException e) { return 0; }
+ })
+ .orElse(0);
+ issueElement.setAttribute("revision", String.valueOf(revision + 1));
+ }
+ applyDastAuditResponse(issueElement, response, tagMappingConfig);
+ writtenInstanceIds.add(entry.getKey());
+ }
+
+ AuditXmlIssuePruner.retainOnly(auditDoc, writtenInstanceIds);
+
+ try (OutputStream os = Files.newOutputStream(fprHandle.getPath("/audit.xml"))) {
+ transformDomToStream(auditDoc, os);
+ } catch (Exception e) {
+ throw new AviatorTechnicalException("Failed to write DAST audit data back into the FPR file", e);
+ }
+ return fprHandle.getFprPath().toFile();
+ }
+
+ private Element createDastIssueElement(String instanceId) {
+ Element issueList = (Element) auditDoc.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "IssueList").item(0);
+ if (issueList == null) {
+ issueList = auditDoc.createElementNS(AUDIT_NAMESPACE_URI, "IssueList");
+ auditDoc.getDocumentElement().appendChild(issueList);
+ }
+ Element issueElement = auditDoc.createElementNS(AUDIT_NAMESPACE_URI, "Issue");
+ issueElement.setAttribute("instanceId", instanceId);
+ issueElement.setAttribute("revision", "0");
+ issueElement.setAttribute("suppressed", "false");
+ issueList.appendChild(issueElement);
+ return issueElement;
+ }
+
+ private void applyDastAuditResponse(Element issueElement, AuditResponse response,
+ TagMappingConfig tagMappingConfig) {
+ String prediction = response.getAviatorPredictionTag();
+ updateOrAddTag(issueElement, Constants.AVIATOR_PREDICTION_TAG_ID, prediction);
+ TagMappingConfig.Result resultConfig = getDastResultConfig(response, tagMappingConfig);
+ if (resultConfig.getValue() != null && !resultConfig.getValue().isBlank()) {
+ updateOrAddTag(issueElement, tagMappingConfig.getTag_id(), resultConfig.getValue());
+ }
+ Boolean suppressedHistoryValue = updateSuppressedState(
+ issueElement, Boolean.TRUE.equals(resultConfig.getSuppress()));
+ updateOrAddTag(issueElement, Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR);
+ if (response.getAuditResult().getComment() != null) {
+ updateOrAddComment(issueElement, response.getAuditResult().getComment());
+ }
+ Element clientAuditTrail = getClientAuditTrailElement(issueElement);
+ addTagHistory(clientAuditTrail, Constants.AVIATOR_PREDICTION_TAG_ID, prediction);
+ if (resultConfig.getValue() != null && !resultConfig.getValue().isBlank()) {
+ addTagHistory(clientAuditTrail, tagMappingConfig.getTag_id(), resultConfig.getValue());
+ }
+ addTagHistory(clientAuditTrail, Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR);
+ if (suppressedHistoryValue != null) {
+ addTagHistory(clientAuditTrail, Constants.SUPPRESSED_TAG_ID, suppressedHistoryValue.toString());
+ }
+ }
+
+ private TagMappingConfig.Result getDastResultConfig(AuditResponse response,
+ TagMappingConfig tagMappingConfig) {
+ boolean tierOne = "GOLD".equalsIgnoreCase(response.getTier());
+ String tagValue = response.getAuditResult().getTagValue();
+ if (Constants.NOT_AN_ISSUE.equalsIgnoreCase(tagValue)) {
+ return tagMappingConfig.getResult(tierOne, TagMappingConfig.ResultType.FP);
+ }
+ if (Constants.EXPLOITABLE.equalsIgnoreCase(tagValue)) {
+ return tagMappingConfig.getResult(tierOne, TagMappingConfig.ResultType.TP);
+ }
+ return tagMappingConfig.getResult(tierOne, TagMappingConfig.ResultType.UNSURE);
+ }
+
private Document generateRemediationsXml(Map auditResponses,
Map remediationCommentTimestamps,
FPRInfo fprInfo) throws AviatorTechnicalException {
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java
index 4c5c9791a70..4f356cea80d 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java
@@ -37,6 +37,7 @@
import com.fortify.aviator.application.GetDefaultQuotaResponse;
import com.fortify.aviator.application.UpdateApplicationRequest;
import com.fortify.aviator.application.ValidateAdminSessionRequest;
+import com.fortify.aviator.dastaudit.DastAuditServiceGrpc;
import com.fortify.aviator.dastentitlement.DastEntitlement;
import com.fortify.aviator.dastentitlement.DastEntitlementServiceGrpc;
import com.fortify.aviator.dastentitlement.ListDastEntitlementsByTenantRequest;
@@ -84,6 +85,7 @@ public class AviatorGrpcClient implements AutoCloseable {
private final EntitlementServiceGrpc.EntitlementServiceBlockingStub entitlementServiceBlockingStub;
private final DastEntitlementServiceGrpc.DastEntitlementServiceBlockingStub dastEntitlementServiceBlockingStub;
private final CorrelationServiceGrpc.CorrelationServiceStub correlationAsyncStub;
+ private final DastAuditServiceGrpc.DastAuditServiceStub dastAuditAsyncStub;
private final long defaultTimeoutSeconds;
private final java.util.concurrent.ExecutorService processingExecutor;
private final long pingIntervalSeconds;
@@ -100,6 +102,11 @@ public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAv
this.entitlementServiceBlockingStub = EntitlementServiceGrpc.newBlockingStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
this.dastEntitlementServiceBlockingStub = DastEntitlementServiceGrpc.newBlockingStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
this.correlationAsyncStub = CorrelationServiceGrpc.newStub(channel).withCompression("gzip").withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE).withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE).withWaitForReady();
+ this.dastAuditAsyncStub = DastAuditServiceGrpc.newStub(channel)
+ .withCompression("gzip")
+ .withMaxInboundMessageSize(Constants.MAX_MESSAGE_SIZE)
+ .withMaxOutboundMessageSize(Constants.MAX_MESSAGE_SIZE)
+ .withWaitForReady();
this.defaultTimeoutSeconds = defaultTimeoutSeconds;
this.processingExecutor = Executors.newFixedThreadPool(4, r -> {
Thread t = new Thread(r, "aviator-client-processing-" + r.hashCode());
@@ -297,6 +304,10 @@ public CorrelationServiceGrpc.CorrelationServiceStub getCorrelationAsyncStub() {
return correlationAsyncStub;
}
+ public DastAuditServiceGrpc.DastAuditServiceStub getDastAuditAsyncStub() {
+ return dastAuditAsyncStub;
+ }
+
public java.util.concurrent.ScheduledExecutorService getPingScheduler() {
return pingScheduler;
}
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditRequestMapper.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditRequestMapper.java
new file mode 100644
index 00000000000..56ca5765eb1
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditRequestMapper.java
@@ -0,0 +1,66 @@
+/*
+ * 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.grpc;
+
+import com.fortify.aviator.dastaudit.DastClassification;
+import com.fortify.aviator.dastaudit.DastFindingContext;
+import com.fortify.aviator.dastaudit.DastReproStep;
+import com.fortify.cli.aviator.dast.DastIssue;
+import com.fortify.cli.aviator.dast.DastSession;
+
+/**
+ * Maps parsed WebInspect data to the DAST audit wire contract.
+ */
+public final class DastAuditRequestMapper {
+ private DastAuditRequestMapper() {}
+
+ public static DastFindingContext toFindingContext(DastSession session, DastIssue issue) {
+ var builder = DastFindingContext.newBuilder()
+ .setIssueId(value(issue.getId()))
+ .setCheckTypeId(value(issue.getCheckTypeId()))
+ .setEngineType(value(issue.getEngineType()))
+ .setVulnerabilityId(value(issue.getVulnerabilityId()))
+ .setSeverity(issue.getSeverity())
+ .setName(value(issue.getName()))
+ .setCategory(value(issue.getCategory()))
+ .setCweId(value(issue.getCweId()))
+ .setCweDescription(value(issue.getCweDescription()))
+ .setSessionUrl(value(session.getUrl() != null ? session.getUrl() : issue.getSessionUrl()))
+ .setSummary(value(issue.getSummary()))
+ .setImplication(value(issue.getImplication()))
+ .setExecution(value(issue.getExecution()))
+ .setFix(value(issue.getFix()))
+ .setReferenceInfo(value(issue.getReferenceInfo()))
+ .setRequestId(value(session.getRequestId()))
+ .setScheme(value(session.getScheme()))
+ .setHost(value(session.getHost()))
+ .setPort(session.getPort())
+ .setAttackParamDescriptor(value(session.getAttackParamDescriptor()))
+ .setRawRequest(value(session.getRawRequest()))
+ .setRawResponse(value(session.getRawResponse()));
+
+ issue.getClassifications().forEach((kind, classificationValue) -> builder.addClassifications(
+ DastClassification.newBuilder().setKind(value(kind)).setValue(value(classificationValue)).build()));
+ builder.addAllReproStepUrls(issue.getReproStepUrls());
+ issue.getReproSteps().forEach(step -> builder.addReproSteps(DastReproStep.newBuilder()
+ .setSource(value(step.getSource()))
+ .setUrl(value(step.getUrl()))
+ .setPostParams(value(step.getPostParams()))
+ .build()));
+ return builder.build();
+ }
+
+ private static String value(String value) {
+ return value != null ? value : "";
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditResponseMapper.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditResponseMapper.java
new file mode 100644
index 00000000000..3cd45debf81
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditResponseMapper.java
@@ -0,0 +1,50 @@
+/*
+ * 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.grpc;
+
+import com.fortify.aviator.dastaudit.DastAuditResponse;
+
+/**
+ * Maps a DAST audit response to the issue associated with its request ID.
+ */
+final class DastAuditResponseMapper {
+ private DastAuditResponseMapper() {}
+
+ static DastAuditResult map(DastAuditResponse response, String expectedIssueId) {
+ String responseIssueId = response.getDastIssueId();
+ if (!responseIssueId.isBlank() && !expectedIssueId.equals(responseIssueId)) {
+ return new DastAuditResult.Failure(
+ expectedIssueId, "FAILED",
+ "DAST audit response issue ID mismatch: expected '" + expectedIssueId
+ + "' but received '" + responseIssueId + "'");
+ }
+
+ if ("SKIPPED".equalsIgnoreCase(response.getStatus())) {
+ return new DastAuditResult.Skipped(expectedIssueId, response.getStatusMessage());
+ }
+ if (!"SUCCESS".equalsIgnoreCase(response.getStatus())) {
+ return new DastAuditResult.Failure(
+ expectedIssueId, response.getStatus(), response.getStatusMessage());
+ }
+ if (!response.hasDecision()) {
+ return new DastAuditResult.Failure(
+ expectedIssueId, "FAILED", "Successful DAST audit response did not contain a decision");
+ }
+
+ var decision = response.getDecision();
+ return new DastAuditResult.Success(
+ expectedIssueId, decision.getTruePositive(), decision.getConfidence(),
+ decision.getReasoning(), decision.getRemediationAdvice(), decision.getFinalComment(),
+ decision.getTagValue(), decision.getTier());
+ }
+}
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditResult.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditResult.java
new file mode 100644
index 00000000000..b4f99f89833
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditResult.java
@@ -0,0 +1,50 @@
+/*
+ * 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.grpc;
+
+/** Domain representation of one terminal DAST audit response. */
+public sealed interface DastAuditResult permits DastAuditResult.Success, DastAuditResult.Skipped, DastAuditResult.Failure {
+ String issueId();
+ String status();
+ String statusMessage();
+
+ record Success(
+ String issueId,
+ boolean truePositive,
+ String confidence,
+ String reasoning,
+ String remediationAdvice,
+ String finalComment,
+ String tagValue,
+ String tier
+ ) implements DastAuditResult {
+ @Override
+ public String status() {
+ return "SUCCESS";
+ }
+
+ @Override
+ public String statusMessage() {
+ return "";
+ }
+ }
+
+ record Skipped(String issueId, String statusMessage) implements DastAuditResult {
+ @Override
+ public String status() {
+ return "SKIPPED";
+ }
+ }
+
+ record Failure(String issueId, String status, String statusMessage) implements DastAuditResult {}
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamConfig.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamConfig.java
new file mode 100644
index 00000000000..031897beb02
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamConfig.java
@@ -0,0 +1,35 @@
+/*
+ * 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.grpc;
+
+import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
+
+/**
+ * Configuration for one DAST audit gRPC stream.
+ */
+public record DastAuditStreamConfig(
+ String token,
+ String applicationName,
+ String sscApplicationName,
+ String sscApplicationVersion,
+ String fprBuildId
+) {
+ public DastAuditStreamConfig {
+ if (token == null || token.isBlank()) {
+ throw new AviatorSimpleException("Aviator token must be specified for DAST audit");
+ }
+ if (applicationName == null || applicationName.isBlank()) {
+ throw new AviatorSimpleException("Aviator application name must be specified for DAST audit");
+ }
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java
new file mode 100644
index 00000000000..7d77ed384a8
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessor.java
@@ -0,0 +1,480 @@
+/*
+ * 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.grpc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fortify.aviator.dastaudit.DastAuditClientMessage;
+import com.fortify.aviator.dastaudit.DastAuditErrorResponse;
+import com.fortify.aviator.dastaudit.DastAuditInitResponse;
+import com.fortify.aviator.dastaudit.DastAuditPingRequest;
+import com.fortify.aviator.dastaudit.DastAuditPongResponse;
+import com.fortify.aviator.dastaudit.DastAuditRequest;
+import com.fortify.aviator.dastaudit.DastAuditResponse;
+import com.fortify.aviator.dastaudit.DastAuditServerMessage;
+import com.fortify.aviator.dastaudit.DastAuditServiceGrpc;
+import com.fortify.aviator.dastaudit.DastAuditStreamInitRequest;
+import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
+import com.fortify.cli.aviator._common.exception.AviatorTechnicalException;
+import com.fortify.cli.aviator.config.IAviatorLogger;
+import com.fortify.cli.aviator.util.Constants;
+
+import io.grpc.Status;
+import io.grpc.stub.ClientCallStreamObserver;
+import io.grpc.stub.ClientResponseObserver;
+import io.grpc.stub.StreamObserver;
+
+/**
+ * Processes DAST findings through the Aviator DAST audit bidirectional stream.
+ */
+public class DastAuditStreamProcessor implements AutoCloseable {
+ private static final Logger LOG = LoggerFactory.getLogger(DastAuditStreamProcessor.class);
+
+ private final IAviatorLogger logger;
+ private final DastAuditServiceGrpc.DastAuditServiceStub asyncStub;
+ private final ScheduledExecutorService pingScheduler;
+ private final long pingIntervalSeconds;
+ private final AtomicBoolean isPinging = new AtomicBoolean(false);
+ private final AtomicBoolean isClosed = new AtomicBoolean(false);
+ private final AtomicBoolean isRpcCompleted = new AtomicBoolean(false);
+ private final AtomicBoolean isRetryScheduled = new AtomicBoolean(false);
+ private final AtomicInteger streamRetryCount = new AtomicInteger();
+ private final AtomicInteger stagnantRetryCount = new AtomicInteger();
+ private final AtomicLong streamGeneration = new AtomicLong();
+
+ private RequestHandler requestHandler;
+ private ScheduledFuture> pingTask;
+ private ScheduledFuture> retryTask;
+ private ClientCallStreamObserver activeRequestStream;
+ private String streamId;
+ private DastAuditStreamConfig config;
+ private List workItems;
+ private int totalReportedIssues;
+ private int lastRetryCompletedCount;
+ private CompletableFuture resultFuture;
+ private final List results = java.util.Collections.synchronizedList(new ArrayList<>());
+ private final Map requestIssueIds = new ConcurrentHashMap<>();
+ private final Map requestIdsByIssue = new ConcurrentHashMap<>();
+ private final Set completedRequestIds = ConcurrentHashMap.newKeySet();
+ private final Set pendingIssueIds = ConcurrentHashMap.newKeySet();
+ private int reservedQuota;
+ private int exceededCount;
+ private boolean unlimitedQuota;
+ private boolean quotaMetadataInitialized;
+ private String quotaLastUpdated;
+ private String nextQuotaUpdateMessage;
+
+ public DastAuditStreamProcessor(
+ IAviatorLogger logger,
+ DastAuditServiceGrpc.DastAuditServiceStub asyncStub,
+ ScheduledExecutorService pingScheduler,
+ long pingIntervalSeconds) {
+ this.logger = logger;
+ this.asyncStub = asyncStub;
+ this.pingScheduler = pingScheduler;
+ this.pingIntervalSeconds = pingIntervalSeconds;
+ }
+
+ public CompletableFuture process(
+ DastAuditStreamConfig config,
+ List workItems,
+ int totalReportedIssues) {
+ this.config = config;
+ this.totalReportedIssues = totalReportedIssues;
+ this.resultFuture = new CompletableFuture<>();
+ this.requestIdsByIssue.clear();
+ this.completedRequestIds.clear();
+ this.results.clear();
+ this.streamRetryCount.set(0);
+ this.stagnantRetryCount.set(0);
+ this.lastRetryCompletedCount = 0;
+ this.isClosed.set(false);
+ this.quotaMetadataInitialized = false;
+ initializeWorkItems(workItems);
+ startStream();
+ return resultFuture;
+ }
+
+ void initializeWorkItems(List items) {
+ this.workItems = List.copyOf(items);
+ this.pendingIssueIds.clear();
+ items.forEach(item -> pendingIssueIds.add(item.issue().getId()));
+ }
+
+ private void startStream() {
+ if (resultFuture.isDone() || isClosed.get()) return;
+ this.streamId = UUID.randomUUID().toString();
+ this.requestIssueIds.clear();
+ this.requestHandler = new RequestHandler<>(streamId);
+ this.activeRequestStream = null;
+ this.isRpcCompleted.set(false);
+ this.isRetryScheduled.set(false);
+ long generation = streamGeneration.incrementAndGet();
+ LOG.debug("Starting DAST audit stream {} with {} pending findings and {} reported findings",
+ streamId, pendingIssueIds.size(), totalReportedIssues);
+
+ try {
+ StreamObserver requestObserver =
+ asyncStub.processDastAuditStream(new ResponseObserver(generation));
+ requestHandler.initialize(requestObserver);
+ startPingPong();
+ sendInit();
+ } catch (RuntimeException exception) {
+ handleStreamError(exception);
+ }
+ }
+
+ private void sendInit() {
+ var init = DastAuditStreamInitRequest.newBuilder()
+ .setToken(value(config.token()))
+ .setApplicationName(value(config.applicationName()))
+ .setStreamId(streamId)
+ .setRequestId(UUID.randomUUID().toString())
+ .setTotalReportedIssues(totalReportedIssues)
+ .setTotalIssuesToAudit(pendingIssueIds.size());
+ if (config.fprBuildId() != null) init.setFprBuildId(config.fprBuildId());
+ if (config.sscApplicationName() != null) init.setSscApplicationName(config.sscApplicationName());
+ if (config.sscApplicationVersion() != null) init.setSscApplicationVersion(config.sscApplicationVersion());
+ sendRequest(DastAuditClientMessage.newBuilder().setInit(init).build());
+ }
+
+ private void handleInit(DastAuditInitResponse response) {
+ if (!isSuccess(response.getStatus())) {
+ fail(new AviatorSimpleException("DAST audit initialization failed: " + response.getStatusMessage()));
+ return;
+ }
+ if (!quotaMetadataInitialized) {
+ reservedQuota = response.getReservedQuota();
+ exceededCount = response.getExceededCount();
+ unlimitedQuota = response.getUnlimitedQuota();
+ quotaLastUpdated = response.hasQuotaLastUpdated() ? response.getQuotaLastUpdated() : null;
+ nextQuotaUpdateMessage = response.hasNextQuotaUpdateMessage() ? response.getNextQuotaUpdateMessage() : null;
+ quotaMetadataInitialized = true;
+ }
+ List remainingWorkItems = remainingWorkItems();
+ logger.info("DAST audit stream initialized; submitting " + remainingWorkItems.size() + " findings");
+ for (DastAuditClientMessage request : prepareAuditRequests(remainingWorkItems, streamId)) {
+ sendRequest(request);
+ }
+ }
+
+ List prepareAuditRequests(List items, String requestStreamId) {
+ List requests = new ArrayList<>(items.size());
+ for (DastAuditWorkItem item : items) {
+ String issueId = item.issue().getId();
+ String requestId = requestIdsByIssue.computeIfAbsent(issueId, ignored -> UUID.randomUUID().toString());
+ var request = DastAuditRequest.newBuilder()
+ .setRequestId(requestId)
+ .setStreamId(requestStreamId)
+ .setFinding(DastAuditRequestMapper.toFindingContext(item.session(), item.issue()))
+ .build();
+ pendingIssueIds.add(issueId);
+ requestIssueIds.put(requestId, issueId);
+ LOG.debug("Submitting DAST issue {} with request {} on stream {}",
+ issueId, requestId, requestStreamId);
+ requests.add(DastAuditClientMessage.newBuilder().setAudit(request).build());
+ }
+ return List.copyOf(requests);
+ }
+
+ int pendingRequestCount() {
+ return pendingIssueIds.size();
+ }
+
+ List remainingWorkItems() {
+ return workItems.stream()
+ .filter(item -> pendingIssueIds.contains(item.issue().getId()))
+ .toList();
+ }
+
+ String completeRequest(String requestId) {
+ String issueId = requestIssueIds.remove(requestId);
+ if (issueId != null) {
+ pendingIssueIds.remove(issueId);
+ completedRequestIds.add(requestId);
+ }
+ return issueId;
+ }
+
+ private void handleAudit(DastAuditResponse response) {
+ String issueId = completeRequest(response.getRequestId());
+ if (issueId == null) {
+ LOG.warn("Ignoring DAST audit response for unknown or completed request {}", response.getRequestId());
+ return;
+ }
+ var decision = response.hasDecision() ? response.getDecision() : null;
+ LOG.debug("Received DAST audit response: issueId={}, requestId={}, status={}, confidence={}, tier={}, hasDecision={}",
+ issueId, response.getRequestId(), response.getStatus(),
+ decision != null ? decision.getConfidence() : null,
+ decision != null ? decision.getTier() : null, decision != null);
+ results.add(DastAuditResponseMapper.map(response, issueId));
+ logger.progress("Audited %d of %d DAST findings", results.size(), workItems.size());
+ completeRequestsIfDone();
+ }
+
+ private void handleError(DastAuditErrorResponse response) {
+ String issueId = completeRequest(response.getRequestId());
+ if (issueId != null) {
+ LOG.debug("Received DAST audit error response: issueId={}, requestId={}, status={}, statusMessage={}",
+ issueId, response.getRequestId(), response.getStatus(), response.getStatusMessage());
+ DastAuditResult result = "SKIPPED".equalsIgnoreCase(response.getStatus())
+ ? new DastAuditResult.Skipped(issueId, response.getStatusMessage())
+ : new DastAuditResult.Failure(issueId, response.getStatus(), response.getStatusMessage());
+ results.add(result);
+ logger.progress("Audited %d of %d DAST findings", results.size(), workItems.size());
+ completeRequestsIfDone();
+ } else {
+ if (completedRequestIds.contains(response.getRequestId())) {
+ LOG.debug("Ignoring duplicate DAST audit error for completed request {}", response.getRequestId());
+ return;
+ }
+ fail(new AviatorSimpleException("DAST audit error: " + response.getStatusMessage()));
+ }
+ }
+
+ private void handlePong(DastAuditPongResponse response) {
+ LOG.debug("DAST audit pong received in {} ms", System.currentTimeMillis() - response.getClientTimestamp());
+ }
+
+ private void completeRequestsIfDone() {
+ if (pendingIssueIds.isEmpty() && requestHandler != null && !requestHandler.isCompleted()) {
+ requestHandler.complete();
+ }
+ }
+
+ private class ResponseObserver implements ClientResponseObserver {
+ private final long generation;
+
+ private ResponseObserver(long generation) {
+ this.generation = generation;
+ }
+
+ private boolean isCurrent() {
+ return generation == streamGeneration.get();
+ }
+
+ @Override
+ public void beforeStart(ClientCallStreamObserver requestStream) {
+ if (isCurrent()) activeRequestStream = requestStream;
+ }
+
+ @Override
+ public void onNext(DastAuditServerMessage message) {
+ if (!isCurrent()) return;
+ switch (message.getResponseTypeCase()) {
+ case INIT -> handleInit(message.getInit());
+ case AUDIT -> handleAudit(message.getAudit());
+ case ERROR -> handleError(message.getError());
+ case PONG -> handlePong(message.getPong());
+ default -> LOG.warn("Unknown DAST audit response type: {}", message.getResponseTypeCase());
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ if (!isCurrent()) return;
+ isRpcCompleted.set(true);
+ handleStreamError(throwable);
+ }
+
+ @Override
+ public void onCompleted() {
+ if (!isCurrent()) return;
+ isRpcCompleted.set(true);
+ stopPingPong();
+ LOG.debug("DAST audit stream {} completed with {} terminal responses for {} submitted findings",
+ streamId, results.size(), workItems.size());
+ if (resultFuture.isDone()) return;
+ if (!pendingIssueIds.isEmpty()) {
+ handleStreamError(Status.UNAVAILABLE
+ .withDescription("DAST audit stream completed before all findings received terminal responses")
+ .asRuntimeException());
+ return;
+ }
+ completeSuccessfully();
+ }
+ }
+
+ private void startPingPong() {
+ if (pingScheduler == null || pingIntervalSeconds <= 0) return;
+ pingTask = pingScheduler.scheduleAtFixedRate(() -> {
+ if (isPinging.compareAndSet(false, true)) {
+ try {
+ if (requestHandler != null && requestHandler.isReady()) {
+ var ping = DastAuditPingRequest.newBuilder()
+ .setStreamId(streamId)
+ .setTimestamp(System.currentTimeMillis())
+ .build();
+ sendRequest(DastAuditClientMessage.newBuilder().setPing(ping).build());
+ }
+ } finally {
+ isPinging.set(false);
+ }
+ }
+ }, pingIntervalSeconds, pingIntervalSeconds, TimeUnit.SECONDS);
+ }
+
+ private void handleStreamError(Throwable throwable) {
+ stopPingPong();
+ if (resultFuture.isDone() || isClosed.get() || isRetryScheduled.get()) return;
+
+ if (pendingIssueIds.isEmpty()) {
+ completeSuccessfully();
+ return;
+ }
+
+ if (isRetryableError(throwable) && canRetry(throwable)) {
+ if (!isRetryScheduled.compareAndSet(false, true)) return;
+ int retryAttempt = streamRetryCount.incrementAndGet();
+ long delay = calculateStreamRetryDelay(retryAttempt);
+ String maxAttempts = isInfiniteRetryError(throwable)
+ ? "infinite" : String.valueOf(Constants.MAX_STREAM_RETRIES);
+ logger.info("Retrying DAST audit stream (attempt %d/%s) after %d ms; %d findings remain",
+ retryAttempt, maxAttempts, delay, pendingIssueIds.size());
+ scheduleRetry(delay);
+ return;
+ }
+
+ Status status = Status.fromThrowable(throwable);
+ completeExceptionally(new AviatorTechnicalException(
+ "DAST audit stream failed: " + status.getDescription(), throwable));
+ }
+
+ private boolean canRetry(Throwable throwable) {
+ int completedCount = workItems.size() - pendingIssueIds.size();
+ if (completedCount == lastRetryCompletedCount) {
+ if (stagnantRetryCount.incrementAndGet() >= 3) {
+ LOG.error("DAST audit stream made no progress after multiple retries");
+ return false;
+ }
+ } else {
+ stagnantRetryCount.set(0);
+ }
+ lastRetryCompletedCount = completedCount;
+ return isInfiniteRetryError(throwable) || streamRetryCount.get() < Constants.MAX_STREAM_RETRIES;
+ }
+
+ private void scheduleRetry(long delay) {
+ Runnable retry = () -> {
+ if (!resultFuture.isDone() && !isClosed.get()) {
+ isRetryScheduled.set(false);
+ startStream();
+ }
+ };
+ if (!isRpcCompleted.get() && activeRequestStream != null) {
+ activeRequestStream.cancel("Retrying DAST audit stream", null);
+ }
+ if (pingScheduler == null) {
+ CompletableFuture.delayedExecutor(delay, TimeUnit.MILLISECONDS).execute(retry);
+ } else {
+ retryTask = pingScheduler.schedule(retry, delay, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ static boolean isRetryableError(Throwable throwable) {
+ Status status = Status.fromThrowable(throwable);
+ String description = status.getDescription();
+ return status.getCode() == Status.Code.UNAVAILABLE ||
+ status.getCode() == Status.Code.INTERNAL && description != null &&
+ (description.contains("RST_STREAM") || description.contains("PROTOCOL_ERROR"));
+ }
+
+ static boolean isInfiniteRetryError(Throwable throwable) {
+ Status status = Status.fromThrowable(throwable);
+ String description = status.getDescription();
+ return status.getCode() == Status.Code.INTERNAL && description != null &&
+ description.contains("PROTOCOL_ERROR");
+ }
+
+ static long calculateStreamRetryDelay(int retryCount) {
+ long delay = (long) (Constants.STREAM_RETRY_BASE_DELAY_MS * Math.pow(2, retryCount - 1));
+ return Math.min(delay, Constants.STREAM_RETRY_MAX_DELAY_MS) +
+ ThreadLocalRandom.current().nextLong(1000);
+ }
+
+ private void sendRequest(DastAuditClientMessage request) {
+ RequestHandler currentHandler = requestHandler;
+ currentHandler.sendRequest(request).whenComplete((sent, throwable) -> {
+ if (currentHandler != requestHandler || resultFuture.isDone() || isClosed.get()) return;
+ if (throwable != null || !Boolean.TRUE.equals(sent)) {
+ Throwable cause = throwable != null ? throwable : Status.UNAVAILABLE
+ .withDescription("Unable to send DAST audit stream request")
+ .asRuntimeException();
+ handleStreamError(cause);
+ }
+ });
+ }
+
+ private void fail(RuntimeException exception) {
+ stopPingPong();
+ if (requestHandler != null && !requestHandler.isCompleted()) {
+ requestHandler.sendError(exception);
+ }
+ completeExceptionally(exception);
+ }
+
+ private void completeExceptionally(RuntimeException exception) {
+ if (!resultFuture.isDone()) resultFuture.completeExceptionally(exception);
+ }
+
+ private void completeSuccessfully() {
+ if (!resultFuture.isDone()) {
+ resultFuture.complete(new DastAuditStreamResult(
+ List.copyOf(results), reservedQuota, exceededCount, unlimitedQuota,
+ quotaLastUpdated, nextQuotaUpdateMessage));
+ }
+ }
+
+ private void stopPingPong() {
+ if (pingTask != null) pingTask.cancel(false);
+ }
+
+ private boolean isSuccess(String status) {
+ return "SUCCESS".equalsIgnoreCase(status) || "OK".equalsIgnoreCase(status);
+ }
+
+ private String value(String value) {
+ return value != null ? value : "";
+ }
+
+ @Override
+ public void close() {
+ isClosed.set(true);
+ stopPingPong();
+ if (retryTask != null) retryTask.cancel(false);
+ if (!isRpcCompleted.get() && activeRequestStream != null) {
+ activeRequestStream.cancel("DAST audit stream processor closed", null);
+ } else if (requestHandler != null && !requestHandler.isCompleted()) {
+ requestHandler.sendError(new AviatorTechnicalException("DAST audit stream processor closed"));
+ }
+ if (resultFuture != null && !resultFuture.isDone()) {
+ completeExceptionally(new AviatorTechnicalException("DAST audit stream processor closed"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamResult.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamResult.java
new file mode 100644
index 00000000000..c504dd5bf29
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditStreamResult.java
@@ -0,0 +1,27 @@
+/*
+ * 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.grpc;
+
+import java.util.List;
+
+/**
+ * Results and quota metadata returned by one DAST audit stream.
+ */
+public record DastAuditStreamResult(
+ List results,
+ int reservedQuota,
+ int exceededCount,
+ boolean unlimitedQuota,
+ String quotaLastUpdated,
+ String nextQuotaUpdateMessage
+) {}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditWorkItem.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditWorkItem.java
new file mode 100644
index 00000000000..1aca385b831
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/DastAuditWorkItem.java
@@ -0,0 +1,21 @@
+/*
+ * 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.grpc;
+
+import com.fortify.cli.aviator.dast.DastIssue;
+import com.fortify.cli.aviator.dast.DastSession;
+
+/**
+ * Associates a DAST finding with the WebInspect session that contains its HTTP evidence.
+ */
+public record DastAuditWorkItem(DastSession session, DastIssue issue) {}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/RequestHandler.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/RequestHandler.java
index 12dcd9550a7..62f1231eea4 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/RequestHandler.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/RequestHandler.java
@@ -134,15 +134,25 @@ public boolean flush() {
* Complete the stream and send any remaining requests.
*/
public CompletableFuture complete() {
- if (!isCompleted.compareAndSet(false, true)) {
+ if (isCompleted.get()) {
return CompletableFuture.completedFuture(null);
}
return CompletableFuture.runAsync(() -> {
sendLock.lock();
try {
- // Flush any remaining requests
- flush();
+ if (!isCompleted.compareAndSet(false, true)) {
+ return;
+ }
+
+ T request;
+ while ((request = requestQueue.poll()) != null) {
+ if (requestObserver != null) {
+ requestObserver.onNext(request);
+ totalSent++;
+ pendingRequests.decrementAndGet();
+ }
+ }
// Complete the stream
if (requestObserver != null) {
diff --git a/fcli-core/fcli-aviator-common/src/main/proto/dast_audit.proto b/fcli-core/fcli-aviator-common/src/main/proto/dast_audit.proto
new file mode 100644
index 00000000000..724d0f470e4
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/proto/dast_audit.proto
@@ -0,0 +1,136 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "com.fortify.aviator.dastaudit";
+
+package dastaudit;
+
+service DastAuditService {
+ rpc ProcessDastAuditStream(stream DastAuditClientMessage) returns (stream DastAuditServerMessage);
+}
+
+message DastAuditStreamInitRequest {
+ string token = 1;
+ string applicationName = 2;
+ string streamId = 3;
+ string requestId = 4;
+ int32 totalReportedIssues = 5;
+ int32 totalIssuesToAudit = 6;
+ optional string fprBuildId = 7;
+ optional string sscApplicationName = 8;
+ optional string sscApplicationVersion = 9;
+}
+
+message DastAuditClientMessage {
+ oneof request_type {
+ DastAuditStreamInitRequest init = 1;
+ DastAuditRequest audit = 2;
+ DastAuditPingRequest ping = 3;
+ }
+}
+
+message DastAuditServerMessage {
+ oneof response_type {
+ DastAuditInitResponse init = 1;
+ DastAuditResponse audit = 2;
+ DastAuditErrorResponse error = 3;
+ DastAuditPongResponse pong = 4;
+ }
+}
+
+message DastAuditInitResponse {
+ string requestId = 1;
+ string clientStreamId = 2;
+ string serverStreamId = 3;
+ string status = 4;
+ string statusMessage = 5;
+ int32 reservedQuota = 6;
+ int32 exceededCount = 7;
+ bool unlimitedQuota = 8;
+ optional string quotaLastUpdated = 9;
+ optional string nextQuotaUpdateMessage = 10;
+ optional string reassignedEntitlementId = 11;
+}
+
+message DastAuditRequest {
+ string requestId = 1;
+ string streamId = 2;
+ DastFindingContext finding = 3;
+}
+
+message DastFindingContext {
+ string issueId = 1;
+ string checkTypeId = 2;
+ string engineType = 3;
+ string vulnerabilityId = 4;
+ int32 severity = 5;
+ string name = 6;
+ string category = 7;
+ string cweId = 8;
+ string cweDescription = 9;
+ string sessionUrl = 10;
+ repeated string reproStepUrls = 11;
+ string summary = 12;
+ string implication = 13;
+ string execution = 14;
+ string fix = 15;
+ string referenceInfo = 16;
+ repeated DastClassification classifications = 17;
+ string requestId = 18;
+ string scheme = 19;
+ string host = 20;
+ int32 port = 21;
+ string attackParamDescriptor = 22;
+ string rawRequest = 23;
+ string rawResponse = 24;
+ repeated DastReproStep reproSteps = 25;
+}
+
+message DastClassification {
+ string kind = 1;
+ string value = 2;
+}
+
+message DastReproStep {
+ string source = 1;
+ string url = 2;
+ string postParams = 3;
+}
+
+message DastAuditResponse {
+ string requestId = 1;
+ string streamId = 2;
+ string dastIssueId = 3;
+ string status = 4;
+ string statusMessage = 5;
+ DastAuditDecision decision = 6;
+}
+
+message DastAuditDecision {
+ bool truePositive = 1;
+ string confidence = 2;
+ string reasoning = 3;
+ string remediationAdvice = 4;
+ string finalComment = 5;
+ string tagValue = 6;
+ string tier = 7;
+}
+
+message DastAuditErrorResponse {
+ string requestId = 1;
+ string clientStreamId = 2;
+ string serverStreamId = 3;
+ string status = 4;
+ string statusMessage = 5;
+}
+
+message DastAuditPingRequest {
+ string streamId = 1;
+ int64 timestamp = 2;
+}
+
+message DastAuditPongResponse {
+ string streamId = 1;
+ int64 serverTimestamp = 2;
+ int64 clientTimestamp = 3;
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/resources/default_dast_tag_mapping.yaml b/fcli-core/fcli-aviator-common/src/main/resources/default_dast_tag_mapping.yaml
new file mode 100644
index 00000000000..5283eadff4c
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/resources/default_dast_tag_mapping.yaml
@@ -0,0 +1,24 @@
+# Set the SSC tag used to store DAST audit results. Optional.
+# If not set, defaults to the SSC Analysis tag.
+tag_id: "87f2364f-dcd4-49e6-861d-f8d3f351686b"
+# Map DAST audit decisions to SSC tag values and suppression behavior.
+# tier_1 contains high-confidence decisions; tier_2 contains lower-confidence decisions.
+mapping:
+ tier_1:
+ fp:
+ value: "Not an Issue"
+ suppress: true
+ tp:
+ value: "Exploitable"
+ suppress: false
+ unsure:
+ suppress: false
+ tier_2:
+ fp:
+ value: "Not an Issue"
+ suppress: false
+ tp:
+ value: "Suspicious"
+ suppress: false
+ unsure:
+ suppress: false
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/DastAuditDecisionMapperTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/DastAuditDecisionMapperTest.java
new file mode 100644
index 00000000000..d46133d3cd8
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/DastAuditDecisionMapperTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.audit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import com.fortify.cli.aviator.grpc.DastAuditResult;
+import com.fortify.cli.aviator.util.Constants;
+
+class DastAuditDecisionMapperTest {
+ @Test
+ void unknownConfidenceFalsePositiveRemainsUnsuppressed() {
+ var result = new DastAuditResult.Success(
+ "DAST-1", false, "UNKNOWN", "reason", "", "comment", "bad", "GOLD");
+
+ var response = DastAuditDecisionMapper.toAuditResponse(result);
+
+ assertEquals("SILVER", response.getTier());
+ assertEquals(Constants.AVIATOR_LIKELY_FP, response.getAviatorPredictionTag());
+ }
+
+ @Test
+ void highConfidenceFalsePositiveIsSuppressible() {
+ var result = new DastAuditResult.Success(
+ "DAST-1", false, "HIGH", "reason", "", "comment", "", "");
+
+ var response = DastAuditDecisionMapper.toAuditResponse(result);
+
+ assertEquals("GOLD", response.getTier());
+ assertEquals(Constants.AVIATOR_NOT_AN_ISSUE, response.getAviatorPredictionTag());
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/DastAuditFPRTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/DastAuditFPRTest.java
new file mode 100644
index 00000000000..95d2ca4c800
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/DastAuditFPRTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.audit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.fortify.cli.aviator._common.config.AviatorConfigManager;
+import com.fortify.cli.aviator.config.TagMappingConfig;
+import com.fortify.cli.aviator.grpc.DastAuditResult;
+import com.fortify.cli.aviator.grpc.DastAuditStreamConfig;
+import com.fortify.cli.aviator.grpc.DastAuditStreamResult;
+import com.fortify.cli.aviator.util.FprHandle;
+import com.fortify.cli.aviator.util.ResourceUtil;
+
+class DastAuditFPRTest {
+ @TempDir Path tempDir;
+
+ @Test
+ void auditsEligibleFindingAndWritesConservativeXml() throws Exception {
+ Path fpr = createFpr();
+ var config = new DastAuditStreamConfig("token", "app", "ssc", "1", null);
+
+ DastAuditFprResult result;
+ try (FprHandle handle = new FprHandle(fpr)) {
+ result = DastAuditFPR.audit(handle, config, defaultTagMapping(), (ignoredConfig, items, total) ->
+ CompletableFuture.completedFuture(new DastAuditStreamResult(List.of(
+ new DastAuditResult.Success("DAST-1", false, "HIGH", "reason", "", "comment", "", "")
+ ), 1, 0, false, null, null)));
+ }
+
+ assertEquals("AUDITED", result.status());
+ assertEquals(1, result.falsePositivesSuppressed());
+ try (FileSystem zip = FileSystems.newFileSystem(fpr)) {
+ String auditXml = Files.readString(zip.getPath("/audit.xml"));
+ assertTrue(auditXml.contains("instanceId=\"DAST-1\""));
+ assertTrue(auditXml.contains("suppressed=\"true\""));
+ assertTrue(auditXml.contains("PROCESSED_BY_AVIATOR"));
+ assertFalse(Files.exists(zip.getPath("/remediations.xml")));
+ }
+ }
+
+ @Test
+ void customTagMappingControlsFinalTagAndSuppression() throws Exception {
+ Path fpr = createFpr();
+ Path tagMapping = tempDir.resolve("dast-tag-mapping.yaml");
+ Files.writeString(tagMapping, """
+ tag_id: "custom-analysis-tag"
+ mapping:
+ tier_1:
+ fp: { value: "Confirmed FP", suppress: false }
+ tp: { value: "Confirmed TP", suppress: false }
+ unsure: { suppress: false }
+ tier_2:
+ fp: { value: "Review FP", suppress: true }
+ tp: { value: "Review TP", suppress: false }
+ unsure: { suppress: false }
+ """);
+ var config = new DastAuditStreamConfig("token", "app", "ssc", "1", null);
+
+ DastAuditFprResult result;
+ try (FprHandle handle = new FprHandle(fpr)) {
+ result = DastAuditFPR.audit(handle, config,
+ ResourceUtil.loadYamlFile(tagMapping.toFile(), TagMappingConfig.class),
+ (ignoredConfig, items, total) ->
+ CompletableFuture.completedFuture(new DastAuditStreamResult(List.of(
+ new DastAuditResult.Success("DAST-1", false, "MEDIUM", "reason", "", "comment", "", "")
+ ), 1, 0, false, null, null)));
+ }
+
+ assertEquals(1, result.falsePositivesSuppressed());
+ try (FileSystem zip = FileSystems.newFileSystem(fpr)) {
+ String auditXml = Files.readString(zip.getPath("/audit.xml"));
+ assertTrue(auditXml.contains("suppressed=\"true\""));
+ assertTrue(auditXml.contains("id=\"custom-analysis-tag\""));
+ assertTrue(auditXml.contains(">Review FP<"));
+ }
+ }
+
+ @Test
+ void writesOnlyIssuesUpdatedByCurrentAudit() throws Exception {
+ Path fpr = createFpr();
+ try (FileSystem zip = FileSystems.newFileSystem(fpr)) {
+ Files.writeString(zip.getPath("/audit.xml"), """
+
+
+
+
+ """);
+ }
+ var config = new DastAuditStreamConfig("token", "app", "ssc", "1", null);
+
+ try (FprHandle handle = new FprHandle(fpr)) {
+ DastAuditFPR.audit(handle, config, defaultTagMapping(), (ignoredConfig, items, total) ->
+ CompletableFuture.completedFuture(new DastAuditStreamResult(List.of(
+ new DastAuditResult.Success("DAST-1", true, "HIGH", "reason", "", "comment", "", "")
+ ), 1, 0, false, null, null)));
+ }
+
+ try (FileSystem zip = FileSystems.newFileSystem(fpr)) {
+ String auditXml = Files.readString(zip.getPath("/audit.xml"));
+ assertTrue(auditXml.contains("instanceId=\"DAST-1\""));
+ assertFalse(auditXml.contains("instanceId=\"DAST-2\""));
+ }
+ }
+
+ @Test
+ void missingTerminalResponseIsCountedAsFailure() throws Exception {
+ Path fpr = createFpr();
+ var config = new DastAuditStreamConfig("token", "app", "ssc", "1", null);
+
+ try (FprHandle handle = new FprHandle(fpr)) {
+ DastAuditFprResult result = DastAuditFPR.audit(
+ handle, config, defaultTagMapping(), (ignoredConfig, items, total) ->
+ CompletableFuture.completedFuture(new DastAuditStreamResult(
+ List.of(), 1, 0, false, null, null)));
+
+ assertEquals("FAILED", result.status());
+ assertEquals(1, result.failed());
+ }
+ }
+
+ private TagMappingConfig defaultTagMapping() {
+ return AviatorConfigManager.getInstance().getDefaultDastTagMappingConfig();
+ }
+
+ private Path createFpr() throws Exception {
+ Path fpr = tempDir.resolve("dast.fpr");
+ try (FileSystem zip = FileSystems.newFileSystem(fpr, Map.of("create", "true"))) {
+ Files.writeString(zip.getPath("/webinspect.xml"), """
+ https://example.test
+ SQL Injection4
+
+ """, StandardCharsets.UTF_8);
+ }
+ return fpr;
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/config/TagMappingConfigTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/config/TagMappingConfigTest.java
index 43aeb69ab9b..c1ed01ee269 100644
--- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/config/TagMappingConfigTest.java
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/config/TagMappingConfigTest.java
@@ -21,6 +21,7 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -135,6 +136,27 @@ void testLoadYamlFileBindsSuppressionExclusionsAcrossEntries() throws Exception
assertTrue(config.isSuppressionExcluded(new TagMappingConfig.SuppressionExclusionContext("privacy violation")));
}
+ @Test
+ void testResolvesResultsAndMappedValues() {
+ TagMappingConfig config = createValidConfig();
+
+ assertTrue(config.getResult(true, TagMappingConfig.ResultType.FP).getSuppress());
+ assertFalse(config.getResult(false, TagMappingConfig.ResultType.FP).getSuppress());
+ assertEquals(Set.of("Not an Issue", "Exploitable"), config.getMappedValues());
+ }
+
+ @Test
+ void testDastValidationRejectsSuppressionExclusions() {
+ TagMappingConfig config = createValidConfig();
+ config.setSuppression_exclusions(new ArrayList<>(List.of(createSuppressionExclusion("Privacy Violation"))));
+
+ AviatorSimpleException exception = assertThrows(AviatorSimpleException.class, config::validateForDast);
+
+ assertEquals(
+ "Invalid DAST tag mapping configuration: suppression_exclusions are not supported",
+ exception.getMessage());
+ }
+
private TagMappingConfig createValidConfig() {
TagMappingConfig config = new TagMappingConfig();
TagMappingConfig.Mapping mapping = new TagMappingConfig.Mapping();
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/dast/StreamingWebInspectParserTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/dast/StreamingWebInspectParserTest.java
new file mode 100644
index 00000000000..582b6641154
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/dast/StreamingWebInspectParserTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.dast;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.fortify.cli.aviator.util.FprHandle;
+
+class StreamingWebInspectParserTest {
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void parseSessionsPreservesCompleteAuditContext() throws Exception {
+ Path fpr = createFpr();
+
+ try (FprHandle handle = new FprHandle(fpr)) {
+ var sessions = new StreamingWebInspectParser(handle).parseSessions();
+
+ assertEquals(1, sessions.size());
+ DastSession session = sessions.get(0);
+ assertEquals("POST /login HTTP/1.1", session.getRawRequest());
+ assertEquals("HTTP/1.1 200 OK", session.getRawResponse());
+ assertEquals("parameter=username", session.getAttackParamDescriptor());
+
+ DastIssue issue = session.getIssues().get(0);
+ assertEquals("Injection", issue.getCategory());
+ assertEquals("Improper Neutralization", issue.getCweDescription());
+ assertEquals("Summary text", issue.getSummary());
+ assertEquals("Fix text", issue.getFix());
+ assertEquals(2, issue.getReproSteps().size());
+ assertEquals("Macro", issue.getReproSteps().get(0).getSource());
+ assertEquals("Attack", issue.getReproSteps().get(1).getSource());
+ assertEquals("username=test%27", issue.getReproSteps().get(1).getPostParams());
+ assertEquals(issue.getReproSteps().stream().map(DastReproStep::getUrl).toList(), issue.getReproStepUrls());
+ }
+ }
+
+ private Path createFpr() throws Exception {
+ Path fpr = tempDir.resolve("dast.fpr");
+ try (FileSystem zip = FileSystems.newFileSystem(fpr, Map.of("create", "true"))) {
+ Files.writeString(zip.getPath("/webinspect.xml"), webInspectXml(), StandardCharsets.UTF_8);
+ }
+ return fpr;
+ }
+
+ private String webInspectXml() {
+ String request = Base64.getEncoder().encodeToString("POST /login HTTP/1.1".getBytes(StandardCharsets.UTF_8));
+ String response = Base64.getEncoder().encodeToString("HTTP/1.1 200 OK".getBytes(StandardCharsets.UTF_8));
+ return """
+
+
+ https://example.test/login
+ httpsexample.test443
+ parameter=username
+ %s%s
+
+ 1001WebInspect
+ WI-10014SQL Injection
+
+ Injection
+ Improper Neutralization
+
+
+ Macrohttps://example.test/login
+ Attackhttps://example.test/login?user=test%%27
+ username=test%%27
+
+ SummarySummary text
]]>
+ FixFix text]]>
+
+
+
+ """.formatted(request, response);
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditRequestMapperTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditRequestMapperTest.java
new file mode 100644
index 00000000000..8f37a595127
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditRequestMapperTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import com.fortify.cli.aviator.dast.DastIssue;
+import com.fortify.cli.aviator.dast.DastReproStep;
+import com.fortify.cli.aviator.dast.DastSession;
+
+class DastAuditRequestMapperTest {
+ @Test
+ void mapsCompleteFindingContext() {
+ var session = new DastSession();
+ session.setRequestId("request-1");
+ session.setUrl("https://example.test");
+ session.setRawRequest("GET / HTTP/1.1");
+ session.setRawResponse("HTTP/1.1 200 OK");
+ var issue = new DastIssue();
+ issue.setId("DAST-1");
+ issue.setName("SQL Injection");
+ issue.getClassifications().put("CWE", "Improper Neutralization");
+ var step = new DastReproStep();
+ step.setSource("Attack");
+ step.setUrl("https://example.test?id=1");
+ step.setPostParams("id=1");
+ issue.getReproSteps().add(step);
+ issue.getReproStepUrls().add(step.getUrl());
+
+ var context = DastAuditRequestMapper.toFindingContext(session, issue);
+
+ assertEquals("DAST-1", context.getIssueId());
+ assertEquals("GET / HTTP/1.1", context.getRawRequest());
+ assertEquals("CWE", context.getClassifications(0).getKind());
+ assertEquals("Attack", context.getReproSteps(0).getSource());
+ assertEquals("id=1", context.getReproSteps(0).getPostParams());
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditResponseMapperTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditResponseMapperTest.java
new file mode 100644
index 00000000000..45b7424972c
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditResponseMapperTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import org.junit.jupiter.api.Test;
+
+import com.fortify.aviator.dastaudit.DastAuditDecision;
+import com.fortify.aviator.dastaudit.DastAuditResponse;
+
+class DastAuditResponseMapperTest {
+ @Test
+ void usesIssueIdAssociatedWithRequest() {
+ var response = DastAuditResponse.newBuilder()
+ .setRequestId("request-1")
+ .setDastIssueId("DAST-1")
+ .setStatus("SUCCESS")
+ .setDecision(DastAuditDecision.newBuilder().setTruePositive(true).setConfidence("HIGH"))
+ .build();
+
+ DastAuditResult result = DastAuditResponseMapper.map(response, "DAST-1");
+
+ var success = assertInstanceOf(DastAuditResult.Success.class, result);
+ assertEquals("DAST-1", result.issueId());
+ assertEquals("SUCCESS", result.status());
+ assertEquals(true, success.truePositive());
+ }
+
+ @Test
+ void rejectsMismatchedServerIssueId() {
+ var response = DastAuditResponse.newBuilder()
+ .setRequestId("request-1")
+ .setDastIssueId("DAST-WRONG")
+ .setStatus("SUCCESS")
+ .build();
+
+ DastAuditResult result = DastAuditResponseMapper.map(response, "DAST-1");
+
+ assertInstanceOf(DastAuditResult.Failure.class, result);
+ assertEquals("DAST-1", result.issueId());
+ assertEquals("FAILED", result.status());
+ }
+
+ @Test
+ void preservesSkippedResponseAsDistinctVariant() {
+ var response = DastAuditResponse.newBuilder()
+ .setRequestId("request-1")
+ .setDastIssueId("DAST-1")
+ .setStatus("SKIPPED")
+ .setStatusMessage("Quota exceeded")
+ .build();
+
+ DastAuditResult result = DastAuditResponseMapper.map(response, "DAST-1");
+
+ assertInstanceOf(DastAuditResult.Skipped.class, result);
+ assertEquals("SKIPPED", result.status());
+ assertEquals("Quota exceeded", result.statusMessage());
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamConfigTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamConfigTest.java
new file mode 100644
index 00000000000..0cd16e80571
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamConfigTest.java
@@ -0,0 +1,33 @@
+/*
+ * 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.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
+
+class DastAuditStreamConfigTest {
+ @Test
+ void rejectsBlankToken() {
+ assertThrows(AviatorSimpleException.class,
+ () -> new DastAuditStreamConfig(" ", "app", "ssc", "1", null));
+ }
+
+ @Test
+ void rejectsBlankApplicationName() {
+ assertThrows(AviatorSimpleException.class,
+ () -> new DastAuditStreamConfig("token", " ", "ssc", "1", null));
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessorTest.java
new file mode 100644
index 00000000000..b0a8952b977
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamProcessorTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import com.fortify.aviator.dastaudit.DastAuditClientMessage;
+import com.fortify.cli.aviator.dast.DastIssue;
+import com.fortify.cli.aviator.dast.DastSession;
+
+import io.grpc.Status;
+
+class DastAuditStreamProcessorTest {
+ @Test
+ void registersAllRequestsBeforeReturningMessagesForSending() {
+ var processor = new DastAuditStreamProcessor(null, null, null, 0);
+
+ List requests = processor.prepareAuditRequests(
+ List.of(workItem("DAST-1"), workItem("DAST-2")), "stream-1");
+
+ assertEquals(List.of("DAST-1", "DAST-2"), requests.stream()
+ .map(request -> request.getAudit().getFinding().getIssueId())
+ .toList());
+ assertEquals(2, processor.pendingRequestCount());
+ }
+
+ @Test
+ void reusesRequestIdsAndRequeuesOnlyUnfinishedWork() {
+ var processor = new DastAuditStreamProcessor(null, null, null, 0);
+ List workItems = List.of(workItem("DAST-1"), workItem("DAST-2"));
+ processor.initializeWorkItems(workItems);
+ List firstAttempt = processor.prepareAuditRequests(workItems, "stream-1");
+
+ processor.completeRequest(firstAttempt.get(0).getAudit().getRequestId());
+ List retry = processor.prepareAuditRequests(
+ processor.remainingWorkItems(), "stream-2");
+
+ assertEquals(1, retry.size());
+ assertEquals("DAST-2", retry.get(0).getAudit().getFinding().getIssueId());
+ assertEquals(firstAttempt.get(1).getAudit().getRequestId(), retry.get(0).getAudit().getRequestId());
+ assertEquals("stream-2", retry.get(0).getAudit().getStreamId());
+ }
+
+ @Test
+ void retriesOnlyTransportDisconnections() {
+ assertTrue(DastAuditStreamProcessor.isRetryableError(Status.UNAVAILABLE.asRuntimeException()));
+ assertTrue(DastAuditStreamProcessor.isRetryableError(
+ Status.INTERNAL.withDescription("RST_STREAM closed").asRuntimeException()));
+ assertTrue(DastAuditStreamProcessor.isInfiniteRetryError(
+ Status.INTERNAL.withDescription("PROTOCOL_ERROR").asRuntimeException()));
+ assertFalse(DastAuditStreamProcessor.isRetryableError(Status.INVALID_ARGUMENT.asRuntimeException()));
+ }
+
+ private DastAuditWorkItem workItem(String issueId) {
+ var issue = new DastIssue();
+ issue.setId(issueId);
+ return new DastAuditWorkItem(new DastSession(), issue);
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/RequestHandlerTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/RequestHandlerTest.java
new file mode 100644
index 00000000000..d720d653aed
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/RequestHandlerTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.grpc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import io.grpc.stub.StreamObserver;
+
+class RequestHandlerTest {
+ @Test
+ void completeFlushesQueuedRequestsBeforeHalfClose() {
+ var events = new ArrayList();
+ var handler = new RequestHandler("stream");
+ handler.sendRequest("one");
+ handler.sendRequest("two");
+ handler.initialize(observer(events));
+
+ handler.complete().join();
+
+ assertEquals(List.of("one", "two", "completed"), events);
+ assertTrue(handler.isCompleted());
+ }
+
+ private StreamObserver observer(List events) {
+ return new StreamObserver<>() {
+ @Override public void onNext(String value) { events.add(value); }
+ @Override public void onError(Throwable throwable) { events.add("error"); }
+ @Override public void onCompleted() { events.add("completed"); }
+ };
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java
index 2a4e73281df..bcbb949a845 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java
@@ -16,7 +16,6 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -312,7 +311,7 @@ private void validateSSCTagsBeforeUpload(UnirestInstance unirest, SSCAppVersionD
LOG.info("Starting SSC tag validation before FPR upload for app version id={}.", av.getVersionId());
TagMappingConfig tagMappingConfig = loadTagMappingForValidation();
LOG.debug("Tag mapping config loaded: tag_id='{}', mapping={}", tagMappingConfig.getTag_id(), tagMappingConfig.getMapping());
- Set analysisTagValues = extractAnalysisTagValues(tagMappingConfig);
+ Set analysisTagValues = tagMappingConfig.getMappedValues();
LOG.info("Analysis tag values to validate: {}", analysisTagValues);
List warnings = AviatorSSCTagValidator.validatePreUpload(
unirest, av.getVersionId(), tagMappingConfig.getTag_id(), analysisTagValues, logger);
@@ -326,22 +325,6 @@ private TagMappingConfig loadTagMappingForValidation() {
return AviatorConfigManager.getInstance().getDefaultTagMappingConfig();
}
- private Set extractAnalysisTagValues(TagMappingConfig config) {
- Set values = new LinkedHashSet<>();
- if (config.getMapping() != null) {
- addTierValues(values, config.getMapping().getTier_1());
- addTierValues(values, config.getMapping().getTier_2());
- }
- return values;
- }
-
- private void addTierValues(Set values, TagMappingConfig.Tier tier) {
- if (tier == null) return;
- if (tier.getFp() != null && tier.getFp().getValue() != null) values.add(tier.getFp().getValue());
- if (tier.getTp() != null && tier.getTp().getValue() != null) values.add(tier.getTp().getValue());
- if (tier.getUnsure() != null && tier.getUnsure().getValue() != null) values.add(tier.getUnsure().getValue());
- }
-
private Path downloadFpr(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger) throws IOException {
logger.progress("Status: Downloading FPR from SSC for app version: %s:%s (id: %s)", av.getApplicationName(), av.getVersionName(), av.getVersionId());
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCommands.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCommands.java
index d7147c455e4..07e7d7429f4 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCommands.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCommands.java
@@ -20,6 +20,7 @@
name = "ssc",
subcommands = {
AviatorSSCAuditCommand.class,
+ AviatorSSCDastAuditCommand.class,
AviatorSSCPrepareCommand.class,
AviatorSSCApplyRemediationsCommand.class,
AviatorSSCDownloadRemediationsCacheCommand.class,
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java
index 4411e1c2386..78dd93b330a 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java
@@ -242,8 +242,8 @@ private String uploadEnrichedDastFpr(DownloadedFprs fprs, List c
new DastFprCorrelationEnricher().injectAndRepackage(fprs.dastPath, confirmed);
logger.progress("Status: Uploading correlated DAST FPR to SSC...");
- AviatorSSCCorrelateDownloadHelper.uploadEnrichedDastFpr(unirest, av, fprs.dastPath, progressWriter);
- String artifactId = fprs.adDast.getId();
+ String artifactId = AviatorSSCCorrelateDownloadHelper.uploadEnrichedDastFpr(
+ unirest, av, fprs.dastPath, progressWriter);
logger.progress("Status: Correlated DAST FPR uploaded successfully (artifact id=%s)", artifactId);
return artifactId;
}
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java
new file mode 100644
index 00000000000..20e4beea679
--- /dev/null
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java
@@ -0,0 +1,172 @@
+/*
+ * 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.cli.cmd;
+
+import static com.fortify.cli.ssc.artifact.helper.SSCArtifactHelper.getLatestDASTArtifact;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fortify.cli.aviator._common.config.AviatorConfigManager;
+import com.fortify.cli.aviator._common.session.user.cli.mixin.AviatorUserSessionDescriptorSupplier;
+import com.fortify.cli.aviator.audit.DastAuditFPR;
+import com.fortify.cli.aviator.audit.DastAuditFprResult;
+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.AviatorSSCAuditHelper;
+import com.fortify.cli.aviator.ssc.helper.AviatorSSCFprTransferHelper;
+import com.fortify.cli.aviator.ssc.helper.AviatorSSCTagValidator;
+import com.fortify.cli.aviator.util.FprHandle;
+import com.fortify.cli.aviator.util.ResourceUtil;
+import com.fortify.cli.common.exception.FcliTechnicalException;
+import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
+import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
+import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin;
+import com.fortify.cli.common.progress.helper.IProgressWriter;
+import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand;
+import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin;
+import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor;
+import com.fortify.cli.ssc.artifact.helper.SSCArtifactDescriptor;
+
+import kong.unirest.UnirestInstance;
+import lombok.Getter;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+import picocli.CommandLine.Option;
+
+@Command(name = "audit-dast")
+public class AviatorSSCDastAuditCommand extends AbstractSSCJsonNodeOutputCommand implements IActionCommandResultSupplier {
+ private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCDastAuditCommand.class);
+
+ @Getter @Mixin private OutputHelperMixins.DetailsNoQuery outputHelper;
+ @Mixin private ProgressWriterFactoryMixin progressWriterFactoryMixin;
+ @Mixin private SSCAppVersionResolverMixin.RequiredOption appVersionResolver;
+ @Mixin private AviatorUserSessionDescriptorSupplier sessionDescriptorSupplier;
+ @Option(names = {"--app"}) private String appName;
+ @Option(names = {"--tag-mapping"}) private String tagMapping;
+
+ private String actionResult = "SKIPPED";
+
+ @Override
+ public JsonNode getJsonNode(UnirestInstance unirest) {
+ Path downloadedFpr = null;
+ try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()) {
+ var logger = new AviatorLoggerImpl(progressWriter);
+ var appVersion = appVersionResolver.getAppVersionDescriptor(unirest);
+ var session = sessionDescriptorSupplier.getSessionDescriptor();
+ TagMappingConfig tagMappingConfig = loadTagMappingConfig();
+ SSCArtifactDescriptor artifact = getLatestDASTArtifact(unirest, appVersion.getVersionId());
+ downloadedFpr = AviatorSSCFprTransferHelper.downloadArtifactFpr(
+ unirest, artifact, logger, progressWriter);
+
+ DastAuditFprResult result = auditFpr(
+ downloadedFpr, appVersion, session, logger, tagMappingConfig);
+ actionResult = result.status();
+ String artifactId = null;
+ if (result.updatedFile() != null && result.succeeded() > 0) {
+ validateSSCTagsBeforeUpload(unirest, appVersion, logger, tagMappingConfig);
+ logger.progress("Status: Uploading audited DAST FPR to SSC");
+ artifactId = AviatorSSCFprTransferHelper.uploadDastFpr(
+ unirest, appVersion, downloadedFpr, progressWriter);
+ }
+ return buildOutput(appVersion, result, artifactId);
+ } catch (RuntimeException e) {
+ actionResult = "FAILED";
+ throw e;
+ } catch (Exception e) {
+ actionResult = "FAILED";
+ throw new FcliTechnicalException("DAST audit failed", e);
+ } finally {
+ if (downloadedFpr != null) {
+ try {
+ Files.deleteIfExists(downloadedFpr);
+ } catch (Exception e) {
+ LOG.warn("Failed to delete temporary DAST FPR {}", downloadedFpr, e);
+ }
+ }
+ }
+ }
+
+ private DastAuditFprResult auditFpr(
+ Path fprPath,
+ SSCAppVersionDescriptor appVersion,
+ com.fortify.cli.aviator._common.session.user.helper.AviatorUserSessionDescriptor session,
+ IAviatorLogger logger,
+ TagMappingConfig tagMappingConfig) throws Exception {
+ String effectiveAppName = appName != null ? appName : appVersion.getApplicationName();
+ var config = new DastAuditStreamConfig(
+ session.getAviatorToken(), effectiveAppName,
+ appVersion.getApplicationName(), appVersion.getVersionName(), null);
+ try (var grpcClient = AviatorGrpcClientHelper.createClient(session.getAviatorUrl(), logger, 30);
+ var streamProcessor = new DastAuditStreamProcessor(
+ logger, grpcClient.getDastAuditAsyncStub(), grpcClient.getPingScheduler(),
+ grpcClient.getPingIntervalSeconds());
+ var fprHandle = new FprHandle(fprPath)) {
+ long timeout = Math.max(grpcClient.getDefaultTimeoutSeconds(), 300);
+ return DastAuditFPR.audit(fprHandle, config, tagMappingConfig, (streamConfig, workItems, totalReported) ->
+ streamProcessor.process(streamConfig, workItems, totalReported)
+ .orTimeout(timeout, TimeUnit.SECONDS));
+ } catch (CompletionException e) {
+ throw e.getCause() instanceof Exception exception ? exception : e;
+ }
+ }
+
+ private ObjectNode buildOutput(
+ SSCAppVersionDescriptor appVersion,
+ DastAuditFprResult audit,
+ String artifactId) {
+ ObjectNode result = AviatorSSCAuditHelper.buildResultNode(appVersion, artifactId, audit.status());
+ AviatorSSCAuditHelper.setDastAuditStats(result, audit);
+ return result;
+ }
+
+ private TagMappingConfig loadTagMappingConfig() {
+ TagMappingConfig tagMappingConfig = tagMapping == null || tagMapping.isBlank()
+ ? AviatorConfigManager.getInstance().getDefaultDastTagMappingConfig()
+ : ResourceUtil.loadYamlFile(new File(tagMapping), TagMappingConfig.class);
+ tagMappingConfig.validateForDast();
+ return tagMappingConfig;
+ }
+
+ private void validateSSCTagsBeforeUpload(UnirestInstance unirest,
+ SSCAppVersionDescriptor appVersion, IAviatorLogger logger,
+ TagMappingConfig tagMappingConfig) {
+ List warnings = AviatorSSCTagValidator.validatePreUpload(
+ unirest, appVersion.getVersionId(), tagMappingConfig.getTag_id(),
+ tagMappingConfig.getMappedValues(), logger);
+ LOG.info("DAST tag validation complete. {} warning(s) found.", warnings.size());
+ }
+
+ @Override
+ public String getActionCommandResult() {
+ return actionResult;
+ }
+
+ @Override
+ public boolean isSingular() {
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java
index 542ee85bb86..48539a13b32 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelper.java
@@ -27,6 +27,7 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fortify.aviator.application.Application;
import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
+import com.fortify.cli.aviator.audit.DastAuditFprResult;
import com.fortify.cli.aviator.audit.model.FPRAuditResult;
import com.fortify.cli.aviator.config.AviatorLoggerImpl;
import com.fortify.cli.aviator.grpc.AviatorGrpcClient;
@@ -141,6 +142,37 @@ public static void setAuditStats(ObjectNode result, FPRAuditResult auditResult)
((ObjectNode) result.get("operation")).set("audit", audit);
}
+ /**
+ * Populates the standard audit output envelope with DAST-specific statistics.
+ *
+ * @param result The result node created by {@link #buildResultNode}.
+ * @param auditResult The DAST FPR audit result.
+ */
+ public static void setDastAuditStats(ObjectNode result, DastAuditFprResult auditResult) {
+ ObjectNode audit = JsonHelper.getObjectMapper().createObjectNode();
+ audit.put("message", getDastAuditMessage(auditResult));
+ audit.put("submitted", auditResult.submitted());
+ audit.put("succeeded", auditResult.succeeded());
+ audit.put("skipped", auditResult.skipped());
+ audit.put("failed", auditResult.failed());
+ ((ObjectNode) result.get("operation")).set("audit", audit);
+ result.remove("state");
+ }
+
+ private static String getDastAuditMessage(DastAuditFprResult auditResult) {
+ return switch (auditResult.status()) {
+ case "AUDITED" -> "DAST audit completed successfully";
+ case "PARTIALLY_AUDITED" -> auditResult.message() != null
+ ? auditResult.message() : "DAST audit partially completed";
+ case "SKIPPED" -> auditResult.message() != null
+ ? auditResult.message() : "No DAST findings to audit";
+ case "FAILED" -> auditResult.message() != null
+ ? auditResult.message() : "DAST audit failed";
+ default -> auditResult.message() != null
+ ? auditResult.message() : "Unknown DAST audit status";
+ };
+ }
+
/**
* Sets only the {@code operation.audit.message} field without audit stats.
* Used for code paths that don't perform an actual audit (SKIPPED, FAILED, QUOTA_EXCEEDED, etc.).
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelateDownloadHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelateDownloadHelper.java
index 2b47c4833c1..876da9d2efb 100644
--- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelateDownloadHelper.java
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelateDownloadHelper.java
@@ -39,15 +39,7 @@ private AviatorSSCCorrelateDownloadHelper() {}
*/
public static Path downloadArtifactFpr(UnirestInstance unirest, SSCArtifactDescriptor ad,
AviatorLoggerImpl logger, IProgressWriter progressWriter) throws IOException {
- Path fprPath = Files.createTempFile("aviator_" + ad.getId() + "_", ".fpr");
- logger.progress("Status: Downloading FPR from SSC (artifact id=" + ad.getId() + ")");
- SSCFileTransferHelper.download(
- unirest,
- SSCUrls.DOWNLOAD_ARTIFACT(ad.getId(), true),
- fprPath.toFile(),
- SSCFileTransferHelper.ISSCAddDownloadTokenFunction.ROUTEPARAM_DOWNLOADTOKEN,
- progressWriter);
- return fprPath;
+ return AviatorSSCFprTransferHelper.downloadArtifactFpr(unirest, ad, logger, progressWriter);
}
/**
@@ -73,18 +65,11 @@ public static Path downloadCurrentSastFpr(UnirestInstance unirest, SSCAppVersion
}
/**
- * Uploads an enriched DAST FPR to SSC using the HTML upload endpoint.
+ * Uploads an enriched DAST FPR to SSC and returns the new artifact ID.
*/
- public static void uploadEnrichedDastFpr(UnirestInstance unirest, SSCAppVersionDescriptor av,
+ public static String uploadEnrichedDastFpr(UnirestInstance unirest, SSCAppVersionDescriptor av,
Path enrichedDastFpr, IProgressWriter progressWriter) {
- SSCFileTransferHelper.htmlUpload(
- unirest,
- SSCUrls.UPLOAD_RESULT_FILE(av.getVersionId()),
- enrichedDastFpr.toFile(),
- SSCFileTransferHelper.ISSCAddUploadTokenFunction.ROUTEPARAM_UPLOADTOKEN,
- String.class,
- progressWriter
- );
+ return AviatorSSCFprTransferHelper.uploadDastFpr(unirest, av, enrichedDastFpr, progressWriter);
}
/**
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCFprTransferHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCFprTransferHelper.java
new file mode 100644
index 00000000000..7ea26de68eb
--- /dev/null
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCFprTransferHelper.java
@@ -0,0 +1,73 @@
+/*
+ * 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 java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fortify.cli.aviator.config.IAviatorLogger;
+import com.fortify.cli.common.progress.helper.IProgressWriter;
+import com.fortify.cli.ssc._common.rest.ssc.SSCUrls;
+import com.fortify.cli.ssc._common.rest.ssc.transfer.SSCFileTransferHelper;
+import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor;
+import com.fortify.cli.ssc.artifact.helper.SSCArtifactDescriptor;
+
+import kong.unirest.UnirestInstance;
+
+/**
+ * Shared SSC transfer operations for artifact-specific DAST FPR workflows.
+ */
+public final class AviatorSSCFprTransferHelper {
+ private AviatorSSCFprTransferHelper() {}
+
+ public static Path downloadArtifactFpr(
+ UnirestInstance unirest,
+ SSCArtifactDescriptor artifact,
+ IAviatorLogger logger,
+ IProgressWriter progressWriter) throws IOException {
+ Path fprPath = Files.createTempFile("aviator_" + artifact.getId() + "_", ".fpr");
+ try {
+ logger.progress("Status: Downloading FPR from SSC (artifact id=%s)", artifact.getId());
+ SSCFileTransferHelper.download(
+ unirest,
+ SSCUrls.DOWNLOAD_ARTIFACT(artifact.getId(), true),
+ fprPath.toFile(),
+ SSCFileTransferHelper.ISSCAddDownloadTokenFunction.ROUTEPARAM_DOWNLOADTOKEN,
+ progressWriter);
+ return fprPath;
+ } catch (RuntimeException e) {
+ try {
+ Files.deleteIfExists(fprPath);
+ } catch (IOException cleanupException) {
+ e.addSuppressed(cleanupException);
+ }
+ throw e;
+ }
+ }
+
+ public static String uploadDastFpr(
+ UnirestInstance unirest,
+ SSCAppVersionDescriptor appVersion,
+ Path dastFpr,
+ IProgressWriter progressWriter) {
+ JsonNode uploadResponse = SSCFileTransferHelper.restUpload(
+ unirest,
+ SSCUrls.PROJECT_VERSION_ARTIFACTS(appVersion.getVersionId()),
+ dastFpr.toFile(),
+ JsonNode.class,
+ progressWriter);
+ return uploadResponse.path("data").path("id").asText("UPLOAD_FAILED");
+ }
+}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator/src/main/resources/com/fortify/cli/aviator/i18n/AviatorMessages.properties b/fcli-core/fcli-aviator/src/main/resources/com/fortify/cli/aviator/i18n/AviatorMessages.properties
index 577c89836aa..9c04c3ba173 100644
--- a/fcli-core/fcli-aviator/src/main/resources/com/fortify/cli/aviator/i18n/AviatorMessages.properties
+++ b/fcli-core/fcli-aviator/src/main/resources/com/fortify/cli/aviator/i18n/AviatorMessages.properties
@@ -142,6 +142,7 @@ fcli.aviator.ssc.audit.usage.description = Downloads the FPR from an SSC applica
command to wait until the audited FPR file has been processed by SSC.
fcli.aviator.ssc.audit.app = Fortify Aviator application name to associate with the audit. If not provided, the SAST/FPR Build ID of the SSC application is used.
fcli.aviator.ssc.audit.tag-mapping = Custom tag mapping for audit results.
+fcli.aviator.ssc.audit-dast.tag-mapping = Custom tag mapping for DAST audit results.
fcli.aviator.ssc.audit.filterset = Name or ID of the FilterSet to apply.
fcli.aviator.ssc.audit.no-filterset = Do not apply any filter sets, including the default enabled filter set from the FPR.
fcli.aviator.ssc.audit.folder = Filter issues by a comma-separated list of specific folder names from the selected FilterSet (e.g., 'Hot,Critical'). This option requires a FilterSet to be active.
@@ -153,6 +154,12 @@ fcli.aviator.ssc.audit.refresh = By default, this command will refresh the sour
Note that for large applications this can lead to an error if the timeout expires.
fcli.aviator.ssc.audit.refresh-timeout = Time-out, for example 30s (30 seconds), 5m (5 minutes), 1h (1 hour). Default value: ${DEFAULT-VALUE}
+fcli.aviator.ssc.audit-dast.usage.header = Audit DAST findings in an SSC application version using Fortify Aviator.
+fcli.aviator.ssc.audit-dast.usage.description = Downloads the latest DAST FPR from SSC, audits eligible WebInspect findings, \
+ writes decisions to audit.xml, and uploads the changed DAST FPR. This command requires an active Fortify Aviator user session.
+fcli.aviator.ssc.audit-dast.app = Fortify Aviator application name to associate with the DAST audit. \
+ If omitted, the SSC application name is used.
+
fcli.aviator.ssc.apply-remediations.usage.header = Apply auto-remediations from a Fortify Remediation Aviator-processed artifact to source code.
fcli.aviator.ssc.apply-remediations.usage.description = Downloads FPR artifact(s) from SSC, or reads a local remediations cache zip, and applies Fortify Remediation Aviator-generated remediations to the specified source directory. \
Exactly one of --from-cache, --artifact-id, --latest, or --all must be specified. Online selection requires an active SSC session; --from-cache does not. \
@@ -226,6 +233,7 @@ fcli.env.default.prefix=FCLI_DEFAULT
fcli.aviator.session.output.table.args = name,type,url,created,expires,expired
fcli.aviator.admin-config.output.table.args = name,type,url,created
fcli.aviator.ssc.audit.output.table.args = id,application.name,name,artifactId,action
+fcli.aviator.ssc.audit-dast.output.table.args = id,application.name,name,artifactId,action
fcli.aviator.app.create.output.table.args = id,name,entitlement_id,disclaimer,quota_last_updated,quota
fcli.aviator.app.add-entitlement.output.table.args = id,name,entitlement_id,entitlement_multiplier,entitlements_consumed,quota_last_updated,quota
fcli.aviator.app.delete.output.table.args = message
diff --git a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommandTest.java b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommandTest.java
new file mode 100644
index 00000000000..cadbfea97ac
--- /dev/null
+++ b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommandTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.cli.cmd;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+import com.fortify.cli.aviator.audit.DastAuditFprResult;
+import com.fortify.cli.aviator.ssc.helper.AviatorSSCAuditHelper;
+import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor;
+
+import picocli.CommandLine;
+
+class AviatorSSCDastAuditCommandTest {
+ @Test
+ void rejectsClientSideToneImprovementOptions() {
+ assertThrows(CommandLine.ParameterException.class, () -> parse("--improve-tone"));
+ assertThrows(CommandLine.ParameterException.class, () -> parse("--no-improve-tone"));
+ }
+
+ @Test
+ void acceptsCustomTagMapping() {
+ var commandLine = new CommandLine(new AviatorSSCDastAuditCommand());
+
+ var parseResult = commandLine.parseArgs("--av", "test:1.0", "--tag-mapping", "dast-tags.yaml");
+
+ assertEquals("dast-tags.yaml", parseResult.matchedOptionValue("--tag-mapping", null));
+ }
+
+ @Test
+ void dastAuditStatsUseSastAuditOutputEnvelope() {
+ var appVersion = new SSCAppVersionDescriptor();
+ appVersion.setVersionId("42");
+ appVersion.setApplicationName("WebGoat");
+ appVersion.setVersionName("1.0");
+ var auditResult = new DastAuditFprResult(
+ null, "PARTIALLY_AUDITED", null, 8, 6, 6, 4,
+ 2, 1, 1, 2, 0, 6, 2, false, null, null);
+
+ var result = AviatorSSCAuditHelper.buildResultNode(appVersion, "2786", auditResult.status());
+ AviatorSSCAuditHelper.setDastAuditStats(result, auditResult);
+
+ assertEquals("42", result.path("id").asText());
+ assertEquals("WebGoat", result.path("applicationName").asText());
+ assertEquals("2786", result.path("artifactId").asText());
+ assertEquals("PARTIALLY_AUDITED", result.path("__action__").asText());
+ assertEquals(6, result.path("operation").path("audit").path("submitted").asInt());
+ assertEquals(4, result.path("operation").path("audit").path("succeeded").asInt());
+ assertEquals(2, result.path("operation").path("audit").path("skipped").asInt());
+ assertEquals(0, result.path("operation").path("audit").path("failed").asInt());
+ assertFalse(result.path("operation").path("audit").has("truePositives"));
+ assertFalse(result.path("operation").path("audit").has("falsePositivesSuppressed"));
+ assertFalse(result.path("operation").path("audit").has("likelyFalsePositives"));
+ assertFalse(result.has("state"));
+ assertFalse(result.has("submitted"));
+ }
+
+ private static AviatorSSCDastAuditCommand parse(String... args) {
+ var command = new AviatorSSCDastAuditCommand();
+ var fullArgs = new ArrayList();
+ Collections.addAll(fullArgs, "--av", "test:1.0");
+ Collections.addAll(fullArgs, args);
+ new CommandLine(command).parseArgs(fullArgs.toArray(String[]::new));
+ return command;
+ }
+}
\ No newline at end of file