-
Notifications
You must be signed in to change notification settings - Fork 34
feat:Add DAST audit command and related functionality #1078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/v3.x/aviator/26.4
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"; | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<DastAuditWorkItem> workItems, | ||
| int missingId, | ||
| int duplicate, | ||
| int suppressed, | ||
| int processed) {} | ||
|
|
||
| @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.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<String, AuditResponse> successfulResponses = new LinkedHashMap<>(); | ||
| int truePositives = 0; | ||
| int falsePositivesSuppressed = 0; | ||
| int likelyFalsePositives = 0; | ||
| int failed = 0; | ||
| int serverSkipped = 0; | ||
| Set<String> 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use import |
||
| 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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Way too many constructor arguments; especially where arguments have same type, it's easy to accidentally put them in wrong order. Use Lombok |
||
| 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<com.fortify.cli.aviator.dast.DastSession> sessions, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use import |
||
| Map<String, AuditIssue> auditIssues) { | ||
| var workItems = new ArrayList<DastAuditWorkItem>(); | ||
| var seenIssueIds = new java.util.HashSet<String>(); | ||
| 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++; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here and in for example |
||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Please consider using Lombok |
||
| } | ||
|
|
||
| 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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned elsewhere, way too many constructor arguments. It's completely unclear what each |
||
| null, "SKIPPED", "No eligible DAST findings to audit", totalReported, 0, 0, 0, | ||
| 0, 0, 0, skipped, 0, 0, 0, false, null, null); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned elsewhere, way to many constructor args. Consider Lombok Note: I guess similar large records exist for SAST; consider improving those as well in separate PR. |
||
| 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 | ||
| ) {} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use proper import (here, and any other similar occurences)