Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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()
Expand All @@ -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";
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<DastAuditWorkItem> workItems;
private List<DastAuditWorkItem> workItems = new ArrayList<>();

private EligibilityResultBuilder addWorkItem(DastAuditWorkItem workItem) {
if (workItems == null) workItems = new ArrayList<>();
workItems.add(workItem);
return this;
}
Expand All @@ -82,6 +84,11 @@ private EligibilityResultBuilder incrementAlreadyProcessed() {
alreadyProcessed++;
return this;
}

private EligibilityResultBuilder incrementHumanAudited() {
humanAudited++;
return this;
}
}
}

Expand Down Expand Up @@ -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<DastAuditStreamResult> 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
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -255,15 +270,42 @@ 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));
}
}
return result.build();
}

private static boolean isProcessedByAviator(AuditIssue auditIssue) {
return Constants.PROCESSED_BY_AVIATOR.equalsIgnoreCase(
auditIssue.getTags().get(Constants.AVIATOR_STATUS_TAG_ID));
Map<String, String> 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<String, String> 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<String, String> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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());
}
}
Loading
Loading