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