From 78f5b4a1c0cb518de85c441d1a31dafb09f96058 Mon Sep 17 00:00:00 2001
From: Ankit Rathod
Date: Fri, 21 Aug 2026 15:03:52 +0530
Subject: [PATCH 1/2] 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
From 2a9ab696c89996bf90cb35b14f69b306b68760fe Mon Sep 17 00:00:00 2001
From: Ankit Rathod
Date: Thu, 27 Aug 2026 17:41:20 +0530
Subject: [PATCH 2/2] feat: Introduce SAST audit command and refactor DAST
audit command
- Added `AviatorSSCSastAuditCommand` for auditing SAST findings in SSC applications.
- Updated `AviatorSSCDastAuditCommand` to return status names instead of enums.
- Refactored DAST audit result handling to use the new status name approach.
- Enhanced error handling for FPR uploads in `AviatorSSCFprTransferHelper`.
- Updated internationalization properties to include new SAST command and deprecate the old audit command.
- Added unit tests for the new SAST audit command and refactored existing tests for DAST audit command.
- Updated bulk audit YAML configuration to use the new SAST audit command.
---
.../fcli-app/aviator/resource-config.json | 3 -
.../fcli/fcli-app/grpc/reflect-config.json | 9 +-
.../fcli/fcli-app/yaml/reflect-config.json | 9 +-
.../fortify/cli/NativeReflectConfigTest.java | 1 +
.../_common/config/AviatorConfigManager.java | 9 +-
.../fortify/cli/aviator/audit/AuditFPR.java | 3 +-
.../audit/DastAuditDecisionMapper.java | 7 +-
.../cli/aviator/audit/DastAuditFPR.java | 104 +++--
.../cli/aviator/audit/DastAuditFprResult.java | 5 +-
.../cli/aviator/audit/DastAuditFprStatus.java | 21 +
.../cli/aviator/config/TagMappingConfig.java | 41 ++
.../aviator/fpr/processor/AuditProcessor.java | 4 +-
.../aviator/grpc/DastAuditResponseMapper.java | 43 +-
.../cli/aviator/grpc/DastAuditResult.java | 5 +
.../aviator/grpc/DastAuditStreamConfig.java | 3 +
.../grpc/DastAuditStreamProcessor.java | 25 +-
.../aviator/grpc/DastAuditStreamResult.java | 9 +-
.../resources/default_dast_tag_mapping.yaml | 24 --
.../main/resources/default_tag_mapping.yaml | 9 +
.../audit/DastAuditDecisionMapperTest.java | 18 +-
.../cli/aviator/audit/DastAuditFPRTest.java | 58 ++-
.../aviator/config/TagMappingConfigTest.java | 73 ++++
.../grpc/DastAuditStreamConfigTest.java | 12 +-
.../grpc/DastAuditStreamResultTest.java | 42 ++
.../ssc/cli/cmd/AviatorSSCAuditCommand.java | 355 +----------------
.../ssc/cli/cmd/AviatorSSCCommands.java | 1 +
.../cli/cmd/AviatorSSCDastAuditCommand.java | 19 +-
.../cli/cmd/AviatorSSCSastAuditCommand.java | 370 ++++++++++++++++++
.../ssc/helper/AviatorSSCAuditHelper.java | 10 +-
.../helper/AviatorSSCFprTransferHelper.java | 12 +-
.../aviator/i18n/AviatorMessages.properties | 24 +-
.../cli/cmd/AviatorSSCAuditCommandTest.java | 36 +-
.../cmd/AviatorSSCDastAuditCommandTest.java | 19 +-
.../AviatorSSCFprTransferHelperTest.java | 40 ++
.../cli/ssc/actions/zip/bulkaudit.yaml | 12 +-
35 files changed, 942 insertions(+), 493 deletions(-)
create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFprStatus.java
delete 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/grpc/DastAuditStreamResultTest.java
create mode 100644 fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCSastAuditCommand.java
create mode 100644 fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCFprTransferHelperTest.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 9db67cb8605..5d73d4c3157 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,9 +10,6 @@
{
"pattern": "default_tag_mapping.yaml"
},
- {
- "pattern": "default_dast_tag_mapping.yaml"
- },
{
"pattern": "remediations.xsd"
},
diff --git a/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/grpc/reflect-config.json b/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/grpc/reflect-config.json
index 8a1c339eb0d..fa3fa4e7f0a 100644
--- a/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/grpc/reflect-config.json
+++ b/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/grpc/reflect-config.json
@@ -600,7 +600,7 @@
"name":"com.fortify.cli.aviator.config.TagMappingConfig",
"allDeclaredFields":true,
"queryAllPublicMethods":true,
- "methods":[{"name":"","parameterTypes":[] }, {"name":"setMapping","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$Mapping"] }, {"name":"setSuppression_exclusions","parameterTypes":["java.util.List"] }, {"name":"setTag_id","parameterTypes":["java.lang.String"] }]
+ "methods":[{"name":"","parameterTypes":[] }, {"name":"setDast","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$ProductMapping"] }, {"name":"setMapping","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$Mapping"] }, {"name":"setSast","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$ProductMapping"] }, {"name":"setSuppression_exclusions","parameterTypes":["java.util.List"] }, {"name":"setTag_id","parameterTypes":["java.lang.String"] }]
},
{
"name":"com.fortify.cli.aviator.config.TagMappingConfig$Mapping",
@@ -631,6 +631,13 @@
{
"name":"com.fortify.cli.aviator.config.TagMappingConfig$MappingCustomizer"
},
+ {
+ "name":"com.fortify.cli.aviator.config.TagMappingConfig$ProductMapping",
+ "allDeclaredConstructors":true,
+ "allDeclaredFields":true,
+ "queryAllPublicMethods":true,
+ "methods":[{"name":"","parameterTypes":[] }, {"name":"setMapping","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$Mapping"] }, {"name":"setSuppression_exclusions","parameterTypes":["java.util.List"] }, {"name":"setTag_id","parameterTypes":["java.lang.String"] }]
+ },
{
"name":"com.fortify.cli.aviator.config.TagMappingConfig$Result",
"allDeclaredConstructors": true,
diff --git a/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/yaml/reflect-config.json b/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/yaml/reflect-config.json
index d42d5a366a6..421d583441c 100644
--- a/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/yaml/reflect-config.json
+++ b/fcli-core/fcli-app/src/main/resources/META-INF/native-image/fcli/fcli-app/yaml/reflect-config.json
@@ -43,7 +43,7 @@
"name":"com.fortify.cli.aviator.config.TagMappingConfig",
"allDeclaredFields":true,
"queryAllPublicMethods":true,
- "methods":[{"name":"","parameterTypes":[] }, {"name":"setMapping","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$Mapping"] }, {"name":"setSuppression_exclusions","parameterTypes":["java.util.List"] }, {"name":"setTag_id","parameterTypes":["java.lang.String"] }]
+ "methods":[{"name":"","parameterTypes":[] }, {"name":"setDast","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$ProductMapping"] }, {"name":"setMapping","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$Mapping"] }, {"name":"setSast","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$ProductMapping"] }, {"name":"setSuppression_exclusions","parameterTypes":["java.util.List"] }, {"name":"setTag_id","parameterTypes":["java.lang.String"] }]
},
{
"name":"com.fortify.cli.aviator.config.TagMappingConfig$Mapping",
@@ -74,6 +74,13 @@
{
"name":"com.fortify.cli.aviator.config.TagMappingConfig$MappingCustomizer"
},
+ {
+ "name":"com.fortify.cli.aviator.config.TagMappingConfig$ProductMapping",
+ "allDeclaredConstructors":true,
+ "allDeclaredFields":true,
+ "queryAllPublicMethods":true,
+ "methods":[{"name":"","parameterTypes":[] }, {"name":"setMapping","parameterTypes":["com.fortify.cli.aviator.config.TagMappingConfig$Mapping"] }, {"name":"setSuppression_exclusions","parameterTypes":["java.util.List"] }, {"name":"setTag_id","parameterTypes":["java.lang.String"] }]
+ },
{
"name":"com.fortify.cli.aviator.config.TagMappingConfig$Result",
"allDeclaredConstructors": true,
diff --git a/fcli-core/fcli-app/src/test/java/com/fortify/cli/NativeReflectConfigTest.java b/fcli-core/fcli-app/src/test/java/com/fortify/cli/NativeReflectConfigTest.java
index 671e032646f..c61a672e005 100644
--- a/fcli-core/fcli-app/src/test/java/com/fortify/cli/NativeReflectConfigTest.java
+++ b/fcli-core/fcli-app/src/test/java/com/fortify/cli/NativeReflectConfigTest.java
@@ -38,6 +38,7 @@ class NativeReflectConfigTest {
private static final List TAG_MAPPING_NESTED_CLASSES = List.of(
"com.fortify.cli.aviator.config.TagMappingConfig$SuppressionExclusion",
"com.fortify.cli.aviator.config.TagMappingConfig$Mapping",
+ "com.fortify.cli.aviator.config.TagMappingConfig$ProductMapping",
"com.fortify.cli.aviator.config.TagMappingConfig$Tier",
"com.fortify.cli.aviator.config.TagMappingConfig$Result");
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 236a42b18f4..e231af243f4 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,7 +28,6 @@ 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();
@@ -36,14 +35,12 @@ 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);
@@ -96,10 +93,6 @@ public TagMappingConfig getDefaultTagMappingConfig() {
}
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;
+ return getDefaultTagMappingConfig();
}
}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/AuditFPR.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/AuditFPR.java
index 4571af0bc7f..5e7935a00c2 100644
--- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/AuditFPR.java
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/AuditFPR.java
@@ -108,8 +108,7 @@ private static TagMappingConfig loadTagMappingConfig(String tagMappingFilePath)
tagMappingConfig = AviatorConfigManager.getInstance().getDefaultTagMappingConfig();
}
- tagMappingConfig.validate();
- return tagMappingConfig;
+ return tagMappingConfig.resolveForSast();
}
private static Map buildIssueCategoryLookup(List vulnerabilities) {
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
index 60ddfb92b13..d18a7a696ab 100644
--- 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
@@ -12,6 +12,8 @@
*/
package com.fortify.cli.aviator.audit;
+import java.util.Locale;
+
import com.fortify.cli.aviator.audit.model.AuditResponse;
import com.fortify.cli.aviator.audit.model.AuditResult;
import com.fortify.cli.aviator.grpc.DastAuditResult;
@@ -65,8 +67,9 @@ public static AuditResponse toAuditResponse(DastAuditResult result) {
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);
+ String normalized = confidence.toUpperCase(Locale.ROOT);
+ return switch (normalized) {
+ case "HIGH", "MEDIUM", "LOW" -> normalized;
default -> "LOW";
};
}
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
index 69dcbe49e38..d904461235e 100644
--- 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
@@ -13,6 +13,7 @@
package com.fortify.cli.aviator.audit;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -24,15 +25,19 @@
import com.fortify.cli.aviator.audit.model.AuditResponse;
import com.fortify.cli.aviator.config.TagMappingConfig;
+import com.fortify.cli.aviator.dast.DastSession;
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.DastAuditResult;
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;
+import lombok.Builder;
+
/**
* Coordinates parsing, filtering, server auditing, and DAST audit.xml updates.
*/
@@ -41,12 +46,43 @@ public final class DastAuditFPR {
private DastAuditFPR() {}
+ @Builder
private record EligibilityResult(
List workItems,
int missingId,
int duplicate,
int suppressed,
- int processed) {}
+ int alreadyProcessed) {
+ private static class EligibilityResultBuilder {
+ private List workItems;
+
+ private EligibilityResultBuilder addWorkItem(DastAuditWorkItem workItem) {
+ if (workItems == null) workItems = new ArrayList<>();
+ workItems.add(workItem);
+ return this;
+ }
+
+ private EligibilityResultBuilder incrementMissingId() {
+ missingId++;
+ return this;
+ }
+
+ private EligibilityResultBuilder incrementDuplicate() {
+ duplicate++;
+ return this;
+ }
+
+ private EligibilityResultBuilder incrementSuppressed() {
+ suppressed++;
+ return this;
+ }
+
+ private EligibilityResultBuilder incrementAlreadyProcessed() {
+ alreadyProcessed++;
+ return this;
+ }
+ }
+ }
@FunctionalInterface
public interface StreamRunner {
@@ -70,7 +106,7 @@ public static DastAuditFprResult audit(
LOG.info("DAST audit eligibility: reported={}, eligible={}, skipped={} "
+ "(missingId={}, duplicate={}, suppressed={}, alreadyProcessed={})",
totalReported, workItems.size(), locallySkipped, eligibility.missingId(),
- eligibility.duplicate(), eligibility.suppressed(), eligibility.processed());
+ eligibility.duplicate(), eligibility.suppressed(), eligibility.alreadyProcessed());
if (workItems.isEmpty()) {
LOG.info("DAST audit skipped because no eligible findings remain");
@@ -84,14 +120,14 @@ public static DastAuditFprResult audit(
int likelyFalsePositives = 0;
int failed = 0;
int serverSkipped = 0;
- Set respondedIssueIds = new java.util.HashSet<>();
+ Set respondedIssueIds = new 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;
+ var success = (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())) {
@@ -126,15 +162,28 @@ public static DastAuditFprResult audit(
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";
+ DastAuditFprStatus status = succeeded == workItems.size() ? DastAuditFprStatus.AUDITED
+ : succeeded > 0 ? DastAuditFprStatus.PARTIALLY_AUDITED : DastAuditFprStatus.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());
+ return DastAuditFprResult.builder()
+ .updatedFile(updatedFile)
+ .status(status)
+ .message(message)
+ .totalReported(totalReported)
+ .eligible(workItems.size())
+ .submitted(workItems.size())
+ .succeeded(succeeded)
+ .truePositives(truePositives)
+ .falsePositivesSuppressed(falsePositivesSuppressed)
+ .likelyFalsePositives(likelyFalsePositives)
+ .skipped(locallySkipped + serverSkipped)
+ .failed(failed)
+ .reservedQuota(streamResult.reservedQuota())
+ .exceededCount(streamResult.exceededCount())
+ .unlimitedQuota(streamResult.unlimitedQuota())
+ .quotaLastUpdated(streamResult.quotaLastUpdated())
+ .nextQuotaUpdateMessage(streamResult.nextQuotaUpdateMessage())
+ .build();
}
private static boolean isSuppressedFalsePositive(AuditResponse response, TagMappingConfig tagMappingConfig) {
@@ -144,42 +193,38 @@ private static boolean isSuppressedFalsePositive(AuditResponse response, TagMapp
}
private static EligibilityResult eligibleWorkItems(
- List sessions,
+ 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;
+ var result = EligibilityResult.builder();
+ var seenIssueIds = new HashSet();
for (var session : sessions) {
for (var issue : session.getIssues()) {
String issueId = issue.getId();
if (issueId == null || issueId.isBlank()) {
- missingId++;
+ result.incrementMissingId();
LOG.debug("Skipping DAST finding without an issue ID in session {}", session.getRequestId());
continue;
}
if (!seenIssueIds.add(issueId)) {
- duplicate++;
+ result.incrementDuplicate();
LOG.debug("Skipping duplicate DAST issue {} in session {}", issueId, session.getRequestId());
continue;
}
AuditIssue auditIssue = auditIssues.get(issueId);
if (auditIssue != null && auditIssue.isSuppressed()) {
- suppressed++;
+ result.incrementSuppressed();
LOG.debug("Skipping DAST issue {} because it is already suppressed", issueId);
continue;
}
if (auditIssue != null && isProcessedByAviator(auditIssue)) {
- processed++;
+ result.incrementAlreadyProcessed();
LOG.debug("Skipping DAST issue {} because it is already processed by Aviator", issueId);
continue;
}
- workItems.add(new DastAuditWorkItem(session, issue));
+ result.addWorkItem(new DastAuditWorkItem(session, issue));
}
}
- return new EligibilityResult(List.copyOf(workItems), missingId, duplicate, suppressed, processed);
+ return result.build();
}
private static boolean isProcessedByAviator(AuditIssue auditIssue) {
@@ -188,8 +233,11 @@ private static boolean isProcessedByAviator(AuditIssue auditIssue) {
}
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);
+ return DastAuditFprResult.builder()
+ .status(DastAuditFprStatus.SKIPPED)
+ .message("No eligible DAST findings to audit")
+ .totalReported(totalReported)
+ .skipped(skipped)
+ .build();
}
}
\ 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
index 5f7ab46709b..005932f3059 100644
--- 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
@@ -14,12 +14,15 @@
import java.io.File;
+import lombok.Builder;
+
/**
* Summary of processing one DAST FPR.
*/
+@Builder
public record DastAuditFprResult(
File updatedFile,
- String status,
+ DastAuditFprStatus status,
String message,
int totalReported,
int eligible,
diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFprStatus.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFprStatus.java
new file mode 100644
index 00000000000..09c9e385754
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/DastAuditFprStatus.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.audit;
+
+/** Outcome of processing a DAST FPR. */
+public enum DastAuditFprStatus {
+ AUDITED,
+ PARTIALLY_AUDITED,
+ SKIPPED,
+ FAILED
+}
\ 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 b6b34b329a9..f5f033707c8 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
@@ -34,6 +34,8 @@ public class TagMappingConfig {
private String tag_id = "87f2364f-dcd4-49e6-861d-f8d3f351686b";
private List suppression_exclusions = new ArrayList<>();
private Mapping mapping;
+ private ProductMapping sast;
+ private ProductMapping dast;
public void setSuppression_exclusions(List suppression_exclusions) {
this.suppression_exclusions = suppression_exclusions == null ? new ArrayList<>() : suppression_exclusions;
@@ -69,6 +71,38 @@ public void validateForDast() {
}
}
+ public TagMappingConfig resolveForSast() {
+ TagMappingConfig resolved = resolve(sast);
+ resolved.validate();
+ return resolved;
+ }
+
+ public TagMappingConfig resolveForDast() {
+ TagMappingConfig resolved = resolve(dast);
+ resolved.validateForDast();
+ return resolved;
+ }
+
+ private TagMappingConfig resolve(ProductMapping productMapping) {
+ TagMappingConfig resolved = new TagMappingConfig();
+ resolved.setTag_id(productMapping != null && productMapping.getTag_id() != null
+ ? productMapping.getTag_id()
+ : tag_id);
+ resolved.setMapping(productMapping != null && productMapping.getMapping() != null
+ ? productMapping.getMapping()
+ : mapping);
+ List productExclusions = productMapping == null
+ ? null
+ : productMapping.getSuppression_exclusions();
+ List effectiveExclusions = productExclusions != null
+ ? productExclusions
+ : suppression_exclusions;
+ resolved.setSuppression_exclusions(effectiveExclusions == null
+ ? Collections.emptyList()
+ : new ArrayList<>(effectiveExclusions));
+ return resolved;
+ }
+
public boolean hasSuppressionExclusions() {
return suppression_exclusions != null
&& suppression_exclusions.stream()
@@ -301,6 +335,13 @@ public static class Mapping {
private Tier tier_2;
}
+ @Data @Reflectable
+ public static class ProductMapping {
+ private String tag_id;
+ private List suppression_exclusions;
+ private Mapping mapping;
+ }
+
public enum ResultType {
FP,
TP,
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 dcf29e450b2..7b5ee12628a 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
@@ -766,9 +766,9 @@ public File updateAndSaveAuditAndRemediationsXml(Map audi
/**
* Applies DAST audit decisions to audit.xml without creating SAST remediation artifacts.
*/
- public File updateAndSaveDastAuditXml(Map auditResponses,
+ public File updateAndSaveDastAuditXml(Map auditResponses,
TagMappingConfig tagMappingConfig) {
- Set writtenInstanceIds = new HashSet<>();
+ Set writtenInstanceIds = new HashSet<>();
for (Map.Entry entry : auditResponses.entrySet()) {
AuditResponse response = entry.getValue();
if (response == null || !"SUCCESS".equalsIgnoreCase(response.getStatus())
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
index 3cd45debf81..a9d8c27780c 100644
--- 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
@@ -23,28 +23,45 @@ 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 + "'");
+ return DastAuditResult.Failure.builder()
+ .issueId(expectedIssueId)
+ .status("FAILED")
+ .statusMessage("DAST audit response issue ID mismatch: expected '" + expectedIssueId
+ + "' but received '" + responseIssueId + "'")
+ .build();
}
if ("SKIPPED".equalsIgnoreCase(response.getStatus())) {
- return new DastAuditResult.Skipped(expectedIssueId, response.getStatusMessage());
+ return DastAuditResult.Skipped.builder()
+ .issueId(expectedIssueId)
+ .statusMessage(response.getStatusMessage())
+ .build();
}
if (!"SUCCESS".equalsIgnoreCase(response.getStatus())) {
- return new DastAuditResult.Failure(
- expectedIssueId, response.getStatus(), response.getStatusMessage());
+ return DastAuditResult.Failure.builder()
+ .issueId(expectedIssueId)
+ .status(response.getStatus())
+ .statusMessage(response.getStatusMessage())
+ .build();
}
if (!response.hasDecision()) {
- return new DastAuditResult.Failure(
- expectedIssueId, "FAILED", "Successful DAST audit response did not contain a decision");
+ return DastAuditResult.Failure.builder()
+ .issueId(expectedIssueId)
+ .status("FAILED")
+ .statusMessage("Successful DAST audit response did not contain a decision")
+ .build();
}
var decision = response.getDecision();
- return new DastAuditResult.Success(
- expectedIssueId, decision.getTruePositive(), decision.getConfidence(),
- decision.getReasoning(), decision.getRemediationAdvice(), decision.getFinalComment(),
- decision.getTagValue(), decision.getTier());
+ return DastAuditResult.Success.builder()
+ .issueId(expectedIssueId)
+ .truePositive(decision.getTruePositive())
+ .confidence(decision.getConfidence())
+ .reasoning(decision.getReasoning())
+ .remediationAdvice(decision.getRemediationAdvice())
+ .finalComment(decision.getFinalComment())
+ .tagValue(decision.getTagValue())
+ .tier(decision.getTier())
+ .build();
}
}
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
index b4f99f89833..cd2b0fe8ea8 100644
--- 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
@@ -12,12 +12,15 @@
*/
package com.fortify.cli.aviator.grpc;
+import lombok.Builder;
+
/** 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();
+ @Builder
record Success(
String issueId,
boolean truePositive,
@@ -39,6 +42,7 @@ public String statusMessage() {
}
}
+ @Builder
record Skipped(String issueId, String statusMessage) implements DastAuditResult {
@Override
public String status() {
@@ -46,5 +50,6 @@ public String status() {
}
}
+ @Builder
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
index 031897beb02..83c21da10d4 100644
--- 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
@@ -14,9 +14,12 @@
import com.fortify.cli.aviator._common.exception.AviatorSimpleException;
+import lombok.Builder;
+
/**
* Configuration for one DAST audit gRPC stream.
*/
+@Builder
public record DastAuditStreamConfig(
String token,
String applicationName,
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
index 7d77ed384a8..c830ce7949c 100644
--- 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
@@ -13,6 +13,7 @@
package com.fortify.cli.aviator.grpc;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -78,7 +79,7 @@ public class DastAuditStreamProcessor implements AutoCloseable {
private int totalReportedIssues;
private int lastRetryCompletedCount;
private CompletableFuture resultFuture;
- private final List results = java.util.Collections.synchronizedList(new ArrayList<>());
+ private final List results = Collections.synchronizedList(new ArrayList<>());
private final Map requestIssueIds = new ConcurrentHashMap<>();
private final Map requestIdsByIssue = new ConcurrentHashMap<>();
private final Set completedRequestIds = ConcurrentHashMap.newKeySet();
@@ -244,8 +245,15 @@ private void handleError(DastAuditErrorResponse response) {
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());
+ ? DastAuditResult.Skipped.builder()
+ .issueId(issueId)
+ .statusMessage(response.getStatusMessage())
+ .build()
+ : DastAuditResult.Failure.builder()
+ .issueId(issueId)
+ .status(response.getStatus())
+ .statusMessage(response.getStatusMessage())
+ .build();
results.add(result);
logger.progress("Audited %d of %d DAST findings", results.size(), workItems.size());
completeRequestsIfDone();
@@ -445,9 +453,14 @@ private void completeExceptionally(RuntimeException exception) {
private void completeSuccessfully() {
if (!resultFuture.isDone()) {
- resultFuture.complete(new DastAuditStreamResult(
- List.copyOf(results), reservedQuota, exceededCount, unlimitedQuota,
- quotaLastUpdated, nextQuotaUpdateMessage));
+ resultFuture.complete(DastAuditStreamResult.builder()
+ .results(List.copyOf(results))
+ .reservedQuota(reservedQuota)
+ .exceededCount(exceededCount)
+ .unlimitedQuota(unlimitedQuota)
+ .quotaLastUpdated(quotaLastUpdated)
+ .nextQuotaUpdateMessage(nextQuotaUpdateMessage)
+ .build());
}
}
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
index c504dd5bf29..f114a495cb9 100644
--- 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
@@ -14,9 +14,12 @@
import java.util.List;
+import lombok.Builder;
+
/**
* Results and quota metadata returned by one DAST audit stream.
*/
+@Builder
public record DastAuditStreamResult(
List results,
int reservedQuota,
@@ -24,4 +27,8 @@ public record DastAuditStreamResult(
boolean unlimitedQuota,
String quotaLastUpdated,
String nextQuotaUpdateMessage
-) {}
\ No newline at end of file
+) {
+ public DastAuditStreamResult {
+ results = results == null ? List.of() : List.copyOf(results);
+ }
+}
\ 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
deleted file mode 100644
index 5283eadff4c..00000000000
--- a/fcli-core/fcli-aviator-common/src/main/resources/default_dast_tag_mapping.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-# 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/main/resources/default_tag_mapping.yaml b/fcli-core/fcli-aviator-common/src/main/resources/default_tag_mapping.yaml
index cbd39912eb4..320b96d6d63 100644
--- a/fcli-core/fcli-aviator-common/src/main/resources/default_tag_mapping.yaml
+++ b/fcli-core/fcli-aviator-common/src/main/resources/default_tag_mapping.yaml
@@ -10,6 +10,15 @@ tag_id: "87f2364f-dcd4-49e6-861d-f8d3f351686b"
# suppression_exclusions:
# - categories:
# - "Privacy Violation"
+# Optional sast and dast sections may override tag_id, mapping, or
+# suppression_exclusions for each product. Existing flat files remain valid.
+# sast:
+# suppression_exclusions:
+# - categories:
+# - "Privacy Violation"
+# dast:
+# tag_id: "dast-specific-tag-id"
+# mapping: ...
mapping:
tier_1:
fp:
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
index d46133d3cd8..2ead662bedf 100644
--- 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
@@ -22,8 +22,14 @@
class DastAuditDecisionMapperTest {
@Test
void unknownConfidenceFalsePositiveRemainsUnsuppressed() {
- var result = new DastAuditResult.Success(
- "DAST-1", false, "UNKNOWN", "reason", "", "comment", "bad", "GOLD");
+ var result = DastAuditResult.Success.builder()
+ .issueId("DAST-1")
+ .confidence("UNKNOWN")
+ .reasoning("reason")
+ .finalComment("comment")
+ .tagValue("bad")
+ .tier("GOLD")
+ .build();
var response = DastAuditDecisionMapper.toAuditResponse(result);
@@ -33,8 +39,12 @@ void unknownConfidenceFalsePositiveRemainsUnsuppressed() {
@Test
void highConfidenceFalsePositiveIsSuppressible() {
- var result = new DastAuditResult.Success(
- "DAST-1", false, "HIGH", "reason", "", "comment", "", "");
+ var result = DastAuditResult.Success.builder()
+ .issueId("DAST-1")
+ .confidence("HIGH")
+ .reasoning("reason")
+ .finalComment("comment")
+ .build();
var response = DastAuditDecisionMapper.toAuditResponse(result);
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
index 95d2ca4c800..a33c93b6f2d 100644
--- 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
@@ -42,17 +42,18 @@ class DastAuditFPRTest {
@Test
void auditsEligibleFindingAndWritesConservativeXml() throws Exception {
Path fpr = createFpr();
- var config = new DastAuditStreamConfig("token", "app", "ssc", "1", null);
+ var config = streamConfig();
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)));
+ CompletableFuture.completedFuture(DastAuditStreamResult.builder()
+ .results(List.of(successResult(false, "HIGH")))
+ .reservedQuota(1)
+ .build()));
}
- assertEquals("AUDITED", result.status());
+ assertEquals(DastAuditFprStatus.AUDITED, result.status());
assertEquals(1, result.falsePositivesSuppressed());
try (FileSystem zip = FileSystems.newFileSystem(fpr)) {
String auditXml = Files.readString(zip.getPath("/audit.xml"));
@@ -79,16 +80,17 @@ void customTagMappingControlsFinalTagAndSuppression() throws Exception {
tp: { value: "Review TP", suppress: false }
unsure: { suppress: false }
""");
- var config = new DastAuditStreamConfig("token", "app", "ssc", "1", null);
+ var config = streamConfig();
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)));
+ CompletableFuture.completedFuture(DastAuditStreamResult.builder()
+ .results(List.of(successResult(false, "MEDIUM")))
+ .reservedQuota(1)
+ .build()));
}
assertEquals(1, result.falsePositivesSuppressed());
@@ -111,13 +113,14 @@ void writesOnlyIssuesUpdatedByCurrentAudit() throws Exception {
""");
}
- var config = new DastAuditStreamConfig("token", "app", "ssc", "1", null);
+ var config = streamConfig();
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)));
+ CompletableFuture.completedFuture(DastAuditStreamResult.builder()
+ .results(List.of(successResult(true, "HIGH")))
+ .reservedQuota(1)
+ .build()));
}
try (FileSystem zip = FileSystems.newFileSystem(fpr)) {
@@ -130,23 +133,44 @@ void writesOnlyIssuesUpdatedByCurrentAudit() throws Exception {
@Test
void missingTerminalResponseIsCountedAsFailure() throws Exception {
Path fpr = createFpr();
- var config = new DastAuditStreamConfig("token", "app", "ssc", "1", null);
+ var config = streamConfig();
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)));
+ CompletableFuture.completedFuture(DastAuditStreamResult.builder()
+ .results(List.of())
+ .reservedQuota(1)
+ .build()));
- assertEquals("FAILED", result.status());
+ assertEquals(DastAuditFprStatus.FAILED, result.status());
assertEquals(1, result.failed());
}
}
+ private DastAuditResult.Success successResult(boolean truePositive, String confidence) {
+ return DastAuditResult.Success.builder()
+ .issueId("DAST-1")
+ .truePositive(truePositive)
+ .confidence(confidence)
+ .reasoning("reason")
+ .finalComment("comment")
+ .build();
+ }
+
private TagMappingConfig defaultTagMapping() {
return AviatorConfigManager.getInstance().getDefaultDastTagMappingConfig();
}
+ private DastAuditStreamConfig streamConfig() {
+ return DastAuditStreamConfig.builder()
+ .token("token")
+ .applicationName("app")
+ .sscApplicationName("ssc")
+ .sscApplicationVersion("1")
+ .build();
+ }
+
private Path createFpr() throws Exception {
Path fpr = tempDir.resolve("dast.fpr");
try (FileSystem zip = FileSystems.newFileSystem(fpr, Map.of("create", "true"))) {
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 c1ed01ee269..f68cd65f334 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
@@ -136,6 +136,79 @@ void testLoadYamlFileBindsSuppressionExclusionsAcrossEntries() throws Exception
assertTrue(config.isSuppressionExcluded(new TagMappingConfig.SuppressionExclusionContext("privacy violation")));
}
+ @Test
+ void testCombinedYamlResolvesProductOverridesAndSharedDefaults() throws Exception {
+ Path yamlFile = tempDir.resolve("combined-tag-mapping.yaml");
+ Files.writeString(yamlFile, """
+ tag_id: "shared-tag"
+ mapping:
+ tier_1:
+ fp: { value: "Shared FP", suppress: true }
+ tp: { value: "Shared TP", suppress: false }
+ unsure: { suppress: false }
+ tier_2:
+ fp: { value: "Shared FP", suppress: false }
+ tp: { value: "Shared TP", suppress: false }
+ unsure: { suppress: false }
+ sast:
+ suppression_exclusions:
+ - categories: ["Privacy Violation"]
+ dast:
+ tag_id: "dast-tag"
+ mapping:
+ tier_1:
+ fp: { value: "DAST FP", suppress: true }
+ tp: { value: "DAST TP", suppress: false }
+ unsure: { suppress: false }
+ tier_2:
+ fp: { value: "DAST FP", suppress: false }
+ tp: { value: "DAST TP", suppress: false }
+ unsure: { suppress: false }
+ """);
+
+ TagMappingConfig config = ResourceUtil.loadYamlFile(yamlFile.toFile(), TagMappingConfig.class);
+ TagMappingConfig sastConfig = config.resolveForSast();
+ TagMappingConfig dastConfig = config.resolveForDast();
+
+ assertEquals("shared-tag", sastConfig.getTag_id());
+ assertEquals(Set.of("Shared FP", "Shared TP"), sastConfig.getMappedValues());
+ assertTrue(sastConfig.isSuppressionExcluded(
+ new TagMappingConfig.SuppressionExclusionContext("Privacy Violation")));
+ assertEquals("dast-tag", dastConfig.getTag_id());
+ assertEquals(Set.of("DAST FP", "DAST TP"), dastConfig.getMappedValues());
+ assertFalse(dastConfig.hasSuppressionExclusions());
+ }
+
+ @Test
+ void testLegacyFlatConfigResolvesForBothProducts() {
+ TagMappingConfig config = createValidConfig();
+
+ assertEquals(config.getMapping(), config.resolveForSast().getMapping());
+ assertEquals(config.getMapping(), config.resolveForDast().getMapping());
+ }
+
+ @Test
+ void testNullSuppressionExclusionsResolveAsEmpty() throws Exception {
+ Path yamlFile = tempDir.resolve("null-exclusions-tag-mapping.yaml");
+ Files.writeString(yamlFile, """
+ suppression_exclusions: null
+ mapping:
+ tier_1:
+ fp: { suppress: true }
+ tp: { suppress: false }
+ unsure: { suppress: false }
+ tier_2:
+ fp: { suppress: false }
+ tp: { suppress: false }
+ unsure: { suppress: false }
+ """);
+
+ TagMappingConfig config = ResourceUtil.loadYamlFile(yamlFile.toFile(), TagMappingConfig.class);
+
+ assertFalse(config.resolveForSast().hasSuppressionExclusions());
+ assertFalse(config.resolveForDast().hasSuppressionExclusions());
+ }
+
@Test
void testResolvesResultsAndMappedValues() {
TagMappingConfig config = createValidConfig();
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
index 0cd16e80571..b3eefdb47f6 100644
--- 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
@@ -22,12 +22,20 @@ class DastAuditStreamConfigTest {
@Test
void rejectsBlankToken() {
assertThrows(AviatorSimpleException.class,
- () -> new DastAuditStreamConfig(" ", "app", "ssc", "1", null));
+ () -> validConfigBuilder().token(" ").build());
}
@Test
void rejectsBlankApplicationName() {
assertThrows(AviatorSimpleException.class,
- () -> new DastAuditStreamConfig("token", " ", "ssc", "1", null));
+ () -> validConfigBuilder().applicationName(" ").build());
+ }
+
+ private DastAuditStreamConfig.DastAuditStreamConfigBuilder validConfigBuilder() {
+ return DastAuditStreamConfig.builder()
+ .token("token")
+ .applicationName("app")
+ .sscApplicationName("ssc")
+ .sscApplicationVersion("1");
}
}
\ No newline at end of file
diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamResultTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamResultTest.java
new file mode 100644
index 00000000000..c5e88e4414b
--- /dev/null
+++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditStreamResultTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.assertThrows;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+class DastAuditStreamResultTest {
+ @Test
+ void defensivelyCopiesResults() {
+ var source = new ArrayList();
+ DastAuditStreamResult result = DastAuditStreamResult.builder().results(source).build();
+
+ source.add(DastAuditResult.Skipped.builder().issueId("DAST-1").build());
+
+ assertEquals(List.of(), result.results());
+ assertThrows(UnsupportedOperationException.class,
+ () -> result.results().add(DastAuditResult.Skipped.builder().issueId("DAST-2").build()));
+ }
+
+ @Test
+ void normalizesNullResultsToEmptyList() {
+ DastAuditStreamResult result = DastAuditStreamResult.builder().build();
+
+ assertEquals(List.of(), result.results());
+ }
+}
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 bcbb949a845..258a3b0a1d5 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
@@ -12,359 +12,14 @@
*/
package com.fortify.cli.aviator.ssc.cli.cmd;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Set;
-
-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._common.session.user.helper.AviatorUserSessionDescriptor;
-import com.fortify.cli.aviator.audit.AuditFPR;
-import com.fortify.cli.aviator.audit.model.AuditFprOptions;
-import com.fortify.cli.aviator.audit.model.FPRAuditResult;
-import com.fortify.cli.aviator.config.AviatorLoggerImpl;
-import com.fortify.cli.aviator.config.TagMappingConfig;
-import com.fortify.cli.aviator.ssc.helper.AviatorSSCAuditHelper;
-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.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.common.rest.unirest.UnexpectedHttpResponseException;
-import com.fortify.cli.common.util.DisableTest;
import com.fortify.cli.common.variable.DefaultVariablePropertyName;
-import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand;
-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.cli.mixin.SSCAppVersionRefreshOptions;
-import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin;
-import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor;
-import com.fortify.cli.ssc.appversion.helper.SSCAppVersionHelper;
-import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueFilterSetOptionMixin;
-import com.fortify.cli.ssc.system_state.helper.SSCJobDescriptor;
-import com.fortify.cli.ssc.system_state.helper.SSCJobHelper;
-import kong.unirest.UnirestInstance;
-import lombok.Getter;
-import lombok.SneakyThrows;
-import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
-import picocli.CommandLine.Mixin;
-import picocli.CommandLine.Option;
+/**
+ * Deprecated SAST audit command retained for backward compatibility.
+ * Use {@code fcli aviator ssc audit-sast} instead.
+ */
@Command(name = "audit")
@DefaultVariablePropertyName("artifactId")
-public class AviatorSSCAuditCommand extends AbstractSSCJsonNodeOutputCommand implements IActionCommandResultSupplier {
- @Getter @Mixin private OutputHelperMixins.DetailsNoQuery outputHelper;
- @Mixin private ProgressWriterFactoryMixin progressWriterFactoryMixin;
- @Mixin private SSCAppVersionResolverMixin.RequiredOption appVersionResolver;
- @Mixin private AviatorUserSessionDescriptorSupplier sessionDescriptorSupplier;
- @Mixin private SSCAppVersionRefreshOptions refreshOptions;
- @Mixin private SSCIssueFilterSetOptionMixin filterSetOptions;
- @Option(names = {"--app"}) private String appName;
- @Option(names = {"--tag-mapping"}) private String tagMapping;
- @Option(names = {"--no-filterset"}) private boolean noFilterSet;
- @Option(names = {"--folder"}, split = ",") @DisableTest(DisableTest.TestType.MULTI_OPT_PLURAL_NAME) private List folderNames;
- @ArgGroup(exclusive = true, multiplicity = "0..1") private QuotaHandlingArgGroup quotaHandlingArgGroup = new QuotaHandlingArgGroup();
- @Option(names = {"--test-exceeding-quota"}) private boolean testExceedingQuota;
- @Option(names = {"--default-quota-fallback"}) private boolean defaultQuotaFallback;
- private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCAuditCommand.class);
- private Long checkedQuotaBefore;
-
- private static final class QuotaHandlingArgGroup {
- @Option(names = {"--skip-if-exceeding-quota"}) private boolean skipIfExceedingQuota;
- @Option(names = {"--folder-priority-order"}, split = ",")
- @DisableTest(DisableTest.TestType.MULTI_OPT_PLURAL_NAME)
- private List folderPriorityOrder;
- }
-
- @Override
- @SneakyThrows
- public JsonNode getJsonNode(UnirestInstance unirest) {
- var sessionDescriptor = sessionDescriptorSupplier.getSessionDescriptor();
- Path downloadedFprPath = null;
- try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()) {
- AviatorLoggerImpl logger = new AviatorLoggerImpl(progressWriter);
- SSCAppVersionDescriptor av = appVersionResolver.getAppVersionDescriptor(unirest);
-
- refreshMetricsIfNeeded(unirest, av, logger);
-
- long auditableIssueCount = AviatorSSCAuditHelper.getAuditableIssueCount(unirest, av, logger, isNoFilterSet(), getFilterSetTitleOrId(), folderNames);
- if (auditableIssueCount == 0) {
- logger.progress("Audit skipped - no auditable issues found matching the specified filters.");
- ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "SKIPPED");
- AviatorSSCAuditHelper.setOperationMessage(result, "No auditable issues found matching the specified filters");
- return result;
- }
-
- JsonNode quotaResult = checkQuota(unirest, av, sessionDescriptor, auditableIssueCount, logger);
- if (quotaResult != null) {
- return quotaResult;
- }
-
- downloadedFprPath = downloadFpr(unirest, av, logger);
- if (downloadedFprPath == null) {
- ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "SKIPPED");
- AviatorSSCAuditHelper.setOperationMessage(result, "No FPR available to audit");
- return result;
- }
-
- ObjectNode result = (ObjectNode) processFpr(unirest, av, sessionDescriptor.getAviatorToken(), sessionDescriptor.getAviatorUrl(), logger, downloadedFprPath);
- if (checkedQuotaBefore != null) {
- AviatorSSCAuditHelper.setAvailableQuotaBefore(result, checkedQuotaBefore);
- }
- return result;
- } finally {
- if (downloadedFprPath != null) {
- Files.deleteIfExists(downloadedFprPath);
- }
- }
- }
-
- String getFilterSetTitleOrId() {
- return filterSetOptions.getFilterSetTitleOrId();
- }
-
- boolean isNoFilterSet() {
- return noFilterSet;
- }
-
- private void refreshMetricsIfNeeded(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger) {
- if (refreshOptions.isRefresh() && av.isRefreshRequired()) {
- logger.progress("Status: Metrics for application version %s:%s are out of date, starting refresh...", av.getApplicationName(), av.getVersionName());
- SSCJobDescriptor refreshJobDesc = SSCAppVersionHelper.refreshMetrics(unirest, av);
- if (refreshJobDesc != null) {
- SSCJobHelper.waitForJob(unirest, refreshJobDesc, refreshOptions.getRefreshTimeout());
- logger.progress("Status: Metrics refreshed successfully.");
- }
- }
- }
-
- private boolean isSkipIfExceedingQuota() {
- return quotaHandlingArgGroup.skipIfExceedingQuota;
- }
-
- private List getFolderPriorityOrder() {
- return quotaHandlingArgGroup.folderPriorityOrder;
- }
-
- /**
- * Checks quota constraints when --skip-if-exceeding-quota or --test-exceeding-quota is active.
- * @return a result JsonNode if the audit should be skipped/reported, or null if the audit should proceed.
- */
- private JsonNode checkQuota(UnirestInstance unirest, SSCAppVersionDescriptor av,
- AviatorUserSessionDescriptor sessionDescriptor,
- long auditableIssueCount, AviatorLoggerImpl logger) {
- if (!isSkipIfExceedingQuota() && !testExceedingQuota) {
- return null;
- }
-
- String effectiveAppName = appName != null ? appName : av.getApplicationName();
- long availableQuota = AviatorSSCAuditHelper.getAvailableQuota(
- sessionDescriptor.getAviatorUrl(), sessionDescriptor.getAviatorToken(),
- effectiveAppName, logger);
-
- // App not found — behavior depends on --default-quota-fallback
- if (availableQuota == AviatorSSCAuditHelper.QUOTA_APP_NOT_FOUND) {
- availableQuota = handleAppNotFound(sessionDescriptor, effectiveAppName, logger);
- if (availableQuota == AviatorSSCAuditHelper.QUOTA_APP_NOT_FOUND) {
- ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "SKIPPED");
- AviatorSSCAuditHelper.setOperationMessage(result, "Application '" + effectiveAppName + "' not found in Aviator");
- return result;
- }
- }
-
- // If auditable issue count is unknown (-1), skip quota comparison and proceed with audit
- if (auditableIssueCount < 0) {
- LOG.info("Auditable issue count unknown; skipping quota evaluation for {}:{}.",
- av.getApplicationName(), av.getVersionName());
- return null;
- }
-
- return evaluateQuota(unirest, av, effectiveAppName, auditableIssueCount, availableQuota, logger);
- }
-
- /**
- * Handles the case where the application is not found in Aviator.
- * @return the resolved quota (possibly from default), or QUOTA_APP_NOT_FOUND if audit should be skipped.
- */
- private long handleAppNotFound(AviatorUserSessionDescriptor sessionDescriptor,
- String effectiveAppName, AviatorLoggerImpl logger) {
- if (defaultQuotaFallback) {
- logger.progress("Application '%s' not found, using default quota for new applications.", effectiveAppName);
- long defaultQuota = AviatorSSCAuditHelper.getDefaultQuota(
- sessionDescriptor.getAviatorUrl(), sessionDescriptor.getAviatorToken(), logger);
- if (defaultQuota == AviatorSSCAuditHelper.QUOTA_UNKNOWN) {
- if (testExceedingQuota) {
- // Caller will need to handle this — we return QUOTA_UNKNOWN to signal
- return AviatorSSCAuditHelper.QUOTA_UNKNOWN;
- }
- logger.progress("Warning: Could not retrieve default quota, proceeding with audit.");
- return AviatorSSCAuditHelper.QUOTA_UNKNOWN;
- }
- return defaultQuota;
- } else {
- logger.progress("Application '%s' does not exist in Aviator.", effectiveAppName);
- return AviatorSSCAuditHelper.QUOTA_APP_NOT_FOUND;
- }
- }
-
- /**
- * Evaluates the resolved quota against the auditable issue count and returns
- * a result node if audit should be skipped, or null to proceed with the audit.
- */
- private JsonNode evaluateQuota(UnirestInstance unirest, SSCAppVersionDescriptor av,
- String effectiveAppName, long auditableIssueCount, long availableQuota,
- AviatorLoggerImpl logger) {
- if (availableQuota == AviatorSSCAuditHelper.QUOTA_UNKNOWN) {
- if (testExceedingQuota) {
- ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "QUOTA_UNKNOWN");
- AviatorSSCAuditHelper.setOperationMessage(result, "Could not retrieve quota for application '" + effectiveAppName + "'");
- return result;
- }
- logger.progress("Warning: Could not retrieve quota for '%s', proceeding with audit.", effectiveAppName);
- } else if (availableQuota >= 0 && auditableIssueCount > availableQuota) {
- checkedQuotaBefore = availableQuota;
- var topCategories = AviatorSSCAuditHelper.getTopUnauditedCategories(unirest, av, logger, 10);
- String detailedMessage = AviatorSSCAuditHelper.formatQuotaExceededMessage(
- av, auditableIssueCount, availableQuota, topCategories);
- LOG.info(detailedMessage);
- logger.progress("Quota exceeded for %s:%s -- Open issues: %d, Available quota: %d. Audit skipped.",
- av.getApplicationName(), av.getVersionName(), auditableIssueCount, availableQuota);
- return AviatorSSCAuditHelper.buildQuotaExceededResultNode(
- av, auditableIssueCount, availableQuota, topCategories);
- } else if (testExceedingQuota) {
- logger.progress("Quota check passed for %s:%s -- Open issues: %d, Available quota: %s",
- av.getApplicationName(), av.getVersionName(), auditableIssueCount,
- availableQuota < 0 ? "unlimited" : String.valueOf(availableQuota));
- ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "QUOTA_OK");
- AviatorSSCAuditHelper.setOperationMessage(result, String.format("Quota check passed: %d issues, %s quota available",
- auditableIssueCount, availableQuota < 0 ? "unlimited" : String.valueOf(availableQuota)));
- AviatorSSCAuditHelper.setAvailableQuotaBefore(result, availableQuota);
- return result;
- }
- // Quota was checked and audit is proceeding — capture the value for the final output
- checkedQuotaBefore = availableQuota >= 0 ? availableQuota : null;
- return null;
- }
-
- @SneakyThrows
- private JsonNode processFpr(UnirestInstance unirest, SSCAppVersionDescriptor av, String token, String url, AviatorLoggerImpl logger, Path downloadedFprPath) {
- FPRAuditResult auditResult;
-
- try (FprHandle fprHandle = new FprHandle(downloadedFprPath)) {
- auditResult = AuditFPR.auditFPR(AuditFprOptions.builder()
- .fprHandle(fprHandle).token(token).url(url)
- .appVersion(appName)
- .sscAppName(av.getApplicationName())
- .sscAppVersion(av.getVersionName())
- .logger(logger)
- .tagMappingPath(tagMapping)
- .filterSetNameOrId(getFilterSetTitleOrId())
- .noFilterSet(isNoFilterSet())
- .folderNames(folderNames)
- .folderPriorityOrder(getFolderPriorityOrder())
- .build());
- } catch (Exception e) {
- LOG.error("FPR audit failed for {}:{}: {}", av.getApplicationName(), av.getVersionName(), e.getMessage(), e);
- ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "FAILED");
- AviatorSSCAuditHelper.setOperationMessage(result, "Audit failed: " + e.getMessage());
- return result;
- }
-
- String action = auditResult.getStatus();
- logger.progress(AviatorSSCAuditHelper.getProgressMessage(auditResult));
-
- String artifactId = null;
- if (auditResult.getUpdatedFile() != null && !"SKIPPED".equals(action) && !"FAILED".equals(action)) {
- validateSSCTagsBeforeUpload(unirest, av, logger);
- try {
- artifactId = uploadAuditedFprToSSC(unirest, auditResult.getUpdatedFile(), av);
- } catch (Exception e) {
- LOG.error("Failed to upload audited FPR for {}:{}: {}", av.getApplicationName(), av.getVersionName(), e.getMessage(), e);
- logger.progress("WARN: Upload of audited FPR to SSC failed: %s", e.getMessage());
- }
- }
-
- ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, artifactId, action);
- AviatorSSCAuditHelper.setAuditStats(result, auditResult);
- return result;
- }
-
- /**
- * Validates that SSC has the required custom tags and Analysis tag values
- * before uploading the audited FPR. Emits warnings for any missing tags or
- * values so the user can take corrective action.
- */
- private void validateSSCTagsBeforeUpload(UnirestInstance unirest, SSCAppVersionDescriptor av,
- AviatorLoggerImpl logger) {
- 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 = tagMappingConfig.getMappedValues();
- LOG.info("Analysis tag values to validate: {}", analysisTagValues);
- List warnings = AviatorSSCTagValidator.validatePreUpload(
- unirest, av.getVersionId(), tagMappingConfig.getTag_id(), analysisTagValues, logger);
- LOG.info("Tag validation complete. {} warning(s) found.", warnings.size());
- }
-
- private TagMappingConfig loadTagMappingForValidation() {
- if (tagMapping != null && !tagMapping.isBlank()) {
- return ResourceUtil.loadYamlFile(new java.io.File(tagMapping), TagMappingConfig.class);
- }
- return AviatorConfigManager.getInstance().getDefaultTagMappingConfig();
- }
-
- 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());
-
- String prefix = String.format("aviator_%s_%s_", av.getApplicationName().replaceAll("[^a-zA-Z0-9.-]", "_"), av.getVersionName().replaceAll("[^a-zA-Z0-9.-]", "_"));
- Path tempFpr = Files.createTempFile(prefix, ".fpr");
-
- try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()) {
- SSCFileTransferHelper.download(
- unirest,
- SSCUrls.DOWNLOAD_CURRENT_FPR(av.getVersionId(), true),
- tempFpr.toFile(),
- SSCFileTransferHelper.ISSCAddDownloadTokenFunction.ROUTEPARAM_DOWNLOADTOKEN,
- progressWriter);
- return tempFpr;
- } catch (UnexpectedHttpResponseException e) {
- Files.deleteIfExists(tempFpr);
- if (e.getStatus() == 400) {
- logger.progress("Audit skipped - no FPR available to audit in SSC for app version %s:%s.", av.getApplicationName(), av.getVersionName());
- LOG.info("SSC returned HTTP 400 when downloading FPR for app version id {}. Assuming no FPR is available.", av.getVersionId());
- return null;
- }
- throw e;
- }
- }
-
- @SneakyThrows
- private String uploadAuditedFprToSSC(UnirestInstance unirest, File auditedFpr, SSCAppVersionDescriptor av) {
- try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()) {
- JsonNode uploadResponse = SSCFileTransferHelper.restUpload(unirest, SSCUrls.PROJECT_VERSION_ARTIFACTS(av.getVersionId()), auditedFpr, JsonNode.class, progressWriter);
- return uploadResponse.path("data").path("id").asText("UPLOAD_FAILED");
- }
- }
-
- @Override
- public String getActionCommandResult() {
- return "AUDITED";
- }
-
- @Override
- public boolean isSingular() {
- return true;
- }
-}
+public class AviatorSSCAuditCommand extends AviatorSSCSastAuditCommand {}
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 07e7d7429f4..9e41052853e 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,
+ AviatorSSCSastAuditCommand.class,
AviatorSSCDastAuditCommand.class,
AviatorSSCPrepareCommand.class,
AviatorSSCApplyRemediationsCommand.class,
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
index 20e4beea679..229c7a6d3ff 100644
--- 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
@@ -28,6 +28,7 @@
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._common.session.user.helper.AviatorUserSessionDescriptor;
import com.fortify.cli.aviator.audit.DastAuditFPR;
import com.fortify.cli.aviator.audit.DastAuditFprResult;
import com.fortify.cli.aviator.config.AviatorLoggerImpl;
@@ -84,7 +85,7 @@ public JsonNode getJsonNode(UnirestInstance unirest) {
DastAuditFprResult result = auditFpr(
downloadedFpr, appVersion, session, logger, tagMappingConfig);
- actionResult = result.status();
+ actionResult = result.status().name();
String artifactId = null;
if (result.updatedFile() != null && result.succeeded() > 0) {
validateSSCTagsBeforeUpload(unirest, appVersion, logger, tagMappingConfig);
@@ -113,13 +114,16 @@ public JsonNode getJsonNode(UnirestInstance unirest) {
private DastAuditFprResult auditFpr(
Path fprPath,
SSCAppVersionDescriptor appVersion,
- com.fortify.cli.aviator._common.session.user.helper.AviatorUserSessionDescriptor session,
+ 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);
+ var config = DastAuditStreamConfig.builder()
+ .token(session.getAviatorToken())
+ .applicationName(effectiveAppName)
+ .sscApplicationName(appVersion.getApplicationName())
+ .sscApplicationVersion(appVersion.getVersionName())
+ .build();
try (var grpcClient = AviatorGrpcClientHelper.createClient(session.getAviatorUrl(), logger, 30);
var streamProcessor = new DastAuditStreamProcessor(
logger, grpcClient.getDastAuditAsyncStub(), grpcClient.getPingScheduler(),
@@ -138,7 +142,7 @@ private ObjectNode buildOutput(
SSCAppVersionDescriptor appVersion,
DastAuditFprResult audit,
String artifactId) {
- ObjectNode result = AviatorSSCAuditHelper.buildResultNode(appVersion, artifactId, audit.status());
+ ObjectNode result = AviatorSSCAuditHelper.buildResultNode(appVersion, artifactId, audit.status().name());
AviatorSSCAuditHelper.setDastAuditStats(result, audit);
return result;
}
@@ -147,8 +151,7 @@ private TagMappingConfig loadTagMappingConfig() {
TagMappingConfig tagMappingConfig = tagMapping == null || tagMapping.isBlank()
? AviatorConfigManager.getInstance().getDefaultDastTagMappingConfig()
: ResourceUtil.loadYamlFile(new File(tagMapping), TagMappingConfig.class);
- tagMappingConfig.validateForDast();
- return tagMappingConfig;
+ return tagMappingConfig.resolveForDast();
}
private void validateSSCTagsBeforeUpload(UnirestInstance unirest,
diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCSastAuditCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCSastAuditCommand.java
new file mode 100644
index 00000000000..dbc26063d1e
--- /dev/null
+++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCSastAuditCommand.java
@@ -0,0 +1,370 @@
+/*
+ * 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 java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+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._common.session.user.helper.AviatorUserSessionDescriptor;
+import com.fortify.cli.aviator.audit.AuditFPR;
+import com.fortify.cli.aviator.audit.model.AuditFprOptions;
+import com.fortify.cli.aviator.audit.model.FPRAuditResult;
+import com.fortify.cli.aviator.config.AviatorLoggerImpl;
+import com.fortify.cli.aviator.config.TagMappingConfig;
+import com.fortify.cli.aviator.ssc.helper.AviatorSSCAuditHelper;
+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.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.common.rest.unirest.UnexpectedHttpResponseException;
+import com.fortify.cli.common.util.DisableTest;
+import com.fortify.cli.common.variable.DefaultVariablePropertyName;
+import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand;
+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.cli.mixin.SSCAppVersionRefreshOptions;
+import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin;
+import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor;
+import com.fortify.cli.ssc.appversion.helper.SSCAppVersionHelper;
+import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueFilterSetOptionMixin;
+import com.fortify.cli.ssc.system_state.helper.SSCJobDescriptor;
+import com.fortify.cli.ssc.system_state.helper.SSCJobHelper;
+
+import kong.unirest.UnirestInstance;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import picocli.CommandLine.ArgGroup;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+import picocli.CommandLine.Option;
+
+@Command(name = "audit-sast")
+@DefaultVariablePropertyName("artifactId")
+public class AviatorSSCSastAuditCommand extends AbstractSSCJsonNodeOutputCommand implements IActionCommandResultSupplier {
+ @Getter @Mixin private OutputHelperMixins.DetailsNoQuery outputHelper;
+ @Mixin private ProgressWriterFactoryMixin progressWriterFactoryMixin;
+ @Mixin private SSCAppVersionResolverMixin.RequiredOption appVersionResolver;
+ @Mixin private AviatorUserSessionDescriptorSupplier sessionDescriptorSupplier;
+ @Mixin private SSCAppVersionRefreshOptions refreshOptions;
+ @Mixin private SSCIssueFilterSetOptionMixin filterSetOptions;
+ @Option(names = {"--app"}) private String appName;
+ @Option(names = {"--tag-mapping"}) private String tagMapping;
+ @Option(names = {"--no-filterset"}) private boolean noFilterSet;
+ @Option(names = {"--folder"}, split = ",") @DisableTest(DisableTest.TestType.MULTI_OPT_PLURAL_NAME) private List folderNames;
+ @ArgGroup(exclusive = true, multiplicity = "0..1") private QuotaHandlingArgGroup quotaHandlingArgGroup = new QuotaHandlingArgGroup();
+ @Option(names = {"--test-exceeding-quota"}) private boolean testExceedingQuota;
+ @Option(names = {"--default-quota-fallback"}) private boolean defaultQuotaFallback;
+ private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCSastAuditCommand.class);
+ private Long checkedQuotaBefore;
+
+ private static final class QuotaHandlingArgGroup {
+ @Option(names = {"--skip-if-exceeding-quota"}) private boolean skipIfExceedingQuota;
+ @Option(names = {"--folder-priority-order"}, split = ",")
+ @DisableTest(DisableTest.TestType.MULTI_OPT_PLURAL_NAME)
+ private List folderPriorityOrder;
+ }
+
+ @Override
+ @SneakyThrows
+ public JsonNode getJsonNode(UnirestInstance unirest) {
+ var sessionDescriptor = sessionDescriptorSupplier.getSessionDescriptor();
+ Path downloadedFprPath = null;
+ try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()) {
+ AviatorLoggerImpl logger = new AviatorLoggerImpl(progressWriter);
+ SSCAppVersionDescriptor av = appVersionResolver.getAppVersionDescriptor(unirest);
+
+ refreshMetricsIfNeeded(unirest, av, logger);
+
+ long auditableIssueCount = AviatorSSCAuditHelper.getAuditableIssueCount(unirest, av, logger, isNoFilterSet(), getFilterSetTitleOrId(), folderNames);
+ if (auditableIssueCount == 0) {
+ logger.progress("Audit skipped - no auditable issues found matching the specified filters.");
+ ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "SKIPPED");
+ AviatorSSCAuditHelper.setOperationMessage(result, "No auditable issues found matching the specified filters");
+ return result;
+ }
+
+ JsonNode quotaResult = checkQuota(unirest, av, sessionDescriptor, auditableIssueCount, logger);
+ if (quotaResult != null) {
+ return quotaResult;
+ }
+
+ downloadedFprPath = downloadFpr(unirest, av, logger);
+ if (downloadedFprPath == null) {
+ ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "SKIPPED");
+ AviatorSSCAuditHelper.setOperationMessage(result, "No FPR available to audit");
+ return result;
+ }
+
+ ObjectNode result = (ObjectNode) processFpr(unirest, av, sessionDescriptor.getAviatorToken(), sessionDescriptor.getAviatorUrl(), logger, downloadedFprPath);
+ if (checkedQuotaBefore != null) {
+ AviatorSSCAuditHelper.setAvailableQuotaBefore(result, checkedQuotaBefore);
+ }
+ return result;
+ } finally {
+ if (downloadedFprPath != null) {
+ Files.deleteIfExists(downloadedFprPath);
+ }
+ }
+ }
+
+ String getFilterSetTitleOrId() {
+ return filterSetOptions.getFilterSetTitleOrId();
+ }
+
+ boolean isNoFilterSet() {
+ return noFilterSet;
+ }
+
+ private void refreshMetricsIfNeeded(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger) {
+ if (refreshOptions.isRefresh() && av.isRefreshRequired()) {
+ logger.progress("Status: Metrics for application version %s:%s are out of date, starting refresh...", av.getApplicationName(), av.getVersionName());
+ SSCJobDescriptor refreshJobDesc = SSCAppVersionHelper.refreshMetrics(unirest, av);
+ if (refreshJobDesc != null) {
+ SSCJobHelper.waitForJob(unirest, refreshJobDesc, refreshOptions.getRefreshTimeout());
+ logger.progress("Status: Metrics refreshed successfully.");
+ }
+ }
+ }
+
+ private boolean isSkipIfExceedingQuota() {
+ return quotaHandlingArgGroup.skipIfExceedingQuota;
+ }
+
+ private List getFolderPriorityOrder() {
+ return quotaHandlingArgGroup.folderPriorityOrder;
+ }
+
+ /**
+ * Checks quota constraints when --skip-if-exceeding-quota or --test-exceeding-quota is active.
+ * @return a result JsonNode if the audit should be skipped/reported, or null if the audit should proceed.
+ */
+ private JsonNode checkQuota(UnirestInstance unirest, SSCAppVersionDescriptor av,
+ AviatorUserSessionDescriptor sessionDescriptor,
+ long auditableIssueCount, AviatorLoggerImpl logger) {
+ if (!isSkipIfExceedingQuota() && !testExceedingQuota) {
+ return null;
+ }
+
+ String effectiveAppName = appName != null ? appName : av.getApplicationName();
+ long availableQuota = AviatorSSCAuditHelper.getAvailableQuota(
+ sessionDescriptor.getAviatorUrl(), sessionDescriptor.getAviatorToken(),
+ effectiveAppName, logger);
+
+ // App not found — behavior depends on --default-quota-fallback
+ if (availableQuota == AviatorSSCAuditHelper.QUOTA_APP_NOT_FOUND) {
+ availableQuota = handleAppNotFound(sessionDescriptor, effectiveAppName, logger);
+ if (availableQuota == AviatorSSCAuditHelper.QUOTA_APP_NOT_FOUND) {
+ ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "SKIPPED");
+ AviatorSSCAuditHelper.setOperationMessage(result, "Application '" + effectiveAppName + "' not found in Aviator");
+ return result;
+ }
+ }
+
+ // If auditable issue count is unknown (-1), skip quota comparison and proceed with audit
+ if (auditableIssueCount < 0) {
+ LOG.info("Auditable issue count unknown; skipping quota evaluation for {}:{}.",
+ av.getApplicationName(), av.getVersionName());
+ return null;
+ }
+
+ return evaluateQuota(unirest, av, effectiveAppName, auditableIssueCount, availableQuota, logger);
+ }
+
+ /**
+ * Handles the case where the application is not found in Aviator.
+ * @return the resolved quota (possibly from default), or QUOTA_APP_NOT_FOUND if audit should be skipped.
+ */
+ private long handleAppNotFound(AviatorUserSessionDescriptor sessionDescriptor,
+ String effectiveAppName, AviatorLoggerImpl logger) {
+ if (defaultQuotaFallback) {
+ logger.progress("Application '%s' not found, using default quota for new applications.", effectiveAppName);
+ long defaultQuota = AviatorSSCAuditHelper.getDefaultQuota(
+ sessionDescriptor.getAviatorUrl(), sessionDescriptor.getAviatorToken(), logger);
+ if (defaultQuota == AviatorSSCAuditHelper.QUOTA_UNKNOWN) {
+ if (testExceedingQuota) {
+ // Caller will need to handle this — we return QUOTA_UNKNOWN to signal
+ return AviatorSSCAuditHelper.QUOTA_UNKNOWN;
+ }
+ logger.progress("Warning: Could not retrieve default quota, proceeding with audit.");
+ return AviatorSSCAuditHelper.QUOTA_UNKNOWN;
+ }
+ return defaultQuota;
+ } else {
+ logger.progress("Application '%s' does not exist in Aviator.", effectiveAppName);
+ return AviatorSSCAuditHelper.QUOTA_APP_NOT_FOUND;
+ }
+ }
+
+ /**
+ * Evaluates the resolved quota against the auditable issue count and returns
+ * a result node if audit should be skipped, or null to proceed with the audit.
+ */
+ private JsonNode evaluateQuota(UnirestInstance unirest, SSCAppVersionDescriptor av,
+ String effectiveAppName, long auditableIssueCount, long availableQuota,
+ AviatorLoggerImpl logger) {
+ if (availableQuota == AviatorSSCAuditHelper.QUOTA_UNKNOWN) {
+ if (testExceedingQuota) {
+ ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "QUOTA_UNKNOWN");
+ AviatorSSCAuditHelper.setOperationMessage(result, "Could not retrieve quota for application '" + effectiveAppName + "'");
+ return result;
+ }
+ logger.progress("Warning: Could not retrieve quota for '%s', proceeding with audit.", effectiveAppName);
+ } else if (availableQuota >= 0 && auditableIssueCount > availableQuota) {
+ checkedQuotaBefore = availableQuota;
+ var topCategories = AviatorSSCAuditHelper.getTopUnauditedCategories(unirest, av, logger, 10);
+ String detailedMessage = AviatorSSCAuditHelper.formatQuotaExceededMessage(
+ av, auditableIssueCount, availableQuota, topCategories);
+ LOG.info(detailedMessage);
+ logger.progress("Quota exceeded for %s:%s -- Open issues: %d, Available quota: %d. Audit skipped.",
+ av.getApplicationName(), av.getVersionName(), auditableIssueCount, availableQuota);
+ return AviatorSSCAuditHelper.buildQuotaExceededResultNode(
+ av, auditableIssueCount, availableQuota, topCategories);
+ } else if (testExceedingQuota) {
+ logger.progress("Quota check passed for %s:%s -- Open issues: %d, Available quota: %s",
+ av.getApplicationName(), av.getVersionName(), auditableIssueCount,
+ availableQuota < 0 ? "unlimited" : String.valueOf(availableQuota));
+ ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "QUOTA_OK");
+ AviatorSSCAuditHelper.setOperationMessage(result, String.format("Quota check passed: %d issues, %s quota available",
+ auditableIssueCount, availableQuota < 0 ? "unlimited" : String.valueOf(availableQuota)));
+ AviatorSSCAuditHelper.setAvailableQuotaBefore(result, availableQuota);
+ return result;
+ }
+ // Quota was checked and audit is proceeding — capture the value for the final output
+ checkedQuotaBefore = availableQuota >= 0 ? availableQuota : null;
+ return null;
+ }
+
+ @SneakyThrows
+ private JsonNode processFpr(UnirestInstance unirest, SSCAppVersionDescriptor av, String token, String url, AviatorLoggerImpl logger, Path downloadedFprPath) {
+ FPRAuditResult auditResult;
+
+ try (FprHandle fprHandle = new FprHandle(downloadedFprPath)) {
+ auditResult = AuditFPR.auditFPR(AuditFprOptions.builder()
+ .fprHandle(fprHandle).token(token).url(url)
+ .appVersion(appName)
+ .sscAppName(av.getApplicationName())
+ .sscAppVersion(av.getVersionName())
+ .logger(logger)
+ .tagMappingPath(tagMapping)
+ .filterSetNameOrId(getFilterSetTitleOrId())
+ .noFilterSet(isNoFilterSet())
+ .folderNames(folderNames)
+ .folderPriorityOrder(getFolderPriorityOrder())
+ .build());
+ } catch (Exception e) {
+ LOG.error("FPR audit failed for {}:{}: {}", av.getApplicationName(), av.getVersionName(), e.getMessage(), e);
+ ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "FAILED");
+ AviatorSSCAuditHelper.setOperationMessage(result, "Audit failed: " + e.getMessage());
+ return result;
+ }
+
+ String action = auditResult.getStatus();
+ logger.progress(AviatorSSCAuditHelper.getProgressMessage(auditResult));
+
+ String artifactId = null;
+ if (auditResult.getUpdatedFile() != null && !"SKIPPED".equals(action) && !"FAILED".equals(action)) {
+ validateSSCTagsBeforeUpload(unirest, av, logger);
+ try {
+ artifactId = uploadAuditedFprToSSC(unirest, auditResult.getUpdatedFile(), av);
+ } catch (Exception e) {
+ LOG.error("Failed to upload audited FPR for {}:{}: {}", av.getApplicationName(), av.getVersionName(), e.getMessage(), e);
+ logger.progress("WARN: Upload of audited FPR to SSC failed: %s", e.getMessage());
+ }
+ }
+
+ ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, artifactId, action);
+ AviatorSSCAuditHelper.setAuditStats(result, auditResult);
+ return result;
+ }
+
+ /**
+ * Validates that SSC has the required custom tags and Analysis tag values
+ * before uploading the audited FPR. Emits warnings for any missing tags or
+ * values so the user can take corrective action.
+ */
+ private void validateSSCTagsBeforeUpload(UnirestInstance unirest, SSCAppVersionDescriptor av,
+ AviatorLoggerImpl logger) {
+ 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 = tagMappingConfig.getMappedValues();
+ LOG.info("Analysis tag values to validate: {}", analysisTagValues);
+ List warnings = AviatorSSCTagValidator.validatePreUpload(
+ unirest, av.getVersionId(), tagMappingConfig.getTag_id(), analysisTagValues, logger);
+ LOG.info("Tag validation complete. {} warning(s) found.", warnings.size());
+ }
+
+ private TagMappingConfig loadTagMappingForValidation() {
+ TagMappingConfig tagMappingConfig = tagMapping != null && !tagMapping.isBlank()
+ ? ResourceUtil.loadYamlFile(new java.io.File(tagMapping), TagMappingConfig.class)
+ : AviatorConfigManager.getInstance().getDefaultTagMappingConfig();
+ return tagMappingConfig.resolveForSast();
+ }
+
+ 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());
+
+ String prefix = String.format("aviator_%s_%s_", av.getApplicationName().replaceAll("[^a-zA-Z0-9.-]", "_"), av.getVersionName().replaceAll("[^a-zA-Z0-9.-]", "_"));
+ Path tempFpr = Files.createTempFile(prefix, ".fpr");
+
+ try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()) {
+ SSCFileTransferHelper.download(
+ unirest,
+ SSCUrls.DOWNLOAD_CURRENT_FPR(av.getVersionId(), true),
+ tempFpr.toFile(),
+ SSCFileTransferHelper.ISSCAddDownloadTokenFunction.ROUTEPARAM_DOWNLOADTOKEN,
+ progressWriter);
+ return tempFpr;
+ } catch (UnexpectedHttpResponseException e) {
+ Files.deleteIfExists(tempFpr);
+ if (e.getStatus() == 400) {
+ logger.progress("Audit skipped - no FPR available to audit in SSC for app version %s:%s.", av.getApplicationName(), av.getVersionName());
+ LOG.info("SSC returned HTTP 400 when downloading FPR for app version id {}. Assuming no FPR is available.", av.getVersionId());
+ return null;
+ }
+ throw e;
+ }
+ }
+
+ @SneakyThrows
+ private String uploadAuditedFprToSSC(UnirestInstance unirest, File auditedFpr, SSCAppVersionDescriptor av) {
+ try (IProgressWriter progressWriter = progressWriterFactoryMixin.create()) {
+ JsonNode uploadResponse = SSCFileTransferHelper.restUpload(unirest, SSCUrls.PROJECT_VERSION_ARTIFACTS(av.getVersionId()), auditedFpr, JsonNode.class, progressWriter);
+ return uploadResponse.path("data").path("id").asText("UPLOAD_FAILED");
+ }
+ }
+
+ @Override
+ public String getActionCommandResult() {
+ return "AUDITED";
+ }
+
+ @Override
+ public boolean isSingular() {
+ return true;
+ }
+}
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 48539a13b32..cf74d9aaa03 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
@@ -161,15 +161,13 @@ public static void setDastAuditStats(ObjectNode result, DastAuditFprResult audit
private static String getDastAuditMessage(DastAuditFprResult auditResult) {
return switch (auditResult.status()) {
- case "AUDITED" -> "DAST audit completed successfully";
- case "PARTIALLY_AUDITED" -> auditResult.message() != null
+ case AUDITED -> "DAST audit completed successfully";
+ case PARTIALLY_AUDITED -> auditResult.message() != null
? auditResult.message() : "DAST audit partially completed";
- case "SKIPPED" -> auditResult.message() != null
+ case SKIPPED -> auditResult.message() != null
? auditResult.message() : "No DAST findings to audit";
- case "FAILED" -> auditResult.message() != null
+ case FAILED -> auditResult.message() != null
? auditResult.message() : "DAST audit failed";
- default -> auditResult.message() != null
- ? auditResult.message() : "Unknown DAST audit status";
};
}
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
index 7ea26de68eb..8403f8dcde8 100644
--- 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
@@ -18,6 +18,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fortify.cli.aviator.config.IAviatorLogger;
+import com.fortify.cli.common.exception.FcliTechnicalException;
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;
@@ -68,6 +69,15 @@ public static String uploadDastFpr(
dastFpr.toFile(),
JsonNode.class,
progressWriter);
- return uploadResponse.path("data").path("id").asText("UPLOAD_FAILED");
+ return getUploadedArtifactId(uploadResponse);
+ }
+
+ static String getUploadedArtifactId(JsonNode uploadResponse) {
+ String artifactId = uploadResponse == null
+ ? null : uploadResponse.path("data").path("id").asText(null);
+ if (artifactId == null || artifactId.isBlank()) {
+ throw new FcliTechnicalException("SSC DAST FPR upload response did not contain an artifact ID");
+ }
+ return artifactId;
}
}
\ 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 9c04c3ba173..42a769201ee 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
@@ -135,8 +135,14 @@ fcli.aviator.entitlement.list-dast.usage.description = Retrieves a list of Forti
# fcli aviator ssc
fcli.aviator.ssc.usage.header = Use Fortify Remediation Aviator with SSC.
-fcli.aviator.ssc.audit.usage.header = Audit an SSC application version using Fortify Remediation Aviator.
-fcli.aviator.ssc.audit.usage.description = Downloads the FPR from an SSC application version, audits it with Fortify Remediation Aviator, and uploads the result back to SSC. \
+fcli.aviator.ssc.audit.usage.header = (DEPRECATED) Audit SAST findings in an SSC application version using Fortify Remediation Aviator.
+fcli.aviator.ssc.audit.usage.description = This command is deprecated, please use 'fcli aviator ssc audit-sast' instead. \
+ Downloads the FPR from an SSC application version, audits it with Fortify Remediation Aviator, and uploads the result back to SSC. \
+ This command requires an active user session. Use 'fcli aviator session login' to create a session. \
+ This command doesn't wait for SSC to finish processing the audited FPR file; please use the 'fcli ssc artifact wait-for' \
+ command to wait until the audited FPR file has been processed by SSC.
+fcli.aviator.ssc.audit-sast.usage.header = Audit SAST findings in an SSC application version using Fortify Remediation Aviator.
+fcli.aviator.ssc.audit-sast.usage.description = Downloads the FPR from an SSC application version, audits it with Fortify Remediation Aviator, and uploads the result back to SSC. \
This command requires an active user session. Use 'fcli aviator session login' to create a session. \
This command doesn't wait for SSC to finish processing the audited FPR file; please use the 'fcli ssc artifact wait-for' \
command to wait until the audited FPR file has been processed by SSC.
@@ -154,6 +160,19 @@ 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-sast.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-sast.tag-mapping = Custom tag mapping for audit results.
+fcli.aviator.ssc.audit-sast.filterset = Name or ID of the FilterSet to apply.
+fcli.aviator.ssc.audit-sast.no-filterset = Do not apply any filter sets, including the default enabled filter set from the FPR.
+fcli.aviator.ssc.audit-sast.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.
+fcli.aviator.ssc.audit-sast.skip-if-exceeding-quota = Skip audit if the number of open issues exceeds the available Fortify Remediation Aviator quota. When skipped, a summary with top unaudited categories is shown.
+fcli.aviator.ssc.audit-sast.test-exceeding-quota = Check whether the number of open issues exceeds the available Fortify Remediation Aviator quota and report the result without performing an audit.
+fcli.aviator.ssc.audit-sast.default-quota-fallback = (Internal) When the Fortify Aviator application does not exist, use the tenant default quota instead of reporting app not found. Used by bulk audit.
+fcli.aviator.ssc.audit-sast.folder-priority-order = Custom priority order for folder-based filtering when quota is exceeded (comma-separated, highest priority first). Example: Critical,High,Medium,Low. If not specified, uses default priority order.
+fcli.aviator.ssc.audit-sast.refresh = By default, this command will refresh the source application version's metrics when copying from it. \
+ Note that for large applications this can lead to an error if the timeout expires.
+fcli.aviator.ssc.audit-sast.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.
@@ -233,6 +252,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-sast.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
diff --git a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommandTest.java b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommandTest.java
index e98fa9a5587..8af4216b96a 100644
--- a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommandTest.java
+++ b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommandTest.java
@@ -19,12 +19,36 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.ResourceBundle;
import org.junit.jupiter.api.Test;
import picocli.CommandLine;
class AviatorSSCAuditCommandTest {
+ @Test
+ void registersCanonicalAndDeprecatedCommandNames() {
+ assertEquals("audit-sast", new CommandLine(new AviatorSSCSastAuditCommand()).getCommandName());
+ assertEquals("audit", new CommandLine(new AviatorSSCAuditCommand()).getCommandName());
+ }
+
+ @Test
+ void deprecatedCommandHelpPointsToCanonicalCommand() {
+ var messages = ResourceBundle.getBundle("com.fortify.cli.aviator.i18n.AviatorMessages");
+ String header = messages.getString("fcli.aviator.ssc.audit.usage.header");
+ String description = messages.getString("fcli.aviator.ssc.audit.usage.description");
+
+ assertTrue(header.contains("(DEPRECATED)"));
+ assertTrue(description.contains("fcli aviator ssc audit-sast"));
+ }
+
+ @Test
+ void canonicalCommandAllowsFilterOptions() {
+ var cmd = parseCanonical("--filterset", "Security Auditor View", "--no-filterset");
+ assertEquals("Security Auditor View", cmd.getFilterSetTitleOrId());
+ assertTrue(cmd.isNoFilterSet());
+ }
+
@Test
void testAllowsCombinedFilterOptions() {
var cmd = parse("--filterset", "Security Auditor View", "--no-filterset");
@@ -48,10 +72,20 @@ void testAllowsNoFilterSetOption() {
private static AviatorSSCAuditCommand parse(String... args) {
var cmd = new AviatorSSCAuditCommand();
+ parse(cmd, args);
+ return cmd;
+ }
+
+ private static AviatorSSCSastAuditCommand parseCanonical(String... args) {
+ var cmd = new AviatorSSCSastAuditCommand();
+ parse(cmd, args);
+ return cmd;
+ }
+
+ private static void parse(AviatorSSCSastAuditCommand cmd, String... args) {
var fullArgs = new ArrayList();
Collections.addAll(fullArgs, "--av", "test:1.0");
Collections.addAll(fullArgs, args);
new CommandLine(cmd).parseArgs(fullArgs.toArray(String[]::new));
- return cmd;
}
}
\ No newline at end of file
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
index cadbfea97ac..b5198888398 100644
--- 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
@@ -22,6 +22,7 @@
import org.junit.jupiter.api.Test;
import com.fortify.cli.aviator.audit.DastAuditFprResult;
+import com.fortify.cli.aviator.audit.DastAuditFprStatus;
import com.fortify.cli.aviator.ssc.helper.AviatorSSCAuditHelper;
import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor;
@@ -49,11 +50,21 @@ void dastAuditStatsUseSastAuditOutputEnvelope() {
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 auditResult = DastAuditFprResult.builder()
+ .status(DastAuditFprStatus.PARTIALLY_AUDITED)
+ .totalReported(8)
+ .eligible(6)
+ .submitted(6)
+ .succeeded(4)
+ .truePositives(2)
+ .falsePositivesSuppressed(1)
+ .likelyFalsePositives(1)
+ .skipped(2)
+ .reservedQuota(6)
+ .exceededCount(2)
+ .build();
- var result = AviatorSSCAuditHelper.buildResultNode(appVersion, "2786", auditResult.status());
+ var result = AviatorSSCAuditHelper.buildResultNode(appVersion, "2786", auditResult.status().name());
AviatorSSCAuditHelper.setDastAuditStats(result, auditResult);
assertEquals("42", result.path("id").asText());
diff --git a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCFprTransferHelperTest.java b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCFprTransferHelperTest.java
new file mode 100644
index 00000000000..509b7e2578b
--- /dev/null
+++ b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCFprTransferHelperTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021-2026 Open Text.
+ *
+ * The only warranties for products and services of Open Text
+ * and its affiliates and licensors ("Open Text") are as may
+ * be set forth in the express warranty statements accompanying
+ * such products and services. Nothing herein should be construed
+ * as constituting an additional warranty. Open Text shall not be
+ * liable for technical or editorial errors or omissions contained
+ * herein. The information contained herein is subject to change
+ * without notice.
+ */
+package com.fortify.cli.aviator.ssc.helper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+import com.fortify.cli.common.exception.FcliTechnicalException;
+import com.fortify.cli.common.json.JsonHelper;
+
+class AviatorSSCFprTransferHelperTest {
+ @Test
+ void returnsUploadedArtifactId() throws Exception {
+ var response = JsonHelper.getObjectMapper().readTree("{\"data\":{\"id\":2786}}");
+
+ assertEquals("2786", AviatorSSCFprTransferHelper.getUploadedArtifactId(response));
+ }
+
+ @Test
+ void rejectsMissingArtifactId() throws Exception {
+ var response = JsonHelper.getObjectMapper().readTree("{\"data\":{}}");
+
+ assertThrows(FcliTechnicalException.class,
+ () -> AviatorSSCFprTransferHelper.getUploadedArtifactId(response));
+ assertThrows(FcliTechnicalException.class,
+ () -> AviatorSSCFprTransferHelper.getUploadedArtifactId(null));
+ }
+}
diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml
index 6bdc82860d6..e2b01118e1c 100644
--- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml
+++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml
@@ -330,7 +330,7 @@ steps:
- if: ${cli['tag-mapping'] != null && cli['tag-mapping'] != ''}
run.fcli:
run_audit:
- cmd: "aviator ssc audit --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
+ cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
status.check: false
records.collect: true
stdout: show
@@ -338,7 +338,7 @@ steps:
- if: ${cli['tag-mapping'] == null || cli['tag-mapping'] == ''}
run.fcli:
run_audit:
- cmd: "aviator ssc audit --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
+ cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
status.check: false
records.collect: true
stdout: show
@@ -366,7 +366,7 @@ steps:
- if: ${cli['tag-mapping'] != null && cli['tag-mapping'] != ''}
run.fcli:
quota_precheck:
- cmd: "aviator ssc audit --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\" --test-exceeding-quota --default-quota-fallback${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
+ cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\" --test-exceeding-quota --default-quota-fallback${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
status.check: false
records.collect: true
stdout: show
@@ -374,7 +374,7 @@ steps:
- if: ${cli['tag-mapping'] == null || cli['tag-mapping'] == ''}
run.fcli:
quota_precheck:
- cmd: "aviator ssc audit --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\" --test-exceeding-quota --default-quota-fallback${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
+ cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\" --test-exceeding-quota --default-quota-fallback${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
status.check: false
records.collect: true
stdout: show
@@ -459,7 +459,7 @@ steps:
- if: ${cli['tag-mapping'] != null && cli['tag-mapping'] != ''}
run.fcli:
run_audit:
- cmd: "aviator ssc audit --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
+ cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
status.check: false
records.collect: true
stdout: show
@@ -467,7 +467,7 @@ steps:
- if: ${cli['tag-mapping'] == null || cli['tag-mapping'] == ''}
run.fcli:
run_audit:
- cmd: "aviator ssc audit --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
+ cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}"
status.check: false
records.collect: true
stdout: show