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 d18a7a696a..25132f1cc5 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,10 +12,9 @@ */ 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.audit.model.AuditTier; import com.fortify.cli.aviator.grpc.DastAuditResult; import com.fortify.cli.aviator.util.Constants; @@ -28,28 +27,22 @@ 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()) + .issueId(result.issueId()) + .status(result.status()) + .statusMessage(result.statusMessage()) .build(); } - String confidence = normalizedConfidence(success.confidence()); + AuditTier tier = success.tier(); + boolean tierOne = tier == AuditTier.GOLD; 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"; + prediction = tierOne ? Constants.AVIATOR_REMEDIATION_REQUIRED : Constants.AVIATOR_LIKELY_TP; } else { tagValue = Constants.NOT_AN_ISSUE; - prediction = Constants.AVIATOR_LIKELY_FP; - tier = "SILVER"; + prediction = tierOne ? Constants.AVIATOR_NOT_AN_ISSUE : Constants.AVIATOR_LIKELY_FP; } String comment = success.finalComment() != null && !success.finalComment().isBlank() @@ -58,19 +51,11 @@ public static AuditResponse toAuditResponse(DastAuditResult result) { return AuditResponse.builder() .issueId(success.issueId()) .status("SUCCESS") - .tier(tier) + .tier(tier.name()) .aviatorPredictionTag(prediction) .isAviatorProcessed(true) .auditResult(AuditResult.builder().tagValue(tagValue).comment(comment).build()) .build(); } - private static String normalizedConfidence(String confidence) { - if (confidence == null) return "LOW"; - String normalized = confidence.toUpperCase(Locale.ROOT); - return switch (normalized) { - case "HIGH", "MEDIUM", "LOW" -> normalized; - 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 index 317888867b..ebc66874c3 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 @@ -24,7 +24,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fortify.cli.aviator._common.exception.AviatorTechnicalException; import com.fortify.cli.aviator.audit.model.AuditResponse; +import com.fortify.cli.aviator.audit.model.AuditTier; import com.fortify.cli.aviator.config.TagMappingConfig; import com.fortify.cli.aviator.dast.DastSession; import com.fortify.cli.aviator.dast.StreamingWebInspectParser; @@ -53,12 +55,12 @@ private record EligibilityResult( int missingId, int duplicate, int suppressed, - int alreadyProcessed) { + int alreadyProcessed, + int humanAudited) { private static class EligibilityResultBuilder { - private List workItems; + private List workItems = new ArrayList<>(); private EligibilityResultBuilder addWorkItem(DastAuditWorkItem workItem) { - if (workItems == null) workItems = new ArrayList<>(); workItems.add(workItem); return this; } @@ -82,6 +84,11 @@ private EligibilityResultBuilder incrementAlreadyProcessed() { alreadyProcessed++; return this; } + + private EligibilityResultBuilder incrementHumanAudited() { + humanAudited++; + return this; + } } } @@ -114,16 +121,24 @@ public static DastAuditFprResult audit( 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={})", + + "(missingId={}, duplicate={}, suppressed={}, alreadyProcessed={}, humanAudited={})", totalReported, workItems.size(), locallySkipped, eligibility.missingId(), - eligibility.duplicate(), eligibility.suppressed(), eligibility.alreadyProcessed()); + eligibility.duplicate(), eligibility.suppressed(), eligibility.alreadyProcessed(), + eligibility.humanAudited()); 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(); + CompletableFuture streamFuture = streamRunner.run(config, workItems, totalReported); + if (streamFuture == null) { + throw new AviatorTechnicalException("DAST audit stream did not return a completion future"); + } + DastAuditStreamResult streamResult = streamFuture.join(); + if (streamResult == null) { + throw new AviatorTechnicalException("DAST audit stream completed without a result"); + } ResponseSummary summary = summarizeResponses(streamResult, workItems, tagMappingConfig); var updatedFile = summary.successfulResponses().isEmpty() ? null @@ -221,7 +236,7 @@ private static DastAuditFprResult buildResult( } private static boolean isSuppressedFalsePositive(AuditResponse response, TagMappingConfig tagMappingConfig) { - boolean tierOne = "GOLD".equalsIgnoreCase(response.getTier()); + boolean tierOne = AuditTier.fromServerValue(response.getTier()) == AuditTier.GOLD; return Boolean.TRUE.equals(tagMappingConfig.getResult( tierOne, TagMappingConfig.ResultType.FP).getSuppress()); } @@ -255,6 +270,11 @@ private static EligibilityResult eligibleWorkItems( LOG.debug("Skipping DAST issue {} because it is already processed by Aviator", issueId); continue; } + if (auditIssue != null && isHumanAudited(auditIssue)) { + result.incrementHumanAudited(); + LOG.debug("Skipping DAST issue {} because it is already audited by a human", issueId); + continue; + } result.addWorkItem(new DastAuditWorkItem(session, issue)); } } @@ -262,8 +282,30 @@ private static EligibilityResult eligibleWorkItems( } private static boolean isProcessedByAviator(AuditIssue auditIssue) { - return Constants.PROCESSED_BY_AVIATOR.equalsIgnoreCase( - auditIssue.getTags().get(Constants.AVIATOR_STATUS_TAG_ID)); + Map tags = getTags(auditIssue); + return Constants.PROCESSED_BY_AVIATOR.equalsIgnoreCase(tags.get(Constants.AVIATOR_STATUS_TAG_ID)) + || tags.containsKey(Constants.AVIATOR_EXPECTED_OUTCOME_TAG_ID); + } + + private static boolean isHumanAudited(AuditIssue auditIssue) { + Map tags = getTags(auditIssue); + return isAuditDecision(tags.get(Constants.AUDITOR_STATUS_TAG_ID)) + || isAuditDecision(tags.get(Constants.FOD_TAG_ID)) + || isAnalysisDecision(tags.get(Constants.ANALYSIS_TAG_ID)); + } + + private static Map getTags(AuditIssue auditIssue) { + return auditIssue.getTags() == null ? Map.of() : auditIssue.getTags(); + } + + private static boolean isAuditDecision(String value) { + return value != null && !value.isBlank() + && !Constants.PENDING_REVIEW.equalsIgnoreCase(value) + && !"Pending Review".equalsIgnoreCase(value); + } + + private static boolean isAnalysisDecision(String value) { + return isAuditDecision(value) && !"Not Set".equalsIgnoreCase(value); } private static DastAuditFprResult emptyResult(int totalReported, int skipped) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditTier.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditTier.java new file mode 100644 index 0000000000..be63d1100f --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditTier.java @@ -0,0 +1,29 @@ +/* + * 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.model; + +/** Supported Aviator audit decision tiers. */ +public enum AuditTier { + GOLD, + SILVER; + + /** + * Parses a server-provided tier, defaulting unknown values to the conservative tier. + * + * @param value server-provided tier value + * @return parsed tier, or {@link #SILVER} when missing or unknown + */ + public static AuditTier fromServerValue(String value) { + return GOLD.name().equalsIgnoreCase(value) ? GOLD : SILVER; + } +} 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 7b5ee12628..e38e5d3209 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 @@ -56,6 +56,7 @@ import com.fortify.cli.aviator._common.exception.AviatorTechnicalException; import com.fortify.cli.aviator.audit.model.AuditResponse; +import com.fortify.cli.aviator.audit.model.AuditTier; import com.fortify.cli.aviator.config.TagMappingConfig; import com.fortify.cli.aviator.fpr.model.AuditIssue; import com.fortify.cli.aviator.fpr.model.FPRInfo; @@ -842,7 +843,7 @@ private void applyDastAuditResponse(Element issueElement, AuditResponse response private TagMappingConfig.Result getDastResultConfig(AuditResponse response, TagMappingConfig tagMappingConfig) { - boolean tierOne = "GOLD".equalsIgnoreCase(response.getTier()); + boolean tierOne = AuditTier.fromServerValue(response.getTier()) == AuditTier.GOLD; String tagValue = response.getAuditResult().getTagValue(); if (Constants.NOT_AN_ISSUE.equalsIgnoreCase(tagValue)) { return tagMappingConfig.getResult(tierOne, TagMappingConfig.ResultType.FP); 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 a9d8c27780..27f06ddda9 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 @@ -13,6 +13,7 @@ package com.fortify.cli.aviator.grpc; import com.fortify.aviator.dastaudit.DastAuditResponse; +import com.fortify.cli.aviator.audit.model.AuditTier; /** * Maps a DAST audit response to the issue associated with its request ID. @@ -61,7 +62,7 @@ static DastAuditResult map(DastAuditResponse response, String expectedIssueId) { .remediationAdvice(decision.getRemediationAdvice()) .finalComment(decision.getFinalComment()) .tagValue(decision.getTagValue()) - .tier(decision.getTier()) + .tier(AuditTier.fromServerValue(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 cd2b0fe8ea..0c36e052e2 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,6 +12,8 @@ */ package com.fortify.cli.aviator.grpc; +import com.fortify.cli.aviator.audit.model.AuditTier; + import lombok.Builder; /** Domain representation of one terminal DAST audit response. */ @@ -29,8 +31,12 @@ record Success( String remediationAdvice, String finalComment, String tagValue, - String tier + AuditTier tier ) implements DastAuditResult { + public Success { + tier = tier == null ? AuditTier.SILVER : tier; + } + @Override public String status() { return "SUCCESS"; 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 2ead662bed..424612fb8b 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 @@ -16,29 +16,30 @@ import org.junit.jupiter.api.Test; +import com.fortify.cli.aviator.audit.model.AuditTier; import com.fortify.cli.aviator.grpc.DastAuditResult; import com.fortify.cli.aviator.util.Constants; class DastAuditDecisionMapperTest { @Test - void unknownConfidenceFalsePositiveRemainsUnsuppressed() { + void serverGoldTierControlsFalsePositiveMappingRegardlessOfConfidence() { var result = DastAuditResult.Success.builder() .issueId("DAST-1") .confidence("UNKNOWN") .reasoning("reason") .finalComment("comment") .tagValue("bad") - .tier("GOLD") + .tier(AuditTier.GOLD) .build(); var response = DastAuditDecisionMapper.toAuditResponse(result); - assertEquals("SILVER", response.getTier()); - assertEquals(Constants.AVIATOR_LIKELY_FP, response.getAviatorPredictionTag()); + assertEquals("GOLD", response.getTier()); + assertEquals(Constants.AVIATOR_NOT_AN_ISSUE, response.getAviatorPredictionTag()); } @Test - void highConfidenceFalsePositiveIsSuppressible() { + void missingServerTierDefaultsToSilverRegardlessOfConfidence() { var result = DastAuditResult.Success.builder() .issueId("DAST-1") .confidence("HIGH") @@ -48,7 +49,34 @@ void highConfidenceFalsePositiveIsSuppressible() { var response = DastAuditDecisionMapper.toAuditResponse(result); - assertEquals("GOLD", response.getTier()); - assertEquals(Constants.AVIATOR_NOT_AN_ISSUE, response.getAviatorPredictionTag()); + assertEquals("SILVER", response.getTier()); + assertEquals(Constants.AVIATOR_LIKELY_FP, response.getAviatorPredictionTag()); + } + + @Test + void missingDomainTierDefaultsToSilver() { + var result = DastAuditResult.Success.builder() + .issueId("DAST-1") + .build(); + + var response = DastAuditDecisionMapper.toAuditResponse(result); + + assertEquals("SILVER", response.getTier()); + assertEquals(Constants.AVIATOR_LIKELY_FP, response.getAviatorPredictionTag()); + } + + @Test + void serverSilverTierControlsTruePositivePrediction() { + var result = DastAuditResult.Success.builder() + .issueId("DAST-1") + .truePositive(true) + .confidence("HIGH") + .tier(AuditTier.SILVER) + .build(); + + var response = DastAuditDecisionMapper.toAuditResponse(result); + + assertEquals("SILVER", response.getTier()); + assertEquals(Constants.AVIATOR_LIKELY_TP, 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 index a33c93b6f2..5349cd7977 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 @@ -14,6 +14,7 @@ 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 static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.charset.StandardCharsets; @@ -29,6 +30,8 @@ import org.junit.jupiter.api.io.TempDir; import com.fortify.cli.aviator._common.config.AviatorConfigManager; +import com.fortify.cli.aviator._common.exception.AviatorTechnicalException; +import com.fortify.cli.aviator.audit.model.AuditTier; import com.fortify.cli.aviator.config.TagMappingConfig; import com.fortify.cli.aviator.grpc.DastAuditResult; import com.fortify.cli.aviator.grpc.DastAuditStreamConfig; @@ -48,7 +51,7 @@ void auditsEligibleFindingAndWritesConservativeXml() throws Exception { try (FprHandle handle = new FprHandle(fpr)) { result = DastAuditFPR.audit(handle, config, defaultTagMapping(), (ignoredConfig, items, total) -> CompletableFuture.completedFuture(DastAuditStreamResult.builder() - .results(List.of(successResult(false, "HIGH"))) + .results(List.of(successResult(false, "HIGH", AuditTier.GOLD))) .reservedQuota(1) .build())); } @@ -88,7 +91,7 @@ void customTagMappingControlsFinalTagAndSuppression() throws Exception { ResourceUtil.loadYamlFile(tagMapping.toFile(), TagMappingConfig.class), (ignoredConfig, items, total) -> CompletableFuture.completedFuture(DastAuditStreamResult.builder() - .results(List.of(successResult(false, "MEDIUM"))) + .results(List.of(successResult(false, "MEDIUM", AuditTier.SILVER))) .reservedQuota(1) .build())); } @@ -118,7 +121,7 @@ void writesOnlyIssuesUpdatedByCurrentAudit() throws Exception { try (FprHandle handle = new FprHandle(fpr)) { DastAuditFPR.audit(handle, config, defaultTagMapping(), (ignoredConfig, items, total) -> CompletableFuture.completedFuture(DastAuditStreamResult.builder() - .results(List.of(successResult(true, "HIGH"))) + .results(List.of(successResult(true, "HIGH", AuditTier.GOLD))) .reservedQuota(1) .build())); } @@ -148,11 +151,87 @@ handle, config, defaultTagMapping(), (ignoredConfig, items, total) -> } } - private DastAuditResult.Success successResult(boolean truePositive, String confidence) { + @Test + void excludesSuppressedAviatorProcessedAndHumanAuditedFindings() throws Exception { + Path fpr = createEligibilityFpr(); + + try (FprHandle handle = new FprHandle(fpr)) { + DastAuditFprResult result = DastAuditFPR.audit( + handle, streamConfig(), defaultTagMapping(), (ignoredConfig, items, total) -> { + assertEquals(List.of("DAST-5"), items.stream().map(item -> item.issue().getId()).toList()); + return CompletableFuture.completedFuture(DastAuditStreamResult.builder() + .results(List.of(successResult("DAST-5", true, "HIGH", AuditTier.GOLD))) + .reservedQuota(1) + .build()); + }); + + assertEquals(5, result.totalReported()); + assertEquals(1, result.submitted()); + assertEquals(4, result.skipped()); + } + } + + @Test + void allExcludedFindingsReturnSkippedWithoutStartingStream() throws Exception { + Path fpr = createFpr(); + try (FileSystem zip = FileSystems.newFileSystem(fpr)) { + Files.writeString(zip.getPath("/audit.xml"), """ + + + + """); + } + + try (FprHandle handle = new FprHandle(fpr)) { + DastAuditFprResult result = DastAuditFPR.audit( + handle, streamConfig(), defaultTagMapping(), (ignoredConfig, items, total) -> { + throw new AssertionError("Stream must not start when all findings are excluded"); + }); + + assertEquals(DastAuditFprStatus.SKIPPED, result.status()); + assertEquals(1, result.totalReported()); + assertEquals(1, result.skipped()); + assertEquals(0, result.submitted()); + } + } + + @Test + void missingStreamFutureProducesTechnicalError() throws Exception { + Path fpr = createFpr(); + + try (FprHandle handle = new FprHandle(fpr)) { + AviatorTechnicalException exception = assertThrows(AviatorTechnicalException.class, + () -> DastAuditFPR.audit(handle, streamConfig(), defaultTagMapping(), + (ignoredConfig, items, total) -> null)); + + assertEquals("DAST audit stream did not return a completion future", exception.getMessage()); + } + } + + @Test + void missingStreamResultProducesTechnicalError() throws Exception { + Path fpr = createFpr(); + + try (FprHandle handle = new FprHandle(fpr)) { + AviatorTechnicalException exception = assertThrows(AviatorTechnicalException.class, + () -> DastAuditFPR.audit(handle, streamConfig(), defaultTagMapping(), + (ignoredConfig, items, total) -> CompletableFuture.completedFuture(null))); + + assertEquals("DAST audit stream completed without a result", exception.getMessage()); + } + } + + private DastAuditResult.Success successResult(boolean truePositive, String confidence, AuditTier tier) { + return successResult("DAST-1", truePositive, confidence, tier); + } + + private DastAuditResult.Success successResult( + String issueId, boolean truePositive, String confidence, AuditTier tier) { return DastAuditResult.Success.builder() - .issueId("DAST-1") + .issueId(issueId) .truePositive(truePositive) .confidence(confidence) + .tier(tier) .reasoning("reason") .finalComment("comment") .build(); @@ -182,4 +261,34 @@ private Path createFpr() throws Exception { } return fpr; } + + private Path createEligibilityFpr() throws Exception { + Path fpr = tempDir.resolve("dast-eligibility.fpr"); + try (FileSystem zip = FileSystems.newFileSystem(fpr, Map.of("create", "true"))) { + Files.writeString(zip.getPath("/webinspect.xml"), """ + https://example.test + Suppressed4 + Aviator status4 + Legacy Aviator outcome4 + Human audited4 + Eligible4 + + """, StandardCharsets.UTF_8); + Files.writeString(zip.getPath("/audit.xml"), """ + + + + PROCESSED_BY_AVIATOR + + + Not an Issue + + + Exploitable + + + """, 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/grpc/DastAuditResponseMapperTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/grpc/DastAuditResponseMapperTest.java index 45b7424972..54bc40c959 100644 --- 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 @@ -19,6 +19,7 @@ import com.fortify.aviator.dastaudit.DastAuditDecision; import com.fortify.aviator.dastaudit.DastAuditResponse; +import com.fortify.cli.aviator.audit.model.AuditTier; class DastAuditResponseMapperTest { @Test @@ -36,6 +37,13 @@ void usesIssueIdAssociatedWithRequest() { assertEquals("DAST-1", result.issueId()); assertEquals("SUCCESS", result.status()); assertEquals(true, success.truePositive()); + assertEquals(AuditTier.SILVER, success.tier()); + } + + @Test + void parsesKnownTierAndDefaultsUnknownTierToSilver() { + assertEquals(AuditTier.GOLD, mapTier("gold")); + assertEquals(AuditTier.SILVER, mapTier("BRONZE")); } @Test @@ -68,4 +76,13 @@ void preservesSkippedResponseAsDistinctVariant() { assertEquals("SKIPPED", result.status()); assertEquals("Quota exceeded", result.statusMessage()); } + + private AuditTier mapTier(String tier) { + var response = DastAuditResponse.newBuilder() + .setDastIssueId("DAST-1") + .setStatus("SUCCESS") + .setDecision(DastAuditDecision.newBuilder().setTier(tier)) + .build(); + return ((DastAuditResult.Success) DastAuditResponseMapper.map(response, "DAST-1")).tier(); + } } \ No newline at end of file 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 ab7c7d0ef8..895a08d4c1 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 @@ -12,8 +12,6 @@ */ 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; @@ -52,7 +50,6 @@ 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.artifact.helper.SSCArtifactDescriptor; import com.fortify.cli.ssc.system_state.helper.SSCJobDescriptor; import com.fortify.cli.ssc.system_state.helper.SSCJobHelper; @@ -87,9 +84,8 @@ public JsonNode getJsonNode(UnirestInstance unirest) { refreshMetricsIfNeeded(unirest, appVersion, logger); - SSCArtifactDescriptor artifact = getLatestDASTArtifact(unirest, appVersion.getVersionId()); - downloadedFpr = AviatorSSCFprTransferHelper.downloadArtifactFpr( - unirest, artifact, logger, progressWriter); + downloadedFpr = AviatorSSCFprTransferHelper.downloadCurrentStateFpr( + unirest, appVersion, logger, progressWriter); DastAuditFprResult result = auditFpr( downloadedFpr, appVersion, session, logger, tagMappingConfig); 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 8403f8dcde..d57fff234e 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 @@ -28,22 +28,42 @@ import kong.unirest.UnirestInstance; /** - * Shared SSC transfer operations for artifact-specific DAST FPR workflows. + * Shared SSC transfer operations for DAST FPR workflows. */ public final class AviatorSSCFprTransferHelper { private AviatorSSCFprTransferHelper() {} + public static Path downloadCurrentStateFpr( + UnirestInstance unirest, + SSCAppVersionDescriptor appVersion, + IAviatorLogger logger, + IProgressWriter progressWriter) throws IOException { + logger.progress("Status: Downloading current FPR state from SSC for app version %s:%s (id=%s)", + appVersion.getApplicationName(), appVersion.getVersionName(), appVersion.getVersionId()); + return downloadFpr(unirest, "aviator_" + appVersion.getVersionId() + "_", + SSCUrls.DOWNLOAD_CURRENT_FPR(appVersion.getVersionId(), true), progressWriter); + } + public static Path downloadArtifactFpr( UnirestInstance unirest, SSCArtifactDescriptor artifact, IAviatorLogger logger, IProgressWriter progressWriter) throws IOException { - Path fprPath = Files.createTempFile("aviator_" + artifact.getId() + "_", ".fpr"); + logger.progress("Status: Downloading FPR from SSC (artifact id=%s)", artifact.getId()); + return downloadFpr(unirest, "aviator_" + artifact.getId() + "_", + SSCUrls.DOWNLOAD_ARTIFACT(artifact.getId(), true), progressWriter); + } + + private static Path downloadFpr( + UnirestInstance unirest, + String filePrefix, + String downloadUrl, + IProgressWriter progressWriter) throws IOException { + Path fprPath = Files.createTempFile(filePrefix, ".fpr"); try { - logger.progress("Status: Downloading FPR from SSC (artifact id=%s)", artifact.getId()); SSCFileTransferHelper.download( unirest, - SSCUrls.DOWNLOAD_ARTIFACT(artifact.getId(), true), + downloadUrl, fprPath.toFile(), SSCFileTransferHelper.ISSCAddDownloadTokenFunction.ROUTEPARAM_DOWNLOADTOKEN, progressWriter); 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 69bcc09989..f6f645bcc0 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 @@ -179,7 +179,7 @@ fcli.ssc.appversion.create.refresh-timeout = Time-out for refreshing application 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, \ +fcli.aviator.ssc.audit-dast.usage.description = Downloads the current application version 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.