Skip to content
Open
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 @@ -600,7 +600,7 @@
"name":"com.fortify.cli.aviator.config.TagMappingConfig",
"allDeclaredFields":true,
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","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":"<init>","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",
Expand Down Expand Up @@ -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":"<init>","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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"name":"com.fortify.cli.aviator.config.TagMappingConfig",
"allDeclaredFields":true,
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","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":"<init>","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",
Expand Down Expand Up @@ -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":"<init>","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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class NativeReflectConfigTest {
private static final List<String> 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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,8 @@ public TagMappingConfig getDefaultTagMappingConfig() {
}
return defaultTagMappingConfig;
}

public TagMappingConfig getDefaultDastTagMappingConfig() {
return getDefaultTagMappingConfig();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ private static TagMappingConfig loadTagMappingConfig(String tagMappingFilePath)
tagMappingConfig = AviatorConfigManager.getInstance().getDefaultTagMappingConfig();
}

tagMappingConfig.validate();
return tagMappingConfig;
return tagMappingConfig.resolveForSast();
}

private static Map<String, String> buildIssueCategoryLookup(List<Vulnerability> vulnerabilities) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.Locale;

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";
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
@@ -0,0 +1,243 @@
/*
* 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.HashSet;
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.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.
*/
public final class DastAuditFPR {
private static final Logger LOG = LoggerFactory.getLogger(DastAuditFPR.class);

private DastAuditFPR() {}

@Builder
private record EligibilityResult(
List<DastAuditWorkItem> workItems,
int missingId,
int duplicate,
int suppressed,
int alreadyProcessed) {
private static class EligibilityResultBuilder {
private List<DastAuditWorkItem> 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 {
CompletableFuture<DastAuditStreamResult> run(
DastAuditStreamConfig config, List<DastAuditWorkItem> workItems, int totalReportedIssues);
}

public static DastAuditFprResult audit(
FprHandle fprHandle,
DastAuditStreamConfig config,
TagMappingConfig tagMappingConfig,
StreamRunner streamRunner) {
tagMappingConfig.validateForDast();
var auditProcessor = new AuditProcessor(fprHandle);
Map<String, AuditIssue> auditIssues = auditProcessor.processAuditXML();
var sessions = new StreamingWebInspectParser(fprHandle).parseSessions();
EligibilityResult eligibility = eligibleWorkItems(sessions, auditIssues);
List<DastAuditWorkItem> 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.alreadyProcessed());

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<String, AuditResponse> successfulResponses = new LinkedHashMap<>();
int truePositives = 0;
int falsePositivesSuppressed = 0;
int likelyFalsePositives = 0;
int failed = 0;
int serverSkipped = 0;
Set<String> 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 = (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);
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 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) {
boolean tierOne = "GOLD".equalsIgnoreCase(response.getTier());
return Boolean.TRUE.equals(tagMappingConfig.getResult(
tierOne, TagMappingConfig.ResultType.FP).getSuppress());
}

private static EligibilityResult eligibleWorkItems(
List<DastSession> sessions,
Map<String, AuditIssue> auditIssues) {
var result = EligibilityResult.builder();
var seenIssueIds = new HashSet<String>();
for (var session : sessions) {
for (var issue : session.getIssues()) {
String issueId = issue.getId();
if (issueId == null || issueId.isBlank()) {
result.incrementMissingId();
LOG.debug("Skipping DAST finding without an issue ID in session {}", session.getRequestId());
continue;
}
if (!seenIssueIds.add(issueId)) {
result.incrementDuplicate();
LOG.debug("Skipping duplicate DAST issue {} in session {}", issueId, session.getRequestId());
continue;
}
AuditIssue auditIssue = auditIssues.get(issueId);
if (auditIssue != null && auditIssue.isSuppressed()) {
result.incrementSuppressed();
LOG.debug("Skipping DAST issue {} because it is already suppressed", issueId);
continue;
}
if (auditIssue != null && isProcessedByAviator(auditIssue)) {
result.incrementAlreadyProcessed();
LOG.debug("Skipping DAST issue {} because it is already processed by Aviator", 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));
}

private static DastAuditFprResult emptyResult(int totalReported, int skipped) {
return DastAuditFprResult.builder()
.status(DastAuditFprStatus.SKIPPED)
.message("No eligible DAST findings to audit")
.totalReported(totalReported)
.skipped(skipped)
.build();
}
}
Loading
Loading