From f9fc70d0c0632268ad0ad687ecd18807e1f2e037 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Wed, 26 Aug 2026 22:38:47 +0530 Subject: [PATCH 1/9] feat: add force re-audit and safe remediation matching --- .../fortify/cli/aviator/audit/AuditFPR.java | 7 +- .../cli/aviator/audit/IssueAuditor.java | 85 +++++-- .../aviator/audit/model/AuditFprOptions.java | 1 + .../fortify/cli/aviator/fpr/FPRProcessor.java | 13 +- .../fpr/processor/RemediationProcessor.java | 59 ++++- .../aviator/util/FuzzyContextSearcher.java | 84 ++++--- .../fortify/cli/aviator/util/StringUtil.java | 11 + .../cli/aviator/audit/IssueAuditorTest.java | 106 +++++++++ .../cli/aviator/fpr/FPRProcessorTest.java | 14 ++ .../processor/RemediationProcessorTest.java | 143 ++++++++++++ .../util/FuzzyContextSearcherTest.java | 41 ++++ .../ssc/cli/cmd/AviatorSSCAuditCommand.java | 19 +- .../ssc/helper/AviatorSSCAuditHelper.java | 41 +++- .../aviator/i18n/AviatorMessages.properties | 1 + .../cli/cmd/AviatorSSCAuditCommandTest.java | 6 + .../ssc/helper/AviatorSSCAuditHelperTest.java | 207 ++++++++++++++++++ 16 files changed, 747 insertions(+), 91 deletions(-) create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java create mode 100644 fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java 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 5888d18affc..805e342444f 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 @@ -74,7 +74,7 @@ public static FPRAuditResult auditFPR(AuditFprOptions options) Map auditResponses = new ConcurrentHashMap<>(); AuditOutcome auditOutcome = performAviatorAudit( parsedData, options.getLogger(), options.getToken(), options.getAppVersion(), options.getUrl(), options.getSscAppName(), options.getSscAppVersion(), - auditResponses, filterSelection, options.getFprHandle(), options.getFolderPriorityOrder(), sourceDecoder + auditResponses, filterSelection, options.getFprHandle(), options.getFolderPriorityOrder(), sourceDecoder, options.isForceReaudit() ); // --- STAGE 4: FINALIZATION --- @@ -131,7 +131,7 @@ private static AuditOutcome performAviatorAudit( ParsedFprData parsedData, IAviatorLogger logger, String token, String appVersion, String url, String sscAppName, String sscAppVersion, Map auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle, - List folderPriorityOrder, ISourceDecoder sourceDecoder) { + List folderPriorityOrder, ISourceDecoder sourceDecoder, boolean forceReaudit) { SourceLanguageResolver sourceLanguageResolver = new SourceLanguageResolver(parsedData.streamingFVDLProcessor.getFvdlMetadata()); parsedData.streamingFVDLProcessor.getFvdlMetadata().clearSourceFileTypeIndexes(); @@ -148,7 +148,8 @@ private static AuditOutcome performAviatorAudit( folderPriorityOrder, sourceLanguageResolver, sourceDecoder, - parsedData.streamingFVDLProcessor.getFvdlMetadata() + parsedData.streamingFVDLProcessor.getFvdlMetadata(), + forceReaudit ); return issueAuditor.performAudit( auditResponsesToFill, token, appVersion, parsedData.fprInfo.getBuildId(), url, fprHandle diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java index dc5b6897892..deae84bfbc5 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java @@ -86,6 +86,7 @@ public class IssueAuditor { private final SourceLanguageResolver sourceLanguageResolver; private final ISourceDecoder sourceDecoder; private final FVDLMetadata fvdlMetadata; + private final boolean forceReaudit; private final IAviatorLogger logger; private final List customPriorityOrder; @@ -95,7 +96,7 @@ public IssueAuditor(List vulnerabilities, AuditProcessor auditPro FilterSelection filterSelection, IAviatorLogger logger, List customPriorityOrder, SourceLanguageResolver sourceLanguageResolver) { this(vulnerabilities, auditProcessor, auditIssueMap, fprInfo, SSCApplicationName, SSCApplicationVersion, - filterSelection, logger, customPriorityOrder, sourceLanguageResolver, SourceDecoders.defaults(), null); + filterSelection, logger, customPriorityOrder, sourceLanguageResolver, SourceDecoders.defaults(), null, false); } public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, @@ -103,6 +104,15 @@ public IssueAuditor(List vulnerabilities, AuditProcessor auditPro FilterSelection filterSelection, IAviatorLogger logger, List customPriorityOrder, SourceLanguageResolver sourceLanguageResolver, ISourceDecoder sourceDecoder, FVDLMetadata fvdlMetadata) { + this(vulnerabilities, auditProcessor, auditIssueMap, fprInfo, SSCApplicationName, SSCApplicationVersion, + filterSelection, logger, customPriorityOrder, sourceLanguageResolver, sourceDecoder, fvdlMetadata, false); + } + + public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, + FPRInfo fprInfo, String SSCApplicationName, String SSCApplicationVersion, + FilterSelection filterSelection, IAviatorLogger logger, List customPriorityOrder, + SourceLanguageResolver sourceLanguageResolver, ISourceDecoder sourceDecoder, + FVDLMetadata fvdlMetadata, boolean forceReaudit) { this.logger = logger; this.customPriorityOrder = customPriorityOrder; this.MAX_PER_CATEGORY = Constants.MAX_PER_CATEGORY; @@ -120,6 +130,7 @@ public IssueAuditor(List vulnerabilities, AuditProcessor auditPro this.sourceLanguageResolver = sourceLanguageResolver; this.sourceDecoder = Objects.requireNonNull(sourceDecoder, "sourceDecoder"); this.fvdlMetadata = fvdlMetadata; + this.forceReaudit = forceReaudit; this.analysisTag = fprInfo.getFilterTemplate().getTagDefinitions().stream().filter(t -> "Analysis".equalsIgnoreCase(t.getName())).findFirst().orElse(null); this.resultsTag = resolveResultTag("", "", analysisTag); } @@ -243,33 +254,71 @@ private ConcurrentLinkedDeque prepareAndFilterPrompts() { private boolean shouldInclude(UserPrompt userPrompt) { + if (forceReaudit && isProcessedByAviator(userPrompt)) { + if (isProtectedFromForceReaudit(userPrompt)) { + LOG.debug("Skipping force re-audit for suppressed or manually audited issue ID: {}", + userPrompt.getIssueData().getInstanceID()); + return false; + } + LOG.debug("Including previously processed Aviator issue for force re-audit: {}", + userPrompt.getIssueData().getInstanceID()); + return true; + } if (isAudited(userPrompt)) { LOG.debug("Skipping already audited issue ID: {}", userPrompt.getIssueData().getInstanceID()); return false; } - if (humanAuditTag != null) { - String issueId = userPrompt.getIssueData().getInstanceID(); - String status = Optional.ofNullable(auditIssueMap.get(issueId)).map(AuditIssue::getTags).map(tags -> tags.get("604f0fbe-b5fe-47cd-a9cb-587ad8ebe93a")).orElse(null); - if (!StringUtil.isEmpty(status) && !Constants.PENDING_REVIEW.equalsIgnoreCase(status)) { - LOG.debug("Skipping because already manually audited: {}", issueId); - return false; - } + if (humanAuditTag != null && isManuallyAudited(userPrompt)) { + LOG.debug("Skipping because already manually audited: {}", userPrompt.getIssueData().getInstanceID()); + return false; } - if (aviatorStatusTag != null) { - String issueId = userPrompt.getIssueData().getInstanceID(); - String status = Optional.ofNullable(auditIssueMap.get(issueId)).map(AuditIssue::getTags).map(tags -> tags.get("FB7B0462-2C2E-46D9-811A-DCC1F3C83051")).orElse(null); - if (!StringUtil.isEmpty(status) && Constants.PROCESSED_BY_AVIATOR.equalsIgnoreCase(status)) { - LOG.debug("Skipping already PROCESSED_BY_AVIATOR: {}", issueId); - return false; - } + if (aviatorStatusTag != null && isProcessedByAviator(userPrompt)) { + LOG.debug("Skipping already processed by Aviator: {}", userPrompt.getIssueData().getInstanceID()); + return false; } return true; } + private boolean isManuallyAudited(UserPrompt userPrompt) { + String issueId = userPrompt.getIssueData().getInstanceID(); + String status = Optional.ofNullable(auditIssueMap.get(issueId)).map(AuditIssue::getTags) + .map(tags -> tags.get(Constants.FOD_TAG_ID)).orElse(null); + return !StringUtil.isPendingReviewValue(status); + } + + private boolean isProcessedByAviator(UserPrompt userPrompt) { + String issueId = userPrompt.getIssueData().getInstanceID(); + String status = Optional.ofNullable(auditIssueMap.get(issueId)).map(AuditIssue::getTags) + .map(tags -> tags.get(Constants.AVIATOR_STATUS_TAG_ID)).orElse(null); + return !StringUtil.isEmpty(status) && Constants.PROCESSED_BY_AVIATOR.equalsIgnoreCase(status); + } + + private boolean isProtectedFromForceReaudit(UserPrompt userPrompt) { + AuditIssue auditIssue = auditIssueMap.get(userPrompt.getIssueData().getInstanceID()); + if (auditIssue == null) { + return false; + } + if (auditIssue.isSuppressed()) { + return true; + } + + Map tags = auditIssue.getTags(); + if (tags == null) { + return false; + } + + String auditorStatus = tags.get(Constants.AUDITOR_STATUS_TAG_ID); + if (!StringUtil.isPendingReviewValue(auditorStatus)) { + return true; + } + + return isManuallyAudited(userPrompt); + } + private boolean isAudited(UserPrompt userPrompt) { String issueId = userPrompt.getIssueData().getInstanceID(); AuditIssue auditIssue = auditIssueMap.get(issueId); @@ -282,7 +331,7 @@ private boolean isAudited(UserPrompt userPrompt) { String auditorStatusTag = Constants.AUDITOR_STATUS_TAG_ID; String auditorStatusValue = tags.get(auditorStatusTag); - if (auditorStatusValue != null && !auditorStatusValue.equalsIgnoreCase("Pending Review")) { + if (!StringUtil.isPendingReviewValue(auditorStatusValue)) { return true; } @@ -299,12 +348,12 @@ private boolean isAudited(UserPrompt userPrompt) { if (analysisTag != null && tags.containsKey(analysisTag.getId())) { String tagValue = tags.get(analysisTag.getId()); - if (tagValue != null && !tagValue.equalsIgnoreCase("Not Set") && !tagValue.equalsIgnoreCase(Constants.PENDING_REVIEW) && !tagValue.trim().isEmpty()) { + if (!StringUtil.isPendingReviewValue(tagValue)) { return true; } } - if (tags.containsKey(analysisTagS) && !tags.get(analysisTagS).equalsIgnoreCase("Not Set") && !StringUtil.isEmpty(tags.get(analysisTagS))) { + if (tags.containsKey(analysisTagS) && !StringUtil.isPendingReviewValue(tags.get(analysisTagS))) { return true; } } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditFprOptions.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditFprOptions.java index 8fa13198222..f05b9dffa59 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditFprOptions.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/AuditFprOptions.java @@ -36,5 +36,6 @@ public class AuditFprOptions { private final boolean noFilterSet; private final List folderNames; private final List folderPriorityOrder; + @Builder.Default private final boolean forceReaudit = false; @Builder.Default private final ISourceDecoder sourceDecoder = SourceDecoders.defaults(); } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/FPRProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/FPRProcessor.java index b0899d4f593..b8744a29020 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/FPRProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/FPRProcessor.java @@ -146,7 +146,7 @@ private boolean isAudited(AuditIssue auditIssue) { } String auditorStatusValue = tags.get(Constants.AUDITOR_STATUS_TAG_ID); - if (!isPendingReviewValue(auditorStatusValue)) { + if (!StringUtil.isPendingReviewValue(auditorStatusValue)) { return true; } @@ -159,16 +159,7 @@ private boolean isAudited(AuditIssue auditIssue) { } String analysisTagValue = tags.get(Constants.ANALYSIS_TAG_ID); - return analysisTagValue != null - && !analysisTagValue.equalsIgnoreCase("Not Set") - && !analysisTagValue.equalsIgnoreCase(Constants.PENDING_REVIEW) - && !StringUtil.isEmpty(analysisTagValue); - } - - private boolean isPendingReviewValue(String value) { - return StringUtil.isEmpty(value) - || value.equalsIgnoreCase("Pending Review") - || value.equalsIgnoreCase(Constants.PENDING_REVIEW); + return !StringUtil.isPendingReviewValue(analysisTagValue); } private String resolveIssueStatus(AuditIssue auditIssue) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java index 2a059b8380b..5afca605ef9 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import java.util.zip.ZipFile; import javax.xml.parsers.DocumentBuilder; @@ -85,6 +86,7 @@ private enum SkipReason { REMEDIATION_DATA_INVALID("Remediation data invalid"), REMEDIATION_LINE_RANGE_INVALID("Remediation line range invalid"), SOURCE_CONTEXT_NOT_FOUND("Source context not found"), + SOURCE_CONTEXT_AMBIGUOUS("Source context matched multiple locations"), ORIGINAL_CODE_NOT_FOUND("Original code not found"), REMEDIATION_ENCODE_FAILED("Remediation encode failed"), SOURCE_WRITE_FAILED("Source file write failed"), @@ -307,7 +309,8 @@ private String applyChange(String instanceId, String filename, String fileHash, LOG.debug("Remediation {} hash check for '{}': {}", instanceId, filename, fileHashMatches ? "matched" : "mismatched"); if (!fileHashMatches) { LOG.debug("File hash mismatch for remediation {} in {}; searching changed source content", instanceId, filename); - String contextText = getRequiredElementText(change, "Context"); + Element contextElement = getRequiredElement(change, "Context"); + String contextText = contextElement.getTextContent(); List contextLine = Arrays.asList(contextText.split("\\r?\\n")); int contextLineFrom = fuzzySearchContext(instanceId, filename, originalLines, contextLine); if (contextLineFrom == -1) { @@ -320,7 +323,10 @@ private String applyChange(String instanceId, String filename, String fileHash, String originalCodeText = getRequiredElementText(change, "OriginalCode"); List originalCodeLine = Arrays.asList(originalCodeText.split("\\r?\\n")); - int[] lineFromTo = fuzzySearchOriginalCode(instanceId, filename, originalLines, originalCodeLine, contextLineFrom); + int contextBefore = parseRequiredContextAttribute(contextElement, "before"); + int contextAfter = parseRequiredContextAttribute(contextElement, "after"); + int[] lineFromTo = fuzzySearchOriginalCode(instanceId, filename, originalLines, originalCodeLine, + contextLineFrom, contextLine.size(), contextBefore, contextAfter); if (lineFromTo[0] == -1 || lineFromTo[1] == -1) { LOG.debug("Original code search failed for remediation {} in {}; context line={}, original code lines={}, source lines={}", instanceId, filename, contextLineFrom + 1, originalCodeLine.size(), originalLines.size()); @@ -387,7 +393,15 @@ private void rollbackRemediationWrites(String instanceId, List originalLines, List contextLine) { try { - return FuzzyContextSearcher.fuzzySearchContext(originalLines, contextLine, 0); + List matches = FuzzyContextSearcher.fuzzySearchContextMatches(originalLines, contextLine, 0); + if (matches.size() > 1) { + String candidateLines = matches.stream() + .map(line -> String.valueOf(line + 1)) + .collect(Collectors.joining(", ")); + throw new SkipRemediationException(SkipReason.SOURCE_CONTEXT_AMBIGUOUS, + "Source context matched multiple locations in file '" + filename + "'; candidate lines: " + candidateLines); + } + return matches.isEmpty() ? -1 : matches.get(0); } catch (IOException e) { throw new SkipRemediationException(SkipReason.SOURCE_CONTEXT_NOT_FOUND, "Error searching source context for remediation '" + instanceId + "' in file '" + filename + "'", e); @@ -395,8 +409,19 @@ private int fuzzySearchContext(String instanceId, String filename, List } private int[] fuzzySearchOriginalCode(String instanceId, String filename, List originalLines, List originalCodeLine, - int contextLineFrom) { - return FuzzyContextSearcher.fuzzySearchOriginalCode(originalLines, originalCodeLine, 0, contextLineFrom); + int contextLineFrom, int contextLineCount, int contextBefore, int contextAfter) { + int contextStart = contextLineFrom + contextBefore; + int contextEnd = contextLineFrom + contextLineCount - contextAfter; + if (contextStart < 0 || contextStart >= contextEnd || contextEnd > originalLines.size()) { + return new int[] {-1, -1}; + } + + int[] lineFromTo = FuzzyContextSearcher.fuzzySearchOriginalCode( + originalLines.subList(contextStart, contextEnd), originalCodeLine, 0, 0); + if (lineFromTo[0] == -1 || lineFromTo[1] == -1) { + return lineFromTo; + } + return new int[] {lineFromTo[0] + contextStart, lineFromTo[1] + contextStart}; } private boolean isFilePresent(Path path) { @@ -447,12 +472,34 @@ private byte[] encodeSourceFile(String content, Charset charset, String filename } private String getRequiredElementText(Element parent, String elementName) { + return getRequiredElement(parent, elementName).getTextContent(); + } + + private Element getRequiredElement(Element parent, String elementName) { NodeList nodes = parent.getElementsByTagNameNS(NAMESPACE_URI, elementName); if (nodes.getLength() == 0 || nodes.item(0) == null) { throw new SkipRemediationException(SkipReason.REMEDIATION_DATA_INVALID, "Missing required remediation element '" + elementName + "'"); } - return nodes.item(0).getTextContent(); + return (Element) nodes.item(0); + } + + private int parseRequiredContextAttribute(Element context, String attributeName) { + String value = context.getAttribute(attributeName); + if (value == null || value.isBlank()) { + throw new SkipRemediationException(SkipReason.REMEDIATION_DATA_INVALID, + "Missing required remediation context attribute '" + attributeName + "'"); + } + try { + int parsedValue = Integer.parseInt(value); + if (parsedValue < 0) { + throw new NumberFormatException("negative value"); + } + return parsedValue; + } catch (NumberFormatException e) { + throw new SkipRemediationException(SkipReason.REMEDIATION_DATA_INVALID, + "Invalid remediation context attribute '" + attributeName + "': " + value, e); + } } private int parseRequiredInt(Element parent, String elementName) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/FuzzyContextSearcher.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/FuzzyContextSearcher.java index 5b3ff717364..c767b303863 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/FuzzyContextSearcher.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/FuzzyContextSearcher.java @@ -28,58 +28,70 @@ public class FuzzyContextSearcher { */ public static int fuzzySearchContext(List sourceLines, List contextLines, int maxMismatches) throws IOException { + List matches = fuzzySearchContextMatches(sourceLines, contextLines, maxMismatches); + return matches.isEmpty() ? -1 : matches.get(0); + } + + public static List fuzzySearchContextMatches(List sourceLines, List contextLines, + int maxMismatches) throws IOException { List normalizedSource = normalizeLines(sourceLines); List normalizedContext = normalizeLines(contextLines); + List matches = new ArrayList<>(); + boolean contextStartsWithBlank = !normalizedContext.isEmpty() && normalizedContext.get(0).isEmpty(); for (int i = 0; i < normalizedSource.size(); i++) { - int mismatchCount = 0; - int sourceIndex = i; - int contextIndex = 0; - boolean similar = false; + boolean sourceStartsWithBlank = normalizedSource.get(i).isEmpty(); + if (contextStartsWithBlank + ? !sourceStartsWithBlank || (i > 0 && normalizedSource.get(i - 1).isEmpty()) + : sourceStartsWithBlank) { + continue; + } + Integer matchStart = findContextMatchStart(normalizedSource, normalizedContext, maxMismatches, i); + if (matchStart != null && !matches.contains(matchStart)) { + matches.add(matchStart); + } + } - while (contextIndex < normalizedContext.size() && sourceIndex < normalizedSource.size()) { - String contextLine = normalizedContext.get(contextIndex).trim(); + return List.copyOf(matches); + } - if (contextLine.isEmpty()) { - contextIndex++; // Skip empty context lines - continue; - } + private static Integer findContextMatchStart(List normalizedSource, List normalizedContext, + int maxMismatches, int startIndex) { + int mismatchCount = 0; + int sourceIndex = startIndex; + int contextIndex = 0; - // Skip empty source lines too - String sourceLine = normalizedSource.get(sourceIndex).trim(); - while (sourceLine.isEmpty()) { - sourceIndex++; - if (sourceIndex >= normalizedSource.size()) { - break; - } - sourceLine = normalizedSource.get(sourceIndex).trim(); - if(!similar) - i = sourceIndex; - } + while (contextIndex < normalizedContext.size() && sourceIndex < normalizedSource.size()) { + String contextLine = normalizedContext.get(contextIndex).trim(); + if (contextLine.isEmpty()) { + contextIndex++; + continue; + } + String sourceLine = normalizedSource.get(sourceIndex).trim(); + while (sourceLine.isEmpty()) { + sourceIndex++; if (sourceIndex >= normalizedSource.size()) { - break; // No more source lines to match + break; } + sourceLine = normalizedSource.get(sourceIndex).trim(); + } + if (sourceIndex >= normalizedSource.size()) { + break; + } - similar = linesSimilar(sourceLine, contextLine); - - if (!similar) { - mismatchCount++; - if (mismatchCount > maxMismatches) { - break; - } + if (!linesSimilar(sourceLine, contextLine)) { + mismatchCount++; + if (mismatchCount > maxMismatches) { + break; } - - sourceIndex++; - contextIndex++; } - if (contextIndex == normalizedContext.size() && mismatchCount <= maxMismatches) { - return i; // Found approximate match starting at i (ignoring blanks) - } + sourceIndex++; + contextIndex++; } - return -1; // Not found + return contextIndex == normalizedContext.size() && mismatchCount <= maxMismatches ? startIndex : null; } public static int[] fuzzySearchOriginalCode(List sourceLines, List originalCodeLine, int maxMismatches, int startIndex) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/StringUtil.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/StringUtil.java index adf83ec46e9..e92f131f91c 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/StringUtil.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/StringUtil.java @@ -23,6 +23,17 @@ public static boolean isEmpty(String test) { return test == null || test.length() == 0; } + public static boolean isPendingReviewValue(String value) { + if (value == null) { + return true; + } + String normalizedValue = value.trim(); + return normalizedValue.isEmpty() + || "Pending Review".equalsIgnoreCase(normalizedValue) + || "Not Set".equalsIgnoreCase(normalizedValue) + || Constants.PENDING_REVIEW.equalsIgnoreCase(normalizedValue); + } + /** * Strips HTML-like tags from a string to clean it up for display. * diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java index 023d2d5bc5c..9b1d459b26a 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java @@ -25,6 +25,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedDeque; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -34,18 +36,30 @@ import org.junit.jupiter.api.Test; import com.fortify.cli.aviator.audit.model.FilterSelection; +import com.fortify.cli.aviator.audit.model.UserPrompt; import com.fortify.cli.aviator.config.IAviatorLogger; import com.fortify.cli.aviator.fpr.Vulnerability; import com.fortify.cli.aviator.fpr.filter.Filter; import com.fortify.cli.aviator.fpr.filter.FilterSet; import com.fortify.cli.aviator.fpr.filter.FilterTemplate; +import com.fortify.cli.aviator.fpr.model.AuditIssue; import com.fortify.cli.aviator.fpr.model.FPRInfo; import com.fortify.cli.aviator.fpr.model.FVDLMetadata; +import com.fortify.cli.aviator.fpr.utils.SourceDecoders; +import com.fortify.cli.aviator.util.Constants; import com.fortify.cli.aviator.util.FprHandle; class IssueAuditorTest { + private static final String TEST_ISSUE_ID = "PROCESSED_ISSUE"; + private static final IAviatorLogger NO_OP_LOGGER = new IAviatorLogger() { + @Override public void progress(String format, Object... args) {} + @Override public void info(String format, Object... args) {} + @Override public void warn(String format, Object... args) {} + @Override public void error(String format, Object... args) {} + }; + private Path tempFprFile; private FprHandle fprHandle; @@ -113,6 +127,98 @@ void testFprInfoMissingBuildIdDefaultsToEmptyString() throws Exception { assertEquals("", fprInfo.getBuildId()); } + @Test + void skipsPreviouslyProcessedAviatorIssueWithoutForceReaudit() throws Exception { + IssueAuditor auditor = createIssueAuditor(false, false, + Map.of(Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR)); + + assertTrue(prepareIssueIds(auditor).isEmpty()); + } + + @Test + void forceReauditIncludesProcessedAviatorIssueAndIgnoresAviatorOutcomeTag() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, false, Map.of( + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.AVIATOR_EXPECTED_OUTCOME_TAG_ID, Constants.EXPLOITABLE)); + + assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); + } + + @Test + void forceReauditStillSkipsSuppressedIssue() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, true, + Map.of(Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR)); + + assertTrue(prepareIssueIds(auditor).isEmpty()); + } + + @Test + void forceReauditStillSkipsHumanTriagedIssue() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, false, Map.of( + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.FOD_TAG_ID, Constants.EXPLOITABLE)); + + assertTrue(prepareIssueIds(auditor).isEmpty()); + } + + @Test + void forceReauditIncludesIssueWithPendingReviewState() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, false, Map.of( + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.FOD_TAG_ID, "Pending Review", + Constants.AUDITOR_STATUS_TAG_ID, Constants.PENDING_REVIEW)); + + assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); + } + + @Test + void treatsAllPendingAnalysisValuesAsUnaudited() throws Exception { + for (String pendingValue : List.of("Pending Review", "Not Set", "Pending Review/Not Set", " pending review ")) { + IssueAuditor auditor = createIssueAuditor(false, false, Map.of(Constants.ANALYSIS_TAG_ID, pendingValue)); + + assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor), pendingValue); + } + } + + @Test + void forceReauditStillSkipsIssueWithManualAuditorStatus() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, false, Map.of( + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.AUDITOR_STATUS_TAG_ID, Constants.EXPLOITABLE)); + + assertTrue(prepareIssueIds(auditor).isEmpty()); + } + + private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags) { + FPRInfo fprInfo = new FPRInfo(fprHandle); + FilterTemplate filterTemplate = new FilterTemplate(); + filterTemplate.setTagDefinitions(new ArrayList<>()); + fprInfo.setFilterTemplate(filterTemplate); + + Vulnerability vulnerability = new Vulnerability(); + vulnerability.setInstanceID(TEST_ISSUE_ID); + AuditIssue auditIssue = AuditIssue.builder() + .instanceId(TEST_ISSUE_ID) + .suppressed(suppressed) + .tags(new HashMap<>(tags)) + .build(); + + return new IssueAuditor( + List.of(vulnerability), null, Map.of(TEST_ISSUE_ID, auditIssue), fprInfo, + "TestApp", "1.0", new FilterSelection(null, null), NO_OP_LOGGER, null, + new SourceLanguageResolver(new FVDLMetadata()), SourceDecoders.defaults(), null, forceReaudit); + } + + @SuppressWarnings("unchecked") + private List prepareIssueIds(IssueAuditor auditor) throws Exception { + Method prepareMethod = IssueAuditor.class.getDeclaredMethod("prepareAndFilterPrompts"); + prepareMethod.setAccessible(true); + ConcurrentLinkedDeque prompts = (ConcurrentLinkedDeque) prepareMethod.invoke(auditor); + return prompts.stream() + .map(prompt -> prompt.getIssueData().getInstanceID()) + .collect(Collectors.toList()); + } + @Test void testFilterVulnerabilities_LegacySyntaxWithSpaces() throws Exception { diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/FPRProcessorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/FPRProcessorTest.java index 7b7a94f007d..61b5734f54d 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/FPRProcessorTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/FPRProcessorTest.java @@ -128,6 +128,20 @@ void testProcessDoesNotMarkPendingReviewDefaultAuditorStatusAsAudited() throws E assertFalse(vulnerability.isAudited()); } + @Test + void testProcessTreatsAllPendingAnalysisValuesAsUnaudited() throws Exception { + createTestFpr(minimalAuditFvdl()); + + for (String pendingValue : List.of("Pending Review", "Not Set", "Pending Review/Not Set", " pending review ")) { + AuditIssue auditIssue = AuditIssue.builder() + .instanceId("instance-1") + .tags(Map.of(Constants.ANALYSIS_TAG_ID, pendingValue)) + .build(); + + assertFalse(processSingleVulnerability(auditIssue).isAudited(), pendingValue); + } + } + private String minimalAuditFvdl() { return """ diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java new file mode 100644 index 00000000000..a9ddc12a501 --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java @@ -0,0 +1,143 @@ +/* + * 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.fpr.processor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.fortify.cli.aviator.util.FprHandle; + +class RemediationProcessorTest { + private static final String REMEDIATIONS_NAMESPACE = "xmlns://www.fortify.com/schema/remediations"; + + @TempDir + Path tempDir; + + @Test + void skipsAmbiguousContextWithoutChangingSource() throws Exception { + String originalSource = "before\nTARGET\nafter\nbefore\nTARGET\nafter\n"; + Path sourceFile = writeSourceFile(originalSource); + Path fprPath = createRemediationFpr(2, 2, 1, 1, "before\ntarget\nafter", "TARGET", "REPLACED"); + + RemediationProcessor.RemediationMetric metric; + try (FprHandle fprHandle = new FprHandle(fprPath)) { + metric = new RemediationProcessor(fprHandle, tempDir.toString()).processRemediationXML(); + } + + assertEquals(1, metric.totalRemediations()); + assertEquals(0, metric.appliedRemediations()); + assertEquals(1, metric.skippedRemediations()); + assertEquals(Map.of("Source context matched multiple locations", 1), metric.skippedByReason()); + assertEquals(originalSource, Files.readString(sourceFile)); + } + + @Test + void appliesRemediationWhenContextMatchesOnce() throws Exception { + Path sourceFile = writeSourceFile("before\nTARGET\nafter\n"); + Path fprPath = createRemediationFpr(2, 2, 1, 1, "before\ntarget\nafter", "TARGET", "REPLACED"); + + RemediationProcessor.RemediationMetric metric; + try (FprHandle fprHandle = new FprHandle(fprPath)) { + metric = new RemediationProcessor(fprHandle, tempDir.toString()).processRemediationXML(); + } + + assertEquals(1, metric.appliedRemediations()); + assertEquals(0, metric.skippedRemediations()); + assertEquals("before\nREPLACED\nafter\n", Files.readString(sourceFile)); + } + + @Test + void appliesOriginalCodeAfterLeadingContextLines() throws Exception { + Path sourceFile = writeSourceFile("TARGET\nkeep\nTARGET\nafter\n"); + Path fprPath = createRemediationFpr(3, 3, 2, 1, "TARGET\nkeep\nTARGET\nafter", "TARGET", "REPLACED"); + + RemediationProcessor.RemediationMetric metric; + try (FprHandle fprHandle = new FprHandle(fprPath)) { + metric = new RemediationProcessor(fprHandle, tempDir.toString()).processRemediationXML(); + } + + assertEquals(1, metric.appliedRemediations()); + assertEquals("TARGET\nkeep\nREPLACED\nafter\n", Files.readString(sourceFile)); + } + + @Test + void appliesRemediationWhenContextStartsWithBlankLine() throws Exception { + Path sourceFile = writeSourceFile("header\n\nTARGET\nafter\n"); + Path fprPath = createRemediationFpr(3, 3, 1, 1, "\nTARGET\nafter", "TARGET", "REPLACED"); + + RemediationProcessor.RemediationMetric metric; + try (FprHandle fprHandle = new FprHandle(fprPath)) { + metric = new RemediationProcessor(fprHandle, tempDir.toString()).processRemediationXML(); + } + + assertEquals(1, metric.appliedRemediations()); + assertEquals("header\n\nREPLACED\nafter\n", Files.readString(sourceFile)); + } + + private Path writeSourceFile(String content) throws Exception { + Path sourceFile = tempDir.resolve("Example.java"); + Files.writeString(sourceFile, content, StandardCharsets.UTF_8); + return sourceFile; + } + + private Path createRemediationFpr(String context, String originalCode, String newCode) throws Exception { + return createRemediationFpr(2, 2, 1, 1, context, originalCode, newCode); + } + + private Path createRemediationFpr(int lineFrom, int lineTo, int contextBefore, int contextAfter, + String context, String originalCode, String newCode) throws Exception { + Path fprPath = tempDir.resolve("remediation.fpr"); + String remediationXml = """ + + + + test + 2026-08-26T00:00:00Z + + + + test + + Example.java + not-the-source-hash + + %d + %d + %s + %s + %s + + + + + + """.formatted(REMEDIATIONS_NAMESPACE, lineFrom, lineTo, contextBefore, contextAfter, + context, originalCode, newCode); + + try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(fprPath))) { + zipOutputStream.putNextEntry(new ZipEntry("remediations.xml")); + zipOutputStream.write(remediationXml.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + } + return fprPath; + } +} diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/util/FuzzyContextSearcherTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/util/FuzzyContextSearcherTest.java index 22d662833b0..2cdc19eaacf 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/util/FuzzyContextSearcherTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/util/FuzzyContextSearcherTest.java @@ -13,6 +13,7 @@ package com.fortify.cli.aviator.util; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.List; @@ -41,4 +42,44 @@ void shouldMatchOriginalCodeAcrossBlankSourceLines() { assertArrayEquals(new int[] {0, 3}, lineFromTo); } + + @Test + void shouldReturnAllMatchingContextStartLines() throws Exception { + List matches = FuzzyContextSearcher.fuzzySearchContextMatches( + List.of("before", "target", "after", "target", "after"), + List.of("target", "after"), + 0); + + assertEquals(List.of(1, 3), matches); + } + + @Test + void shouldReturnOneMatchingContextStartLine() throws Exception { + List matches = FuzzyContextSearcher.fuzzySearchContextMatches( + List.of("before", "target", "after"), + List.of("target", "after"), + 0); + + assertEquals(List.of(1), matches); + } + + @Test + void shouldReturnNoMatchingContextStartLines() throws Exception { + List matches = FuzzyContextSearcher.fuzzySearchContextMatches( + List.of("before", "after"), + List.of("target", "after"), + 0); + + assertEquals(List.of(), matches); + } + + @Test + void shouldKeepPhysicalStartWhenContextBeginsWithBlankLine() throws Exception { + List matches = FuzzyContextSearcher.fuzzySearchContextMatches( + List.of("header", "", "target", "after"), + List.of("", "target", "after"), + 0); + + assertEquals(List.of(1), matches); + } } \ 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 5a0c876ef9e..1d7529aaa51 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 @@ -80,6 +80,7 @@ public class AviatorSSCAuditCommand extends AbstractSSCJsonNodeOutputCommand imp @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; + @Option(names = {"--force-reaudit"}) private boolean forceReaudit; @Mixin private SourceEncodingsMixin sourceEncodingsMixin; private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCAuditCommand.class); private Long checkedQuotaBefore; @@ -102,7 +103,8 @@ public JsonNode getJsonNode(UnirestInstance unirest) { refreshMetricsIfNeeded(unirest, av, logger); - long auditableIssueCount = AviatorSSCAuditHelper.getAuditableIssueCount(unirest, av, logger, isNoFilterSet(), getFilterSetTitleOrId(), folderNames); + long auditableIssueCount = AviatorSSCAuditHelper.getAuditableIssueCount( + unirest, av, logger, isNoFilterSet(), getFilterSetTitleOrId(), folderNames, forceReaudit); if (auditableIssueCount == 0) { logger.progress("Audit skipped - no auditable issues found matching the specified filters."); ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "SKIPPED"); @@ -110,7 +112,7 @@ public JsonNode getJsonNode(UnirestInstance unirest) { return result; } - JsonNode quotaResult = checkQuota(unirest, av, sessionDescriptor, auditableIssueCount, logger); + JsonNode quotaResult = checkQuota(unirest, av, sessionDescriptor, auditableIssueCount, logger, forceReaudit); if (quotaResult != null) { return quotaResult; } @@ -142,6 +144,10 @@ boolean isNoFilterSet() { return noFilterSet; } + boolean isForceReaudit() { + return forceReaudit; + } + 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()); @@ -167,7 +173,7 @@ private List getFolderPriorityOrder() { */ private JsonNode checkQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorUserSessionDescriptor sessionDescriptor, - long auditableIssueCount, AviatorLoggerImpl logger) { + long auditableIssueCount, AviatorLoggerImpl logger, boolean forceReaudit) { if (!isSkipIfExceedingQuota() && !testExceedingQuota) { return null; } @@ -194,7 +200,7 @@ private JsonNode checkQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, return null; } - return evaluateQuota(unirest, av, effectiveAppName, auditableIssueCount, availableQuota, logger); + return evaluateQuota(unirest, av, effectiveAppName, auditableIssueCount, availableQuota, logger, forceReaudit); } /** @@ -228,7 +234,7 @@ private long handleAppNotFound(AviatorUserSessionDescriptor sessionDescriptor, */ private JsonNode evaluateQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, String effectiveAppName, long auditableIssueCount, long availableQuota, - AviatorLoggerImpl logger) { + AviatorLoggerImpl logger, boolean forceReaudit) { if (availableQuota == AviatorSSCAuditHelper.QUOTA_UNKNOWN) { if (testExceedingQuota) { ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "QUOTA_UNKNOWN"); @@ -238,7 +244,7 @@ private JsonNode evaluateQuota(UnirestInstance unirest, SSCAppVersionDescriptor 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); + var topCategories = AviatorSSCAuditHelper.getTopUnauditedCategories(unirest, av, logger, 10, forceReaudit); String detailedMessage = AviatorSSCAuditHelper.formatQuotaExceededMessage( av, auditableIssueCount, availableQuota, topCategories); LOG.info(detailedMessage); @@ -277,6 +283,7 @@ private JsonNode processFpr(UnirestInstance unirest, SSCAppVersionDescriptor av, .noFilterSet(isNoFilterSet()) .folderNames(folderNames) .folderPriorityOrder(getFolderPriorityOrder()) + .forceReaudit(forceReaudit) .sourceDecoder(sourceEncodingsMixin.getSourceDecoder()) .build()); } catch (Exception e) { 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 e9ce3228879..b5747d188de 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 @@ -216,10 +216,18 @@ public static String getProgressMessage(FPRAuditResult auditResult) { * Queries SSC to get the number of auditable issues for a given application version. */ public static long getAuditableIssueCount(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger, boolean noFilterSet, String filterSetTitleOrId, List folderNames) { + return getAuditableIssueCount(unirest, av, logger, noFilterSet, filterSetTitleOrId, folderNames, false); + } + + /** + * Queries SSC to get the number of issues eligible for the requested audit mode. + */ + public static long getAuditableIssueCount(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger, + boolean noFilterSet, String filterSetTitleOrId, List folderNames, boolean forceReaudit) { logger.progress("Status: Checking for auditable issues..."); - LOG.debug("Starting auditable issue count for SSC version {} (application='{}', version='{}') with pageLimit={}, noFilterSet={}", - av.getVersionId(), av.getApplicationName(), av.getVersionName(), PAGE_LIMIT, noFilterSet); + LOG.debug("Starting auditable issue count for SSC version {} (application='{}', version='{}') with pageLimit={}, noFilterSet={}, forceReaudit={}", + av.getVersionId(), av.getApplicationName(), av.getVersionName(), PAGE_LIMIT, noFilterSet, forceReaudit); String effectiveFilterSetTitleOrId = filterSetTitleOrId != null && !filterSetTitleOrId.isBlank() ? filterSetTitleOrId @@ -290,7 +298,7 @@ public static long getAuditableIssueCount(UnirestInstance unirest, SSCAppVersion ArrayNode issues = (ArrayNode) response.get("data"); if (issues != null && !issues.isEmpty()) { long auditableOnPage = JsonHelper.stream(issues) - .filter(issue -> !isProcessedByAviator(issue)) + .filter(issue -> forceReaudit || !isProcessedByAviator(issue)) .count(); totalAuditableCount += auditableOnPage; LOG.debug("Processed SSC issues page for version {}: pageStart={}, pageSize={}, auditableOnPage={}, cumulativeAuditableCount={}, totalFromServer={}", @@ -434,9 +442,18 @@ public static long getDefaultQuota(String aviatorUrl, String aviatorToken, public static List> getTopUnauditedCategories( UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger, int topN) { + return getTopUnauditedCategories(unirest, av, logger, topN, false); + } + + /** + * Retrieves the top categories using the same Aviator-status scope as the audit preflight. + */ + public static List> getTopUnauditedCategories( + UnirestInstance unirest, SSCAppVersionDescriptor av, + AviatorLoggerImpl logger, int topN, boolean forceReaudit) { try { - return getTopUnauditedCategoriesInternal(unirest, av, logger, topN); + return getTopUnauditedCategoriesInternal(unirest, av, logger, topN, forceReaudit); } catch (Exception e) { LOG.warn("Failed to retrieve top unaudited categories for {}:{}: {}", av.getApplicationName(), av.getVersionName(), e.getMessage()); @@ -447,7 +464,7 @@ public static List> getTopUnauditedCategories( private static List> getTopUnauditedCategoriesInternal( UnirestInstance unirest, SSCAppVersionDescriptor av, - AviatorLoggerImpl logger, int topN) { + AviatorLoggerImpl logger, int topN, boolean forceReaudit) { String versionId = av.getVersionId(); @@ -458,12 +475,14 @@ private static List> getTopUnauditedCategoriesInternal( // Resolve "Aviator status:Not Set" filter dynamically String aviatorStatusFilter = null; - try { - SSCIssueFilterHelper filterHelper = new SSCIssueFilterHelper(unirest, versionId); - aviatorStatusFilter = filterHelper.getFilter("Aviator status:Not Set"); - } catch (FcliSimpleException e) { - // Tag doesn't exist on this version — all issues are unprocessed - LOG.debug("Aviator status tag not found for version {}. All issues considered unprocessed.", versionId); + if (!forceReaudit) { + try { + SSCIssueFilterHelper filterHelper = new SSCIssueFilterHelper(unirest, versionId); + aviatorStatusFilter = filterHelper.getFilter("Aviator status:Not Set"); + } catch (FcliSimpleException e) { + // Tag doesn't exist on this version — all issues are unprocessed + LOG.debug("Aviator status tag not found for version {}. All issues considered unprocessed.", versionId); + } } // Call issueGroups API 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 7a2c64f68e3..907990c0223 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 @@ -128,6 +128,7 @@ fcli.aviator.ssc.audit.folder = Filter issues by a comma-separated list of speci fcli.aviator.ssc.audit.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.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.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.force-reaudit = Re-audit issues previously processed by Aviator, while preserving suppressed and manually triaged issues. fcli.aviator.ssc.audit.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.source-encodings = Comma-separated source encoding candidates to try in order when decoding source files. Use FPR to try the source encoding recorded in audit.fvdl. When writing remediated files, the accepted encoding is used. Default value: ${DEFAULT-VALUE}. fcli.aviator.ssc.audit.refresh = By default, this command will refresh the source application version's metrics when copying from it. \ 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 d8988e8a860..8bbc0f47531 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 @@ -52,6 +52,12 @@ void testAllowsNoFilterSetOption() { assertNull(cmd.getFilterSetTitleOrId()); } + @Test + void testAllowsForceReauditOption() { + var cmd = parse("--force-reaudit"); + assertTrue(cmd.isForceReaudit()); + } + @Test void reportsDecodeSkippedIssuesAsNotSubmitted() { ObjectNode result = JsonHelper.getObjectMapper().createObjectNode(); diff --git a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java new file mode 100644 index 00000000000..66f6db624c1 --- /dev/null +++ b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java @@ -0,0 +1,207 @@ +/* + * 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.assertTrue; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.aviator.config.AviatorLoggerImpl; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.progress.helper.IProgressWriter; +import com.fortify.cli.common.rest.unirest.UnirestHelper; +import com.fortify.cli.common.rest.unirest.config.UnirestJsonHeaderConfigurer; +import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +import kong.unirest.UnirestInstance; + +class AviatorSSCAuditHelperTest { + + @Test + void excludesPreviouslyProcessedIssuesFromNormalPreflightCount() throws Exception { + try (var server = new TestSscServer(processedIssue(), unprocessedIssue()); + var unirest = newUnirest(server)) { + assertEquals(1, AviatorSSCAuditHelper.getAuditableIssueCount( + unirest, appVersion(), logger(), true, null, null)); + assertTrue(server.getLastIssueQuery().contains("q=audited:false")); + } + } + + @Test + void forceReauditPreflightIncludesPreviouslyProcessedIssues() throws Exception { + try (var server = new TestSscServer(processedIssue(), unprocessedIssue()); + var unirest = newUnirest(server)) { + assertEquals(2, AviatorSSCAuditHelper.getAuditableIssueCount( + unirest, appVersion(), logger(), true, null, null, true)); + assertTrue(server.getLastIssueQuery().contains("q=audited:false")); + } + } + + @Test + void forceReauditPreflightDoesNotSkipAnAllProcessedApplicationVersion() throws Exception { + try (var server = new TestSscServer(processedIssue()); + var unirest = newUnirest(server)) { + assertEquals(1, AviatorSSCAuditHelper.getAuditableIssueCount( + unirest, appVersion(), logger(), true, null, null, true)); + assertTrue(server.getLastIssueQuery().contains("q=audited:false")); + } + } + + @Test + void forceReauditCategoryBreakdownDoesNotApplyAviatorStatusFilter() throws Exception { + try (var server = new TestSscServer(processedIssue()); + var unirest = newUnirest(server)) { + List> categories = AviatorSSCAuditHelper.getTopUnauditedCategories( + unirest, appVersion(), logger(), 10, true); + + assertEquals(1, categories.size()); + assertEquals("Category A", categories.get(0).get("categoryName")); + assertEquals(1, server.getSelectorSetRequestCount()); + } + } + + private static SSCAppVersionDescriptor appVersion() { + var descriptor = new SSCAppVersionDescriptor(); + descriptor.setVersionId("42"); + descriptor.setApplicationName("TestApp"); + descriptor.setVersionName("1.0"); + return descriptor; + } + + private static AviatorLoggerImpl logger() { + return new AviatorLoggerImpl(new NoOpProgressWriter()); + } + + private static UnirestInstance newUnirest(TestSscServer server) { + return UnirestHelper.createUnirestInstance(unirest -> { + UnirestJsonHeaderConfigurer.configure(unirest); + unirest.config().defaultBaseUrl(server.getBaseUrl()); + }); + } + + private static ObjectNode processedIssue() { + ObjectNode issue = JsonHelper.getObjectMapper().createObjectNode(); + ArrayNode auditValues = issue.putObject("_embed").putArray("auditValues"); + auditValues.addObject() + .put("customTagGuid", AviatorSSCTagDefs.AVIATOR_STATUS_TAG.getGuid()) + .put("customTagIndex", 0); + return issue; + } + + private static ObjectNode unprocessedIssue() { + ObjectNode issue = JsonHelper.getObjectMapper().createObjectNode(); + issue.putObject("_embed").putArray("auditValues"); + return issue; + } + + private static final class TestSscServer implements AutoCloseable { + private final HttpServer server; + private final ArrayNode issues = JsonHelper.getObjectMapper().createArrayNode(); + private int selectorSetRequestCount; + private String lastIssueQuery = ""; + + private TestSscServer(ObjectNode... issueNodes) throws IOException { + for (ObjectNode issue : issueNodes) { + issues.add(issue); + } + server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/api/v1/projectVersions/42/issues", this::handleIssues); + server.createContext("/api/v1/projectVersions/42/issueSelectorSet", this::handleSelectorSet); + server.createContext("/api/v1/projectVersions/42/issueGroups", this::handleIssueGroups); + server.start(); + } + + private String getBaseUrl() { + return "http://localhost:" + server.getAddress().getPort(); + } + + private void handleIssues(HttpExchange exchange) throws IOException { + lastIssueQuery = URLDecoder.decode(exchange.getRequestURI().getRawQuery(), StandardCharsets.UTF_8); + byte[] response = JsonHelper.getObjectMapper().createObjectNode() + .put("count", issues.size()) + .set("data", issues) + .toString() + .getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length); + try (var outputStream = exchange.getResponseBody()) { + outputStream.write(response); + } + } + + private void handleSelectorSet(HttpExchange exchange) throws IOException { + selectorSetRequestCount++; + ObjectNode data = JsonHelper.getObjectMapper().createObjectNode(); + data.putArray("filterBySet"); + data.putArray("groupBySet").addObject() + .put("guid", "category-guid") + .put("displayName", "Category"); + writeJson(exchange, JsonHelper.getObjectMapper().createObjectNode().set("data", data)); + } + + private void handleIssueGroups(HttpExchange exchange) throws IOException { + ObjectNode group = JsonHelper.getObjectMapper().createObjectNode() + .put("id", "Category A") + .put("visibleCount", 1) + .put("auditedCount", 0); + ArrayNode data = JsonHelper.getObjectMapper().createArrayNode().add(group); + writeJson(exchange, JsonHelper.getObjectMapper().createObjectNode().set("data", data)); + } + + private void writeJson(HttpExchange exchange, ObjectNode responseNode) throws IOException { + byte[] response = responseNode.toString().getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length); + try (var outputStream = exchange.getResponseBody()) { + outputStream.write(response); + } + } + + private String getLastIssueQuery() { + return lastIssueQuery; + } + + private int getSelectorSetRequestCount() { + return selectorSetRequestCount; + } + + @Override + public void close() { + server.stop(0); + } + } + + private static final class NoOpProgressWriter implements IProgressWriter { + @Override public boolean isMultiLineSupported() { return false; } + @Override public void writeProgress(String message, Object... args) {} + @Override public void writeInfo(String message, Object... args) {} + @Override public void writeInfoWithException(String message, Throwable cause, Object... args) {} + @Override public void writeWarning(String message, Object... args) {} + @Override public void writeWarningWithException(String message, Throwable cause, Object... args) {} + @Override public void clearProgress() {} + @Override public void close() {} + @Override public String type() { return "test"; } + } +} From d14dc735b45d64a9256546e00f16268d5f394398 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 27 Aug 2026 01:32:10 +0530 Subject: [PATCH 2/9] chore: Collapse IssueAuditor constructors and reuse command forceReaudit field --- .../cli/aviator/audit/IssueAuditor.java | 18 ------------------ .../cli/aviator/audit/IssueAuditorTest.java | 2 +- .../ssc/cli/cmd/AviatorSSCAuditCommand.java | 8 ++++---- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java index deae84bfbc5..cd0d7c619b8 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java @@ -52,7 +52,6 @@ import com.fortify.cli.aviator.fpr.model.FVDLMetadata; import com.fortify.cli.aviator.fpr.processor.AuditProcessor; import com.fortify.cli.aviator.fpr.utils.ISourceDecoder; -import com.fortify.cli.aviator.fpr.utils.SourceDecoders; import com.fortify.cli.aviator.grpc.AviatorGrpcClient; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; import com.fortify.cli.aviator.util.Constants; @@ -91,23 +90,6 @@ public class IssueAuditor { private final IAviatorLogger logger; private final List customPriorityOrder; - public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, - FPRInfo fprInfo, String SSCApplicationName, String SSCApplicationVersion, - FilterSelection filterSelection, IAviatorLogger logger, List customPriorityOrder, - SourceLanguageResolver sourceLanguageResolver) { - this(vulnerabilities, auditProcessor, auditIssueMap, fprInfo, SSCApplicationName, SSCApplicationVersion, - filterSelection, logger, customPriorityOrder, sourceLanguageResolver, SourceDecoders.defaults(), null, false); - } - - public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, - FPRInfo fprInfo, String SSCApplicationName, String SSCApplicationVersion, - FilterSelection filterSelection, IAviatorLogger logger, List customPriorityOrder, - SourceLanguageResolver sourceLanguageResolver, ISourceDecoder sourceDecoder, - FVDLMetadata fvdlMetadata) { - this(vulnerabilities, auditProcessor, auditIssueMap, fprInfo, SSCApplicationName, SSCApplicationVersion, - filterSelection, logger, customPriorityOrder, sourceLanguageResolver, sourceDecoder, fvdlMetadata, false); - } - public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, FPRInfo fprInfo, String SSCApplicationName, String SSCApplicationVersion, FilterSelection filterSelection, IAviatorLogger logger, List customPriorityOrder, diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java index 9b1d459b26a..eb6bcebbd30 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java @@ -268,7 +268,7 @@ void testFilterVulnerabilities_LegacySyntaxWithSpaces() throws Exception { IssueAuditor auditor = new IssueAuditor( inputList, null, new HashMap<>(), fprInfo, "TestApp", "1.0", selection, dummyLogger, null, - new SourceLanguageResolver(new FVDLMetadata()) + new SourceLanguageResolver(new FVDLMetadata()), SourceDecoders.defaults(), null, false ); Method filterMethod = IssueAuditor.class.getDeclaredMethod("filterVulnerabilities", List.class, FilterSet.class); 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 1d7529aaa51..989ff38b08a 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 @@ -112,7 +112,7 @@ public JsonNode getJsonNode(UnirestInstance unirest) { return result; } - JsonNode quotaResult = checkQuota(unirest, av, sessionDescriptor, auditableIssueCount, logger, forceReaudit); + JsonNode quotaResult = checkQuota(unirest, av, sessionDescriptor, auditableIssueCount, logger); if (quotaResult != null) { return quotaResult; } @@ -173,7 +173,7 @@ private List getFolderPriorityOrder() { */ private JsonNode checkQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorUserSessionDescriptor sessionDescriptor, - long auditableIssueCount, AviatorLoggerImpl logger, boolean forceReaudit) { + long auditableIssueCount, AviatorLoggerImpl logger) { if (!isSkipIfExceedingQuota() && !testExceedingQuota) { return null; } @@ -200,7 +200,7 @@ private JsonNode checkQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, return null; } - return evaluateQuota(unirest, av, effectiveAppName, auditableIssueCount, availableQuota, logger, forceReaudit); + return evaluateQuota(unirest, av, effectiveAppName, auditableIssueCount, availableQuota, logger); } /** @@ -234,7 +234,7 @@ private long handleAppNotFound(AviatorUserSessionDescriptor sessionDescriptor, */ private JsonNode evaluateQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, String effectiveAppName, long auditableIssueCount, long availableQuota, - AviatorLoggerImpl logger, boolean forceReaudit) { + AviatorLoggerImpl logger) { if (availableQuota == AviatorSSCAuditHelper.QUOTA_UNKNOWN) { if (testExceedingQuota) { ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "QUOTA_UNKNOWN"); From bfd1853a35bd967478a07b239ed92b9b047ded12 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 27 Aug 2026 07:51:21 +0530 Subject: [PATCH 3/9] fix: Include audited Aviator issues in --force-reaudit preflight --- .../cli/aviator/audit/IssueAuditorTest.java | 9 + .../ssc/helper/AviatorSSCAuditHelper.java | 179 +++++++++++------- .../ssc/helper/AviatorSSCAuditHelperTest.java | 44 ++++- 3 files changed, 155 insertions(+), 77 deletions(-) diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java index eb6bcebbd30..c6b270674e0 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java @@ -144,6 +144,15 @@ void forceReauditIncludesProcessedAviatorIssueAndIgnoresAviatorOutcomeTag() thro assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); } + @Test + void forceReauditIncludesProcessedAviatorIssueWithAnalysisTag() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, false, Map.of( + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.ANALYSIS_TAG_ID, Constants.EXPLOITABLE)); + + assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); + } + @Test void forceReauditStillSkipsSuppressedIssue() throws Exception { IssueAuditor auditor = createIssueAuditor(true, 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 b5747d188de..aaa4a1f291c 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 @@ -212,78 +212,76 @@ public static String getProgressMessage(FPRAuditResult auditResult) { } } - /** - * Queries SSC to get the number of auditable issues for a given application version. - */ - public static long getAuditableIssueCount(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger, boolean noFilterSet, String filterSetTitleOrId, List folderNames) { - return getAuditableIssueCount(unirest, av, logger, noFilterSet, filterSetTitleOrId, folderNames, false); - } - /** * Queries SSC to get the number of issues eligible for the requested audit mode. + * Force re-audit counts previously processed Aviator issues even when SSC already + * marks them audited; human-audited issues Aviator never processed stay excluded. */ public static long getAuditableIssueCount(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger, boolean noFilterSet, String filterSetTitleOrId, List folderNames, boolean forceReaudit) { logger.progress("Status: Checking for auditable issues..."); - LOG.debug("Starting auditable issue count for SSC version {} (application='{}', version='{}') with pageLimit={}, noFilterSet={}, forceReaudit={}", av.getVersionId(), av.getApplicationName(), av.getVersionName(), PAGE_LIMIT, noFilterSet, forceReaudit); + AuditableIssueCountQuery query = resolveAuditableIssueCountQuery( + unirest, av, logger, noFilterSet, filterSetTitleOrId, folderNames, forceReaudit); + return countEligibleIssues(query, logger); + } + + private record AuditableIssueCountQuery(UnirestInstance unirest, SSCAppVersionDescriptor av, boolean noFilterSet, + String filterSetGuid, String folderFilter, boolean forceReaudit) {} + + private static AuditableIssueCountQuery resolveAuditableIssueCountQuery(UnirestInstance unirest, SSCAppVersionDescriptor av, + AviatorLoggerImpl logger, boolean noFilterSet, String filterSetTitleOrId, List folderNames, + boolean forceReaudit) { String effectiveFilterSetTitleOrId = filterSetTitleOrId != null && !filterSetTitleOrId.isBlank() ? filterSetTitleOrId : null; boolean effectiveNoFilterSet = noFilterSet && effectiveFilterSetTitleOrId == null; + SSCIssueFilterSetDescriptor filterSetDescriptor = resolveFilterSet( + unirest, av, logger, effectiveNoFilterSet, effectiveFilterSetTitleOrId); + String filterSetGuid = filterSetDescriptor != null ? filterSetDescriptor.getGuid() : null; + String folderFilter = resolveFolderFilter(logger, av, effectiveNoFilterSet, filterSetDescriptor, folderNames); + return new AuditableIssueCountQuery(unirest, av, effectiveNoFilterSet, filterSetGuid, folderFilter, forceReaudit); + } - // Apply filter set if specified - SSCIssueFilterSetDescriptor filterSetDescriptor = null; - String filterSetGuid = null; - if (!effectiveNoFilterSet) { - SSCIssueFilterSetHelper filterSetHelper = new SSCIssueFilterSetHelper(unirest, av.getVersionId()); - filterSetDescriptor = filterSetHelper.getDescriptorByTitleOrId(effectiveFilterSetTitleOrId, false); - if (filterSetDescriptor != null) { - logger.progress("Status: Applying filter set '%s' for issue count check", filterSetDescriptor.getTitle()); - filterSetGuid = filterSetDescriptor.getGuid(); - LOG.debug("Applied SSC filter set '{}' with guid {} while counting auditable issues for version {}", - filterSetDescriptor.getTitle(), filterSetGuid, av.getVersionId()); - } else { - LOG.debug("No SSC filter set resolved from options while counting auditable issues for version {}", - av.getVersionId()); - } + private static SSCIssueFilterSetDescriptor resolveFilterSet(UnirestInstance unirest, SSCAppVersionDescriptor av, + AviatorLoggerImpl logger, boolean noFilterSet, String filterSetTitleOrId) { + if (noFilterSet) { + return null; } - - // Apply folder filter if specified - String folderFilter = null; - if (folderNames != null && !folderNames.isEmpty()) { - folderFilter = getFolderFilter(effectiveNoFilterSet, filterSetDescriptor, folderNames); - logger.progress("Status: Applying folder filter for: %s", String.join(", ", folderNames)); - LOG.debug("Applied folder filter '{}' for folders {} while counting auditable issues for version {}", - folderFilter, folderNames, av.getVersionId()); + SSCIssueFilterSetHelper filterSetHelper = new SSCIssueFilterSetHelper(unirest, av.getVersionId()); + SSCIssueFilterSetDescriptor filterSetDescriptor = filterSetHelper.getDescriptorByTitleOrId(filterSetTitleOrId, false); + if (filterSetDescriptor != null) { + logger.progress("Status: Applying filter set '%s' for issue count check", filterSetDescriptor.getTitle()); + LOG.debug("Applied SSC filter set '{}' with guid {} while counting auditable issues for version {}", + filterSetDescriptor.getTitle(), filterSetDescriptor.getGuid(), av.getVersionId()); + } else { + LOG.debug("No SSC filter set resolved from options while counting auditable issues for version {}", + av.getVersionId()); } + return filterSetDescriptor; + } - long totalAuditableCount = 0; - int start = 0; - long totalFromServer = -1; + private static String resolveFolderFilter(AviatorLoggerImpl logger, SSCAppVersionDescriptor av, boolean noFilterSet, + SSCIssueFilterSetDescriptor filterSetDescriptor, List folderNames) { + if (folderNames == null || folderNames.isEmpty()) { + return null; + } + String folderFilter = getFolderFilter(noFilterSet, filterSetDescriptor, folderNames); + logger.progress("Status: Applying folder filter for: %s", String.join(", ", folderNames)); + LOG.debug("Applied folder filter '{}' for folders {} while counting auditable issues for version {}", + folderFilter, folderNames, av.getVersionId()); + return folderFilter; + } + private static long countEligibleIssues(AuditableIssueCountQuery query, AviatorLoggerImpl logger) { try { + long totalAuditableCount = 0; + int start = 0; + long totalFromServer = -1; do { - GetRequest request = unirest.get(SSCUrls.PROJECT_VERSION_ISSUES(av.getVersionId())) - .queryString("limit", PAGE_LIMIT) - .queryString("embed", "auditValues") - .queryString("qm", "issues") - .queryString("q", "audited:false") - .queryString("start", start); - if (effectiveNoFilterSet) { - request.queryString("showhidden", "true"); - } - if (filterSetGuid != null) { - request.queryString("filterset", filterSetGuid); - } - if (folderFilter != null) { - request.queryString("filter", folderFilter); - } - LOG.debug("Requesting SSC issues page for version {} with start={} and limit={}", - av.getVersionId(), start, PAGE_LIMIT); - JsonNode response = request.asObject(JsonNode.class).getBody(); + JsonNode response = fetchAuditableIssuesPage(query, start); if (response == null || !response.has("data")) { LOG.warn("Invalid response received from issue check; proceeding with FPR download."); logger.progress("WARN: Invalid response from issue check. Proceeding with FPR download."); @@ -292,27 +290,23 @@ public static long getAuditableIssueCount(UnirestInstance unirest, SSCAppVersion if (totalFromServer == -1) { totalFromServer = response.get("count").asLong(0); LOG.debug("SSC reported {} total issues matching the initial auditable count query for version {}", - totalFromServer, av.getVersionId()); + totalFromServer, query.av().getVersionId()); } - ArrayNode issues = (ArrayNode) response.get("data"); - if (issues != null && !issues.isEmpty()) { - long auditableOnPage = JsonHelper.stream(issues) - .filter(issue -> forceReaudit || !isProcessedByAviator(issue)) - .count(); - totalAuditableCount += auditableOnPage; - LOG.debug("Processed SSC issues page for version {}: pageStart={}, pageSize={}, auditableOnPage={}, cumulativeAuditableCount={}, totalFromServer={}", - av.getVersionId(), start, issues.size(), auditableOnPage, totalAuditableCount, totalFromServer); - start += issues.size(); - } else { + if (issues == null || issues.isEmpty()) { LOG.debug("SSC returned no more issues for version {} at start={}; stopping pagination with cumulativeAuditableCount={} and totalFromServer={}", - av.getVersionId(), start, totalAuditableCount, totalFromServer); - break; // No more issues + query.av().getVersionId(), start, totalAuditableCount, totalFromServer); + break; } + long auditableOnPage = countEligibleOnPage(issues, query.forceReaudit()); + totalAuditableCount += auditableOnPage; + LOG.debug("Processed SSC issues page for version {}: pageStart={}, pageSize={}, auditableOnPage={}, cumulativeAuditableCount={}, totalFromServer={}", + query.av().getVersionId(), start, issues.size(), auditableOnPage, totalAuditableCount, totalFromServer); + start += issues.size(); } while (start < totalFromServer); LOG.debug("Completed auditable issue count for version {}: totalAuditableCount={}, totalFromServer={}", - av.getVersionId(), totalAuditableCount, totalFromServer); + query.av().getVersionId(), totalAuditableCount, totalFromServer); logger.progress("Status: Found %d auditable issues.", totalAuditableCount); return totalAuditableCount; } catch (UnexpectedHttpResponseException e) { @@ -322,6 +316,49 @@ public static long getAuditableIssueCount(UnirestInstance unirest, SSCAppVersion } } + private static JsonNode fetchAuditableIssuesPage(AuditableIssueCountQuery query, int start) { + GetRequest request = query.unirest().get(SSCUrls.PROJECT_VERSION_ISSUES(query.av().getVersionId())) + .queryString("limit", PAGE_LIMIT) + .queryString("embed", "auditValues") + .queryString("qm", "issues") + .queryString("start", start); + // Normal audit only needs unaudited issues. Force re-audit must also + // retrieve Aviator-processed issues, which SSC marks audited after Analysis is set. + if (!query.forceReaudit()) { + request.queryString("q", "audited:false"); + } + if (query.noFilterSet()) { + request.queryString("showhidden", "true"); + } + if (query.filterSetGuid() != null) { + request.queryString("filterset", query.filterSetGuid()); + } + if (query.folderFilter() != null) { + request.queryString("filter", query.folderFilter()); + } + LOG.debug("Requesting SSC issues page for version {} with start={} and limit={}", + query.av().getVersionId(), start, PAGE_LIMIT); + return request.asObject(JsonNode.class).getBody(); + } + + private static long countEligibleOnPage(ArrayNode issues, boolean forceReaudit) { + return JsonHelper.stream(issues) + .filter(issue -> isEligibleForRequestedAudit(issue, forceReaudit)) + .count(); + } + + /** + * Force re-audit includes previously processed Aviator issues even when SSC + * already marks them audited. Human-audited issues that Aviator never processed + * stay out. Normal audit still excludes processed issues. + */ + private static boolean isEligibleForRequestedAudit(JsonNode issue, boolean forceReaudit) { + if (isProcessedByAviator(issue)) { + return forceReaudit; + } + return !issue.path("audited").asBoolean(false); + } + /** * Checks if an issue has the 'Aviator status' custom tag set. */ @@ -498,20 +535,22 @@ private static List> getTopUnauditedCategoriesInternal( JsonNode response = request.asObject(JsonNode.class).getBody(); ArrayNode groups = (ArrayNode) response.get("data"); - // Calculate truly-unaudited count per category and sort descending + // Rank categories by issues eligible for the requested audit mode. List> categories = new ArrayList<>(); if (groups != null) { for (JsonNode group : groups) { String name = group.path("id").asText("Unknown"); long visibleCount = group.path("visibleCount").asLong(0); long auditedCount = group.path("auditedCount").asLong(0); - long unaudited = visibleCount - auditedCount; - if (unaudited > 0) { + // Force re-audit sends Aviator-processed (already audited) issues, so + // visible count is the quota-relevant size for the category. + long eligibleCount = forceReaudit ? visibleCount : visibleCount - auditedCount; + if (eligibleCount > 0) { Map entry = new LinkedHashMap<>(); entry.put("categoryName", name); entry.put("totalIssues", visibleCount); entry.put("auditedIssues", auditedCount); - entry.put("unauditedCount", unaudited); + entry.put("unauditedCount", eligibleCount); categories.add(entry); } } diff --git a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java index 66f6db624c1..adb8b2edc2c 100644 --- a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java +++ b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java @@ -13,6 +13,7 @@ package com.fortify.cli.aviator.ssc.helper; 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.io.IOException; @@ -24,6 +25,7 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.aviator.config.AviatorLoggerImpl; @@ -44,7 +46,7 @@ void excludesPreviouslyProcessedIssuesFromNormalPreflightCount() throws Exceptio try (var server = new TestSscServer(processedIssue(), unprocessedIssue()); var unirest = newUnirest(server)) { assertEquals(1, AviatorSSCAuditHelper.getAuditableIssueCount( - unirest, appVersion(), logger(), true, null, null)); + unirest, appVersion(), logger(), true, null, null, false)); assertTrue(server.getLastIssueQuery().contains("q=audited:false")); } } @@ -55,7 +57,7 @@ void forceReauditPreflightIncludesPreviouslyProcessedIssues() throws Exception { var unirest = newUnirest(server)) { assertEquals(2, AviatorSSCAuditHelper.getAuditableIssueCount( unirest, appVersion(), logger(), true, null, null, true)); - assertTrue(server.getLastIssueQuery().contains("q=audited:false")); + assertFalse(server.getLastIssueQuery().contains("q=audited:false")); } } @@ -65,12 +67,22 @@ void forceReauditPreflightDoesNotSkipAnAllProcessedApplicationVersion() throws E var unirest = newUnirest(server)) { assertEquals(1, AviatorSSCAuditHelper.getAuditableIssueCount( unirest, appVersion(), logger(), true, null, null, true)); - assertTrue(server.getLastIssueQuery().contains("q=audited:false")); + assertFalse(server.getLastIssueQuery().contains("q=audited:false")); } } @Test - void forceReauditCategoryBreakdownDoesNotApplyAviatorStatusFilter() throws Exception { + void forceReauditPreflightExcludesHumanAuditedIssuesThatAviatorNeverProcessed() throws Exception { + try (var server = new TestSscServer(processedIssue(), unprocessedIssue(), humanAuditedIssue()); + var unirest = newUnirest(server)) { + assertEquals(2, AviatorSSCAuditHelper.getAuditableIssueCount( + unirest, appVersion(), logger(), true, null, null, true)); + assertFalse(server.getLastIssueQuery().contains("q=audited:false")); + } + } + + @Test + void forceReauditCategoryBreakdownIncludesFullyAuditedCategories() throws Exception { try (var server = new TestSscServer(processedIssue()); var unirest = newUnirest(server)) { List> categories = AviatorSSCAuditHelper.getTopUnauditedCategories( @@ -78,6 +90,7 @@ void forceReauditCategoryBreakdownDoesNotApplyAviatorStatusFilter() throws Excep assertEquals(1, categories.size()); assertEquals("Category A", categories.get(0).get("categoryName")); + assertEquals(1L, categories.get(0).get("unauditedCount")); assertEquals(1, server.getSelectorSetRequestCount()); } } @@ -103,6 +116,7 @@ private static UnirestInstance newUnirest(TestSscServer server) { private static ObjectNode processedIssue() { ObjectNode issue = JsonHelper.getObjectMapper().createObjectNode(); + issue.put("audited", true); ArrayNode auditValues = issue.putObject("_embed").putArray("auditValues"); auditValues.addObject() .put("customTagGuid", AviatorSSCTagDefs.AVIATOR_STATUS_TAG.getGuid()) @@ -112,6 +126,14 @@ private static ObjectNode processedIssue() { private static ObjectNode unprocessedIssue() { ObjectNode issue = JsonHelper.getObjectMapper().createObjectNode(); + issue.put("audited", false); + issue.putObject("_embed").putArray("auditValues"); + return issue; + } + + private static ObjectNode humanAuditedIssue() { + ObjectNode issue = JsonHelper.getObjectMapper().createObjectNode(); + issue.put("audited", true); issue.putObject("_embed").putArray("auditValues"); return issue; } @@ -139,9 +161,17 @@ private String getBaseUrl() { private void handleIssues(HttpExchange exchange) throws IOException { lastIssueQuery = URLDecoder.decode(exchange.getRequestURI().getRawQuery(), StandardCharsets.UTF_8); + ArrayNode matchingIssues = JsonHelper.getObjectMapper().createArrayNode(); + boolean unauditedOnly = lastIssueQuery.contains("q=audited:false"); + for (JsonNode issue : issues) { + if (unauditedOnly && issue.path("audited").asBoolean(false)) { + continue; + } + matchingIssues.add(issue); + } byte[] response = JsonHelper.getObjectMapper().createObjectNode() - .put("count", issues.size()) - .set("data", issues) + .put("count", matchingIssues.size()) + .set("data", matchingIssues) .toString() .getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json"); @@ -165,7 +195,7 @@ private void handleIssueGroups(HttpExchange exchange) throws IOException { ObjectNode group = JsonHelper.getObjectMapper().createObjectNode() .put("id", "Category A") .put("visibleCount", 1) - .put("auditedCount", 0); + .put("auditedCount", 1); ArrayNode data = JsonHelper.getObjectMapper().createArrayNode().add(group); writeJson(exchange, JsonHelper.getObjectMapper().createObjectNode().set("data", data)); } From 391eddcb3478c744d255cf680c8fb7e988f33107 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 27 Aug 2026 08:24:48 +0530 Subject: [PATCH 4/9] chore: Use AuditFprOptions in IssueAuditor and name ambiguous skip lines --- .../fortify/cli/aviator/audit/AuditFPR.java | 23 +++++-------------- .../cli/aviator/audit/IssueAuditor.java | 20 ++++++++-------- .../fpr/processor/RemediationProcessor.java | 21 +++++++++++------ .../cli/aviator/audit/IssueAuditorTest.java | 20 ++++++++++++---- .../processor/RemediationProcessorTest.java | 4 +++- .../ssc/helper/AviatorSSCAuditHelper.java | 18 ++++----------- 6 files changed, 52 insertions(+), 54 deletions(-) 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 805e342444f..38dfa154970 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 @@ -31,7 +31,6 @@ import com.fortify.cli.aviator.audit.model.FPRAuditResult; import com.fortify.cli.aviator.audit.model.FilterSelection; import com.fortify.cli.aviator.audit.model.ParsedFprData; -import com.fortify.cli.aviator.config.IAviatorLogger; import com.fortify.cli.aviator.config.TagMappingConfig; import com.fortify.cli.aviator.fpr.FPRProcessor; import com.fortify.cli.aviator.fpr.Vulnerability; @@ -72,10 +71,7 @@ public static FPRAuditResult auditFPR(AuditFprOptions options) // --- STAGE 3: AUDITING --- Map auditResponses = new ConcurrentHashMap<>(); - AuditOutcome auditOutcome = performAviatorAudit( - parsedData, options.getLogger(), options.getToken(), options.getAppVersion(), options.getUrl(), options.getSscAppName(), options.getSscAppVersion(), - auditResponses, filterSelection, options.getFprHandle(), options.getFolderPriorityOrder(), sourceDecoder, options.isForceReaudit() - ); + AuditOutcome auditOutcome = performAviatorAudit(parsedData, auditResponses, filterSelection, options); // --- STAGE 4: FINALIZATION --- return finalizeFprAudit( @@ -127,11 +123,8 @@ private static Map buildIssueCategoryLookup(List return issueCategoryLookup; } - private static AuditOutcome performAviatorAudit( - ParsedFprData parsedData, IAviatorLogger logger, - String token, String appVersion, String url, String sscAppName, String sscAppVersion, - Map auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle, - List folderPriorityOrder, ISourceDecoder sourceDecoder, boolean forceReaudit) { + private static AuditOutcome performAviatorAudit(ParsedFprData parsedData, Map auditResponsesToFill, + FilterSelection filterSelection, AuditFprOptions options) { SourceLanguageResolver sourceLanguageResolver = new SourceLanguageResolver(parsedData.streamingFVDLProcessor.getFvdlMetadata()); parsedData.streamingFVDLProcessor.getFvdlMetadata().clearSourceFileTypeIndexes(); @@ -141,18 +134,14 @@ private static AuditOutcome performAviatorAudit( parsedData.auditProcessor, parsedData.auditIssueMap, parsedData.fprInfo, - sscAppName, - sscAppVersion, filterSelection, - logger, - folderPriorityOrder, sourceLanguageResolver, - sourceDecoder, parsedData.streamingFVDLProcessor.getFvdlMetadata(), - forceReaudit + options ); return issueAuditor.performAudit( - auditResponsesToFill, token, appVersion, parsedData.fprInfo.getBuildId(), url, fprHandle + auditResponsesToFill, options.getToken(), options.getAppVersion(), + parsedData.fprInfo.getBuildId(), options.getUrl(), options.getFprHandle() ); } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java index cd0d7c619b8..4d78f8372e9 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java @@ -36,6 +36,7 @@ import com.fortify.cli.aviator._common.exception.AviatorQuotaFilterException; import com.fortify.cli.aviator._common.exception.AviatorSimpleException; import com.fortify.cli.aviator._common.exception.AviatorTechnicalException; +import com.fortify.cli.aviator.audit.model.AuditFprOptions; import com.fortify.cli.aviator.audit.model.AuditOutcome; import com.fortify.cli.aviator.audit.model.AuditResponse; import com.fortify.cli.aviator.audit.model.FilterSelection; @@ -91,12 +92,11 @@ public class IssueAuditor { private final List customPriorityOrder; public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, - FPRInfo fprInfo, String SSCApplicationName, String SSCApplicationVersion, - FilterSelection filterSelection, IAviatorLogger logger, List customPriorityOrder, - SourceLanguageResolver sourceLanguageResolver, ISourceDecoder sourceDecoder, - FVDLMetadata fvdlMetadata, boolean forceReaudit) { - this.logger = logger; - this.customPriorityOrder = customPriorityOrder; + FPRInfo fprInfo, FilterSelection filterSelection, SourceLanguageResolver sourceLanguageResolver, + FVDLMetadata fvdlMetadata, AuditFprOptions options) { + Objects.requireNonNull(options, "options"); + this.logger = options.getLogger(); + this.customPriorityOrder = options.getFolderPriorityOrder(); this.MAX_PER_CATEGORY = Constants.MAX_PER_CATEGORY; this.MAX_TOTAL = Constants.MAX_TOTAL; this.MAX_PER_CATEGORY_EXCEEDED = Constants.MAX_PER_CATEGORY_EXCEEDED; @@ -107,12 +107,12 @@ public IssueAuditor(List vulnerabilities, AuditProcessor auditPro this.auditIssueMap = auditIssueMap; this.fprInfo = fprInfo; this.filterSelection = filterSelection; - this.SSCApplicationName = SSCApplicationName; - this.SSCApplicationVersion = SSCApplicationVersion; + this.SSCApplicationName = options.getSscAppName(); + this.SSCApplicationVersion = options.getSscAppVersion(); this.sourceLanguageResolver = sourceLanguageResolver; - this.sourceDecoder = Objects.requireNonNull(sourceDecoder, "sourceDecoder"); + this.sourceDecoder = Objects.requireNonNull(options.getSourceDecoder(), "sourceDecoder"); this.fvdlMetadata = fvdlMetadata; - this.forceReaudit = forceReaudit; + this.forceReaudit = options.isForceReaudit(); this.analysisTag = fprInfo.getFilterTemplate().getTagDefinitions().stream().filter(t -> "Analysis".equalsIgnoreCase(t.getName())).findFirst().orElse(null); this.resultsTag = resolveResultTag("", "", analysisTag); } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java index 5afca605ef9..76d30cfc1a9 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java @@ -213,7 +213,7 @@ private boolean processRemediation(Element remediation, Path sourceBasePath, FVD try { Map pendingWrites = prepareFileChanges(remediation, sourceBasePath, fvdlMetadata); if (pendingWrites.isEmpty()) { - recordSkipped(skippedByReason, SkipReason.NO_CHANGES); + recordSkipped(skippedByReason, SkipReason.NO_CHANGES.displayName); return false; } try { @@ -224,15 +224,15 @@ private boolean processRemediation(Element remediation, Path sourceBasePath, FVD throw new SkipRemediationException(SkipReason.SOURCE_WRITE_FAILED, e.getMessage(), e); } } catch (SkipRemediationException e) { - recordSkipped(skippedByReason, e.reason); - LOG.info("Skipping remediation {}: {}", instanceId, e.getMessage()); + recordSkipped(skippedByReason, skipReasonLabel(e)); + LOG.warn("Skipping remediation {}: {}", instanceId, e.getMessage()); LOG.debug("Skip reason for remediation {}: {}", instanceId, e.reason.displayName, e); return false; } catch (RollbackRemediationException e) { throw e; } catch (Exception e) { - recordSkipped(skippedByReason, SkipReason.UNEXPECTED_ERROR); - LOG.info("Skipping remediation {} due to an unexpected processing error", instanceId); + recordSkipped(skippedByReason, SkipReason.UNEXPECTED_ERROR.displayName); + LOG.warn("Skipping remediation {} due to an unexpected processing error", instanceId); LOG.debug("Unexpected error while processing remediation {}", instanceId, e); return false; } @@ -564,8 +564,15 @@ private String calculateHashBase64(String content, String algorithm) { } } - private void recordSkipped(Map skippedByReason, SkipReason reason) { - skippedByReason.merge(reason.displayName, 1, Integer::sum); + private void recordSkipped(Map skippedByReason, String reason) { + skippedByReason.merge(reason, 1, Integer::sum); + } + + private String skipReasonLabel(SkipRemediationException exception) { + if (exception.reason == SkipReason.SOURCE_CONTEXT_AMBIGUOUS) { + return exception.getMessage(); + } + return exception.reason.displayName; } private String formatSkippedReasons(Map skippedByReason) { diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java index c6b270674e0..734f25e1374 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.fortify.cli.aviator.audit.model.AuditFprOptions; import com.fortify.cli.aviator.audit.model.FilterSelection; import com.fortify.cli.aviator.audit.model.UserPrompt; import com.fortify.cli.aviator.config.IAviatorLogger; @@ -45,7 +46,6 @@ import com.fortify.cli.aviator.fpr.model.AuditIssue; import com.fortify.cli.aviator.fpr.model.FPRInfo; import com.fortify.cli.aviator.fpr.model.FVDLMetadata; -import com.fortify.cli.aviator.fpr.utils.SourceDecoders; import com.fortify.cli.aviator.util.Constants; import com.fortify.cli.aviator.util.FprHandle; @@ -214,8 +214,18 @@ private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed return new IssueAuditor( List.of(vulnerability), null, Map.of(TEST_ISSUE_ID, auditIssue), fprInfo, - "TestApp", "1.0", new FilterSelection(null, null), NO_OP_LOGGER, null, - new SourceLanguageResolver(new FVDLMetadata()), SourceDecoders.defaults(), null, forceReaudit); + new FilterSelection(null, null), new SourceLanguageResolver(new FVDLMetadata()), null, + auditOptions(forceReaudit, NO_OP_LOGGER)); + } + + private AuditFprOptions auditOptions(boolean forceReaudit, IAviatorLogger logger) { + return AuditFprOptions.builder() + .fprHandle(fprHandle) + .logger(logger) + .sscAppName("TestApp") + .sscAppVersion("1.0") + .forceReaudit(forceReaudit) + .build(); } @SuppressWarnings("unchecked") @@ -276,8 +286,8 @@ void testFilterVulnerabilities_LegacySyntaxWithSpaces() throws Exception { IssueAuditor auditor = new IssueAuditor( inputList, null, new HashMap<>(), fprInfo, - "TestApp", "1.0", selection, dummyLogger, null, - new SourceLanguageResolver(new FVDLMetadata()), SourceDecoders.defaults(), null, false + selection, new SourceLanguageResolver(new FVDLMetadata()), null, + auditOptions(false, dummyLogger) ); Method filterMethod = IssueAuditor.class.getDeclaredMethod("filterVulnerabilities", List.class, FilterSet.class); diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java index a9ddc12a501..0965c2b4b81 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java @@ -46,7 +46,9 @@ void skipsAmbiguousContextWithoutChangingSource() throws Exception { assertEquals(1, metric.totalRemediations()); assertEquals(0, metric.appliedRemediations()); assertEquals(1, metric.skippedRemediations()); - assertEquals(Map.of("Source context matched multiple locations", 1), metric.skippedByReason()); + assertEquals(Map.of( + "Source context matched multiple locations in file 'Example.java'; candidate lines: 1, 4", 1), + metric.skippedByReason()); assertEquals(originalSource, Files.readString(sourceFile)); } 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 aaa4a1f291c..13aaa8e7d73 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 @@ -470,20 +470,10 @@ public static long getDefaultQuota(String aviatorUrl, String aviatorToken, } /** - * Returns the top N SAST categories ordered by truly-unaudited issue count (descending). - * Uses the SSC issueGroups API with: - * - groupingtype = dynamically resolved "Category" GUID - * - filter = dynamically resolved "Aviator status:Not Set" technical filter - * Each entry contains "categoryName" and "unauditedCount". - */ - public static List> getTopUnauditedCategories( - UnirestInstance unirest, SSCAppVersionDescriptor av, - AviatorLoggerImpl logger, int topN) { - return getTopUnauditedCategories(unirest, av, logger, topN, false); - } - - /** - * Retrieves the top categories using the same Aviator-status scope as the audit preflight. + * Returns the top N SAST categories for the requested audit mode. + * Normal audit ranks by unaudited issues with Aviator status not set. + * Force re-audit ranks by visible issues, including Aviator-processed ones. + * Each entry contains "categoryName" and "unauditedCount" (eligible count for that mode). */ public static List> getTopUnauditedCategories( UnirestInstance unirest, SSCAppVersionDescriptor av, From 55deed88206ef013243b1edd7c03d0d43b90f90e Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 27 Aug 2026 09:44:58 +0530 Subject: [PATCH 5/9] chore: Use last TagHistory writer for --force-reaudit eligibility --- .../fortify/cli/aviator/audit/AuditFPR.java | 5 +- .../cli/aviator/audit/IssueAuditor.java | 161 ++++++++++++------ .../cli/aviator/fpr/model/AuditIssue.java | 1 + .../aviator/fpr/processor/AuditProcessor.java | 19 +++ .../fortify/cli/aviator/util/Constants.java | 16 +- .../cli/aviator/audit/IssueAuditorTest.java | 85 ++++++++- 6 files changed, 229 insertions(+), 58 deletions(-) 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 38dfa154970..f00064598a9 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 @@ -139,10 +139,7 @@ private static AuditOutcome performAviatorAudit(ParsedFprData parsedData, Map customPriorityOrder; + private final AuditFprOptions options; + private final Set resultTagIds; public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, FPRInfo fprInfo, FilterSelection filterSelection, SourceLanguageResolver sourceLanguageResolver, FVDLMetadata fvdlMetadata, AuditFprOptions options) { Objects.requireNonNull(options, "options"); - this.logger = options.getLogger(); - this.customPriorityOrder = options.getFolderPriorityOrder(); + Objects.requireNonNull(options.getSourceDecoder(), "sourceDecoder"); + this.options = options; this.MAX_PER_CATEGORY = Constants.MAX_PER_CATEGORY; this.MAX_TOTAL = Constants.MAX_TOTAL; this.MAX_PER_CATEGORY_EXCEEDED = Constants.MAX_PER_CATEGORY_EXCEEDED; @@ -110,11 +108,10 @@ public IssueAuditor(List vulnerabilities, AuditProcessor auditPro this.SSCApplicationName = options.getSscAppName(); this.SSCApplicationVersion = options.getSscAppVersion(); this.sourceLanguageResolver = sourceLanguageResolver; - this.sourceDecoder = Objects.requireNonNull(options.getSourceDecoder(), "sourceDecoder"); this.fvdlMetadata = fvdlMetadata; - this.forceReaudit = options.isForceReaudit(); this.analysisTag = fprInfo.getFilterTemplate().getTagDefinitions().stream().filter(t -> "Analysis".equalsIgnoreCase(t.getName())).findFirst().orElse(null); this.resultsTag = resolveResultTag("", "", analysisTag); + this.resultTagIds = resolveResultTagIds(); } private TagDefinition resolveResultTag(String tagName, String tagGuid, TagDefinition analysisTag) { @@ -158,30 +155,30 @@ private TagDefinition resolveHumanAuditStatus() { return new TagDefinition(name, id, values, false); } - public AuditOutcome performAudit(Map auditResponses, String token, - String projectName, String projectBuildId, String url, FprHandle fprHandle) { - projectName = StringUtil.isEmpty(projectName) ? projectBuildId : projectName; - logger.progress("Starting audit for project: %s", projectName); + public AuditOutcome performAudit(Map auditResponses) { + String projectName = StringUtil.isEmpty(options.getAppVersion()) ? fprInfo.getBuildId() : options.getAppVersion(); + options.getLogger().progress("Starting audit for project: %s", projectName); ConcurrentLinkedDeque promptsToAudit = prepareAndFilterPrompts(); int totalIssuesToAudit = promptsToAudit.size(); - logger.progress("Final count of issues to be audited: %d", totalIssuesToAudit); + options.getLogger().progress("Final count of issues to be audited: %d", totalIssuesToAudit); if (promptsToAudit.isEmpty()) { - logger.progress("Audit skipped - no issues to process after filtering."); + options.getLogger().progress("Audit skipped - no issues to process after filtering."); } else { - try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(url, logger, DEFAULT_PING_INTERVAL_SECONDS)) { + try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(options.getUrl(), options.getLogger(), DEFAULT_PING_INTERVAL_SECONDS)) { CompletableFuture> future = client.processBatchRequests(promptsToAudit, projectName, fprInfo.getBuildId(), SSCApplicationName, - SSCApplicationVersion, token, fprHandle, customPriorityOrder, sourceDecoder, fvdlMetadata); + SSCApplicationVersion, options.getToken(), options.getFprHandle(), + options.getFolderPriorityOrder(), options.getSourceDecoder(), fvdlMetadata); Map responses = future.get(500, TimeUnit.MINUTES); responses.forEach((requestId, response) -> auditResponses.put(response.getIssueId(), response)); - logger.progress("Audit completed"); + options.getLogger().progress("Audit completed"); } catch (ExecutionException e) { Throwable cause = e.getCause(); // Handle quota filtering exception (all issues filtered out) if (cause instanceof AviatorQuotaFilterException) { - logger.progress("All issues filtered out due to quota constraints: %s", cause.getMessage()); + options.getLogger().progress("All issues filtered out due to quota constraints: %s", cause.getMessage()); // Update totalIssuesToAudit to reflect actual auditable count (0) totalIssuesToAudit = 0; } else if (cause instanceof AviatorSimpleException) { @@ -192,10 +189,10 @@ public AuditOutcome performAudit(Map auditResponses, Stri throw new AviatorTechnicalException("Unexpected error during audit execution", cause); } } catch (TimeoutException e) { - logger.error("Audit failed due to timeout after 500 minutes"); + options.getLogger().error("Audit failed due to timeout after 500 minutes"); throw new AviatorTechnicalException("Audit timed out after 500 minutes", e); } catch (InterruptedException e) { - logger.error("Audit failed due to interruption"); + options.getLogger().error("Audit failed due to interruption"); Thread.currentThread().interrupt(); throw new AviatorTechnicalException("Audit interrupted", e); } @@ -236,15 +233,18 @@ private ConcurrentLinkedDeque prepareAndFilterPrompts() { private boolean shouldInclude(UserPrompt userPrompt) { - if (forceReaudit && isProcessedByAviator(userPrompt)) { - if (isProtectedFromForceReaudit(userPrompt)) { - LOG.debug("Skipping force re-audit for suppressed or manually audited issue ID: {}", + AuditIssue auditIssue = auditIssue(userPrompt); + if (options.isForceReaudit()) { + if (hasHumanTriage(auditIssue)) { + LOG.debug("Skipping force re-audit for suppressed or human-triaged issue ID: {}", userPrompt.getIssueData().getInstanceID()); return false; } - LOG.debug("Including previously processed Aviator issue for force re-audit: {}", - userPrompt.getIssueData().getInstanceID()); - return true; + if (isAviatorWork(auditIssue)) { + LOG.debug("Including previously processed Aviator issue for force re-audit: {}", + userPrompt.getIssueData().getInstanceID()); + return true; + } } if (isAudited(userPrompt)) { @@ -252,12 +252,12 @@ private boolean shouldInclude(UserPrompt userPrompt) { return false; } - if (humanAuditTag != null && isManuallyAudited(userPrompt)) { + if (humanAuditTag != null && isManuallyAudited(auditIssue)) { LOG.debug("Skipping because already manually audited: {}", userPrompt.getIssueData().getInstanceID()); return false; } - if (aviatorStatusTag != null && isProcessedByAviator(userPrompt)) { + if (aviatorStatusTag != null && isProcessedByAviator(auditIssue)) { LOG.debug("Skipping already processed by Aviator: {}", userPrompt.getIssueData().getInstanceID()); return false; } @@ -265,40 +265,99 @@ private boolean shouldInclude(UserPrompt userPrompt) { return true; } - private boolean isManuallyAudited(UserPrompt userPrompt) { - String issueId = userPrompt.getIssueData().getInstanceID(); - String status = Optional.ofNullable(auditIssueMap.get(issueId)).map(AuditIssue::getTags) - .map(tags -> tags.get(Constants.FOD_TAG_ID)).orElse(null); - return !StringUtil.isPendingReviewValue(status); + private AuditIssue auditIssue(UserPrompt userPrompt) { + return auditIssueMap.get(userPrompt.getIssueData().getInstanceID()); } - private boolean isProcessedByAviator(UserPrompt userPrompt) { - String issueId = userPrompt.getIssueData().getInstanceID(); - String status = Optional.ofNullable(auditIssueMap.get(issueId)).map(AuditIssue::getTags) - .map(tags -> tags.get(Constants.AVIATOR_STATUS_TAG_ID)).orElse(null); + private static boolean isManuallyAudited(AuditIssue auditIssue) { + if (auditIssue == null || auditIssue.getTags() == null) { + return false; + } + return !StringUtil.isPendingReviewValue(auditIssue.getTags().get(Constants.FOD_TAG_ID)); + } + + private boolean isProcessedByAviator(AuditIssue auditIssue) { + if (auditIssue == null || auditIssue.getTags() == null) { + return false; + } + String status = auditIssue.getTags().get(Constants.AVIATOR_STATUS_TAG_ID); return !StringUtil.isEmpty(status) && Constants.PROCESSED_BY_AVIATOR.equalsIgnoreCase(status); } - private boolean isProtectedFromForceReaudit(UserPrompt userPrompt) { - AuditIssue auditIssue = auditIssueMap.get(userPrompt.getIssueData().getInstanceID()); + private boolean hasHumanTriage(AuditIssue auditIssue) { if (auditIssue == null) { return false; } if (auditIssue.isSuppressed()) { return true; } + boolean processedByAviator = isProcessedByAviator(auditIssue); + for (String tagId : resultTagIds) { + if (!isResultTagSet(auditIssue, tagId)) { + continue; + } + String username = lastWriterUsername(auditIssue, tagId); + if (Constants.isAviatorAuditUsername(username)) { + continue; + } + if (!StringUtil.isEmpty(username) || Constants.FOD_TAG_ID.equalsIgnoreCase(tagId) || !processedByAviator) { + return true; + } + } + return false; + } + + private boolean isAviatorWork(AuditIssue auditIssue) { + if (isProcessedByAviator(auditIssue)) { + return true; + } + for (String tagId : resultTagIds) { + if (isResultTagSet(auditIssue, tagId) && Constants.isAviatorAuditUsername(lastWriterUsername(auditIssue, tagId))) { + return true; + } + } + return false; + } + private boolean isResultTagSet(AuditIssue auditIssue, String tagId) { Map tags = auditIssue.getTags(); - if (tags == null) { - return false; + return tags != null && !StringUtil.isPendingReviewValue(tags.get(tagId)); + } + + private String lastWriterUsername(AuditIssue auditIssue, String tagId) { + Map lastTagUsernames = auditIssue.getLastTagUsernames(); + if (lastTagUsernames == null) { + return null; } + String username = lastTagUsernames.get(tagId); + return StringUtil.isEmpty(username) ? null : username; + } - String auditorStatus = tags.get(Constants.AUDITOR_STATUS_TAG_ID); - if (!StringUtil.isPendingReviewValue(auditorStatus)) { - return true; + private Set resolveResultTagIds() { + Set tagIds = new LinkedHashSet<>(List.of( + Constants.ANALYSIS_TAG_ID, Constants.AUDITOR_STATUS_TAG_ID, Constants.FOD_TAG_ID)); + if (analysisTag != null && !StringUtil.isEmpty(analysisTag.getId())) { + tagIds.add(analysisTag.getId()); } + String mappedTagId = mappedTagId(); + if (!StringUtil.isEmpty(mappedTagId)) { + tagIds.add(mappedTagId); + } + return Set.copyOf(tagIds); + } - return isManuallyAudited(userPrompt); + private String mappedTagId() { + String tagMappingPath = options.getTagMappingPath(); + if (tagMappingPath == null || tagMappingPath.isBlank()) { + return null; + } + try { + TagMappingConfig config = ResourceUtil.loadYamlFile(new File(tagMappingPath), TagMappingConfig.class); + return config == null ? null : config.getTag_id(); + } catch (Exception e) { + LOG.debug("Could not read tag mapping file {}", tagMappingPath, e); + return null; + } } private boolean isAudited(UserPrompt userPrompt) { @@ -383,7 +442,7 @@ private List filterVulnerabilities(List allVulnera .flatMap(List::stream) .distinct() .collect(Collectors.toList()); - logger.info("FilterSet '{}' applied. {} of {} total vulnerabilities remain.", fs.getTitle(), result.size(), allVulnerabilities.size()); + options.getLogger().info("FilterSet '{}' applied. {} of {} total vulnerabilities remain.", fs.getTitle(), result.size(), allVulnerabilities.size()); return result; } else { Set targetFolderIds = fs.getFolderDefinitions().stream() @@ -402,7 +461,7 @@ private List filterVulnerabilities(List allVulnera .distinct() .collect(Collectors.toList()); - logger.info("Filtered by folder(s) '{}'. {} vulnerabilities remain.", targetFolderNames, result.size()); + options.getLogger().info("Filtered by folder(s) '{}'. {} vulnerabilities remain.", targetFolderNames, result.size()); return result; } } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/AuditIssue.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/AuditIssue.java index 75c616be25c..f62638a885a 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/AuditIssue.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/model/AuditIssue.java @@ -30,6 +30,7 @@ public class AuditIssue { private boolean suppressed; private int revision; @Builder.Default private Map tags = new HashMap<>(); + @Builder.Default private Map lastTagUsernames = new HashMap<>(); @Builder.Default private List threadedComments = new ArrayList<>(); public void addTag(String tagId, String tagValue) { 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 d919ff321b6..11a79e38abf 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 @@ -249,6 +249,7 @@ private AuditIssue processAuditIssue(Element issueElement) { tags.put(tagId, tagValue); } auditIssueBuilder.tags(tags); + auditIssueBuilder.lastTagUsernames(lastTagUsernames(issueElement)); List threadedComments = new ArrayList<>(); NodeList commentNodes = issueElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Comment"); @@ -267,6 +268,24 @@ private AuditIssue processAuditIssue(Element issueElement) { } + private Map lastTagUsernames(Element issueElement) { + Map lastTagUsernames = new HashMap<>(); + NodeList tagHistories = issueElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "TagHistory"); + for (int index = 0; index < tagHistories.getLength(); index++) { + Element tagHistory = (Element) tagHistories.item(index); + NodeList tags = tagHistory.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Tag"); + if (tags.getLength() == 0) { + continue; + } + String tagId = ((Element) tags.item(0)).getAttribute("id"); + String username = Optional.ofNullable(getFirstElementContentNS(tagHistory, "Username")).orElse(""); + if (tagId != null && !tagId.isBlank()) { + lastTagUsernames.put(tagId, username); + } + } + return lastTagUsernames; + } + private String getTagValue(Element tagElement) { NodeList valueNodes = tagElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Value"); if (valueNodes.getLength() > 0) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/Constants.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/Constants.java index 756789a0883..00613cd242b 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/Constants.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/Constants.java @@ -12,6 +12,9 @@ */ package com.fortify.cli.aviator.util; +import java.util.Locale; +import java.util.Set; + public class Constants { // Audit Result Values @@ -45,8 +48,19 @@ public class Constants { public static final String AUDITOR_STATUS_TAG_ID = "ACB05E55-E74D-468C-8501-52E1FDC27D71"; public static final String FOD_TAG_ID = "604f0fbe-b5fe-47cd-a9cb-587ad8ebe93a"; - // User Names + // User Names written into audit.xml TagHistory / comments public static final String USER_NAME = "Fortify Remediation Aviator"; + public static final String USER_NAME_LEGACY_FORTIFY_AVIATOR = "Fortify Aviator"; + public static final String USER_NAME_LEGACY_CORE_SAST_AVIATOR = "Core SAST Aviator"; + private static final Set AVIATOR_AUDIT_USERNAMES = Set.of( + USER_NAME.toLowerCase(Locale.ROOT), + USER_NAME_LEGACY_FORTIFY_AVIATOR.toLowerCase(Locale.ROOT), + USER_NAME_LEGACY_CORE_SAST_AVIATOR.toLowerCase(Locale.ROOT) + ); + + public static boolean isAviatorAuditUsername(String username) { + return username != null && AVIATOR_AUDIT_USERNAMES.contains(username.trim().toLowerCase(Locale.ROOT)); + } // Other Constants public static final String AUDIT_NAMESPACE_URI = "xmlns://www.fortify.com/schema/audit"; diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java index 734f25e1374..5fd22435933 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java @@ -193,12 +193,87 @@ void treatsAllPendingAnalysisValuesAsUnaudited() throws Exception { void forceReauditStillSkipsIssueWithManualAuditorStatus() throws Exception { IssueAuditor auditor = createIssueAuditor(true, false, Map.of( Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, - Constants.AUDITOR_STATUS_TAG_ID, Constants.EXPLOITABLE)); + Constants.AUDITOR_STATUS_TAG_ID, Constants.EXPLOITABLE), + Map.of(Constants.AUDITOR_STATUS_TAG_ID, "analyst.user")); assertTrue(prepareIssueIds(auditor).isEmpty()); } + @Test + void forceReauditIncludesAviatorWrittenAuditorStatus() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, false, Map.of( + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.AUDITOR_STATUS_TAG_ID, Constants.EXPLOITABLE), + Map.of(Constants.AUDITOR_STATUS_TAG_ID, Constants.USER_NAME)); + + assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); + } + + @Test + void forceReauditSkipsHumanAnalysisOverrideAfterAviator() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, false, Map.of( + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.ANALYSIS_TAG_ID, Constants.EXPLOITABLE), + Map.of(Constants.ANALYSIS_TAG_ID, "analyst.user")); + + assertTrue(prepareIssueIds(auditor).isEmpty()); + } + + @Test + void forceReauditIncludesLegacyFortifyAviatorUsername() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, false, Map.of( + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.ANALYSIS_TAG_ID, Constants.EXPLOITABLE), + Map.of(Constants.ANALYSIS_TAG_ID, Constants.USER_NAME_LEGACY_FORTIFY_AVIATOR)); + + assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); + } + + @Test + void forceReauditIncludesMappedTagWrittenByAviatorWithoutStatusTag() throws Exception { + Path mappingFile = writeMappedTagFile(); + try { + IssueAuditor auditor = createIssueAuditor(true, false, + Map.of("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Constants.EXPLOITABLE), + Map.of("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Constants.USER_NAME), + mappingFile.toString()); + assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); + } finally { + Files.deleteIfExists(mappingFile); + } + } + + @Test + void forceReauditSkipsHumanWriterOnMappedTag() throws Exception { + Path mappingFile = writeMappedTagFile(); + try { + IssueAuditor auditor = createIssueAuditor(true, false, + Map.of("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Constants.EXPLOITABLE), + Map.of("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "analyst.user"), + mappingFile.toString()); + assertTrue(prepareIssueIds(auditor).isEmpty()); + } finally { + Files.deleteIfExists(mappingFile); + } + } + + private Path writeMappedTagFile() throws IOException { + Path mappingFile = Files.createTempFile("tag-mapping", ".yaml"); + Files.writeString(mappingFile, "tag_id: \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\"\n"); + return mappingFile; + } + private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags) { + return createIssueAuditor(forceReaudit, suppressed, tags, Map.of(), null); + } + + private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags, + Map lastTagUsernames) { + return createIssueAuditor(forceReaudit, suppressed, tags, lastTagUsernames, null); + } + + private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags, + Map lastTagUsernames, String tagMappingPath) { FPRInfo fprInfo = new FPRInfo(fprHandle); FilterTemplate filterTemplate = new FilterTemplate(); filterTemplate.setTagDefinitions(new ArrayList<>()); @@ -210,20 +285,26 @@ private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed .instanceId(TEST_ISSUE_ID) .suppressed(suppressed) .tags(new HashMap<>(tags)) + .lastTagUsernames(new HashMap<>(lastTagUsernames)) .build(); return new IssueAuditor( List.of(vulnerability), null, Map.of(TEST_ISSUE_ID, auditIssue), fprInfo, new FilterSelection(null, null), new SourceLanguageResolver(new FVDLMetadata()), null, - auditOptions(forceReaudit, NO_OP_LOGGER)); + auditOptions(forceReaudit, NO_OP_LOGGER, tagMappingPath)); } private AuditFprOptions auditOptions(boolean forceReaudit, IAviatorLogger logger) { + return auditOptions(forceReaudit, logger, null); + } + + private AuditFprOptions auditOptions(boolean forceReaudit, IAviatorLogger logger, String tagMappingPath) { return AuditFprOptions.builder() .fprHandle(fprHandle) .logger(logger) .sscAppName("TestApp") .sscAppVersion("1.0") + .tagMappingPath(tagMappingPath) .forceReaudit(forceReaudit) .build(); } From 42bde4dcdf8e83468b4e67d98751b8f6acb8c457 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 27 Aug 2026 09:49:10 +0530 Subject: [PATCH 6/9] fix: Skip suppressed force-reaudit preflight issues and test TagHistory writers --- .../AuditProcessorLastTagUsernamesTest.java | 129 ++++++++++++++++++ .../ssc/helper/AviatorSSCAuditHelper.java | 13 +- .../ssc/helper/AviatorSSCAuditHelperTest.java | 15 ++ 3 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/AuditProcessorLastTagUsernamesTest.java diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/AuditProcessorLastTagUsernamesTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/AuditProcessorLastTagUsernamesTest.java new file mode 100644 index 00000000000..53a830524cd --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/AuditProcessorLastTagUsernamesTest.java @@ -0,0 +1,129 @@ +/* + * 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.fpr.processor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.fortify.cli.aviator.fpr.model.AuditIssue; +import com.fortify.cli.aviator.util.Constants; +import com.fortify.cli.aviator.util.FprHandle; + +class AuditProcessorLastTagUsernamesTest { + private static final String INSTANCE_ID = "ISSUE-1"; + + @TempDir + Path tempDir; + + private FprHandle fprHandle; + + @AfterEach + void tearDown() throws Exception { + if (fprHandle != null) { + fprHandle.close(); + } + } + + @Test + void lastDocumentOrderTagHistoryWinsPerTag() throws Exception { + fprHandle = new FprHandle(createTestFpr(""" + + + + + + Exploitable + + + Exploitable + + + + Not an Issue + + %s + + + + Exploitable + + analyst.user + + + + Exploitable + + %s + + + + + """.formatted( + Constants.ANALYSIS_TAG_ID, Constants.AUDITOR_STATUS_TAG_ID, + Constants.ANALYSIS_TAG_ID, Constants.USER_NAME, + Constants.ANALYSIS_TAG_ID, + Constants.AUDITOR_STATUS_TAG_ID, Constants.USER_NAME_LEGACY_FORTIFY_AVIATOR))); + + Map issues = new AuditProcessor(fprHandle).processAuditXML(); + Map lastTagUsernames = issues.get(INSTANCE_ID).getLastTagUsernames(); + + assertEquals("analyst.user", lastTagUsernames.get(Constants.ANALYSIS_TAG_ID)); + assertEquals(Constants.USER_NAME_LEGACY_FORTIFY_AVIATOR, lastTagUsernames.get(Constants.AUDITOR_STATUS_TAG_ID)); + } + + @Test + void missingUsernameIsStoredAsEmptyString() throws Exception { + fprHandle = new FprHandle(createTestFpr(""" + + + + + + + Exploitable + + + + + + """.formatted(Constants.ANALYSIS_TAG_ID))); + + Map issues = new AuditProcessor(fprHandle).processAuditXML(); + + assertEquals("", issues.get(INSTANCE_ID).getLastTagUsernames().get(Constants.ANALYSIS_TAG_ID)); + } + + private Path createTestFpr(String auditXml) throws Exception { + Path fprPath = Files.createTempFile(tempDir, "audit-processor", ".fpr"); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(fprPath))) { + zipOutputStream.putNextEntry(new ZipEntry("audit.xml")); + zipOutputStream.write(auditXml.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + + zipOutputStream.putNextEntry(new ZipEntry("src-archive/index.xml")); + zipOutputStream.write("".getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + } + return fprPath; + } +} 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 13aaa8e7d73..bf0be506133 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 @@ -213,9 +213,9 @@ public static String getProgressMessage(FPRAuditResult auditResult) { } /** - * Queries SSC to get the number of issues eligible for the requested audit mode. - * Force re-audit counts previously processed Aviator issues even when SSC already - * marks them audited; human-audited issues Aviator never processed stay excluded. + * Coarse SSC download gate. Force re-audit counts Aviator-processed issues even when + * SSC marks them audited; suppressed issues and human-audited issues Aviator never + * processed stay excluded. Last TagHistory writer is applied later from the FPR. */ public static long getAuditableIssueCount(UnirestInstance unirest, SSCAppVersionDescriptor av, AviatorLoggerImpl logger, boolean noFilterSet, String filterSetTitleOrId, List folderNames, boolean forceReaudit) { @@ -349,10 +349,13 @@ private static long countEligibleOnPage(ArrayNode issues, boolean forceReaudit) /** * Force re-audit includes previously processed Aviator issues even when SSC - * already marks them audited. Human-audited issues that Aviator never processed - * stay out. Normal audit still excludes processed issues. + * already marks them audited. Suppressed issues and human-audited issues that + * Aviator never processed stay out. Normal audit still excludes processed issues. */ private static boolean isEligibleForRequestedAudit(JsonNode issue, boolean forceReaudit) { + if (issue.path("suppressed").asBoolean(false)) { + return false; + } if (isProcessedByAviator(issue)) { return forceReaudit; } diff --git a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java index adb8b2edc2c..c1a3f0f6122 100644 --- a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java +++ b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAuditHelperTest.java @@ -81,6 +81,15 @@ void forceReauditPreflightExcludesHumanAuditedIssuesThatAviatorNeverProcessed() } } + @Test + void forceReauditPreflightExcludesSuppressedAviatorIssues() throws Exception { + try (var server = new TestSscServer(processedIssue(), suppressedProcessedIssue()); + var unirest = newUnirest(server)) { + assertEquals(1, AviatorSSCAuditHelper.getAuditableIssueCount( + unirest, appVersion(), logger(), true, null, null, true)); + } + } + @Test void forceReauditCategoryBreakdownIncludesFullyAuditedCategories() throws Exception { try (var server = new TestSscServer(processedIssue()); @@ -138,6 +147,12 @@ private static ObjectNode humanAuditedIssue() { return issue; } + private static ObjectNode suppressedProcessedIssue() { + ObjectNode issue = processedIssue(); + issue.put("suppressed", true); + return issue; + } + private static final class TestSscServer implements AutoCloseable { private final HttpServer server; private final ArrayNode issues = JsonHelper.getObjectMapper().createArrayNode(); From 20b7b9a9b32f058e2e9cc6e8e6106ebe226c7c10 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 27 Aug 2026 14:32:31 +0530 Subject: [PATCH 7/9] chore: Simplify skip reason message for ambiguous source context in RemediationProcessor --- .../cli/aviator/fpr/processor/RemediationProcessor.java | 3 --- .../cli/aviator/fpr/processor/RemediationProcessorTest.java | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java index 76d30cfc1a9..2f717dd66b8 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessor.java @@ -569,9 +569,6 @@ private void recordSkipped(Map skippedByReason, String reason) } private String skipReasonLabel(SkipRemediationException exception) { - if (exception.reason == SkipReason.SOURCE_CONTEXT_AMBIGUOUS) { - return exception.getMessage(); - } return exception.reason.displayName; } diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java index 0965c2b4b81..fe171bb4b71 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/fpr/processor/RemediationProcessorTest.java @@ -47,7 +47,7 @@ void skipsAmbiguousContextWithoutChangingSource() throws Exception { assertEquals(0, metric.appliedRemediations()); assertEquals(1, metric.skippedRemediations()); assertEquals(Map.of( - "Source context matched multiple locations in file 'Example.java'; candidate lines: 1, 4", 1), + "Source context matched multiple locations", 1), metric.skippedByReason()); assertEquals(originalSource, Files.readString(sourceFile)); } From e576338734383aa96111bd130ecd03ecbf15e3a4 Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 27 Aug 2026 14:48:51 +0530 Subject: [PATCH 8/9] chore: remove redudant set copy --- .../main/java/com/fortify/cli/aviator/audit/IssueAuditor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java index 05e50ad412f..cdc205d0060 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java @@ -343,7 +343,7 @@ private Set resolveResultTagIds() { if (!StringUtil.isEmpty(mappedTagId)) { tagIds.add(mappedTagId); } - return Set.copyOf(tagIds); + return tagIds; } private String mappedTagId() { From 89cbf4a0dd4c726ae52c5e9a19f9dcd30d8180fd Mon Sep 17 00:00:00 2001 From: kireetivar Date: Fri, 28 Aug 2026 08:58:29 +0530 Subject: [PATCH 9/9] chore: Builder for IssueAuditor, drop unused getter, fix test indent --- .../fortify/cli/aviator/audit/AuditFPR.java | 20 +- .../cli/aviator/audit/IssueAuditor.java | 3 + .../aviator/util/FuzzyContextSearcher.java | 28 +-- .../cli/aviator/audit/IssueAuditorTest.java | 227 +++++++++--------- .../util/FuzzyContextSearcherTest.java | 10 + .../ssc/cli/cmd/AviatorSSCAuditCommand.java | 4 - .../cli/cmd/AviatorSSCAuditCommandTest.java | 7 +- 7 files changed, 158 insertions(+), 141 deletions(-) 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 f00064598a9..7eff9f7a473 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 @@ -129,16 +129,16 @@ private static AuditOutcome performAviatorAudit(ParsedFprData parsedData, Map resultTagIds; + @Builder public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, FPRInfo fprInfo, FilterSelection filterSelection, SourceLanguageResolver sourceLanguageResolver, FVDLMetadata fvdlMetadata, AuditFprOptions options) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/FuzzyContextSearcher.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/FuzzyContextSearcher.java index c767b303863..4a4b8e6dd51 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/FuzzyContextSearcher.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/FuzzyContextSearcher.java @@ -40,10 +40,7 @@ public static List fuzzySearchContextMatches(List sourceLines, boolean contextStartsWithBlank = !normalizedContext.isEmpty() && normalizedContext.get(0).isEmpty(); for (int i = 0; i < normalizedSource.size(); i++) { - boolean sourceStartsWithBlank = normalizedSource.get(i).isEmpty(); - if (contextStartsWithBlank - ? !sourceStartsWithBlank || (i > 0 && normalizedSource.get(i - 1).isEmpty()) - : sourceStartsWithBlank) { + if (isUnusableContextStart(normalizedSource, i, contextStartsWithBlank)) { continue; } Integer matchStart = findContextMatchStart(normalizedSource, normalizedContext, maxMismatches, i); @@ -55,6 +52,14 @@ public static List fuzzySearchContextMatches(List sourceLines, return List.copyOf(matches); } + private static boolean isUnusableContextStart(List normalizedSource, int index, boolean contextStartsWithBlank) { + boolean sourceStartsWithBlank = normalizedSource.get(index).isEmpty(); + if (!contextStartsWithBlank) { + return sourceStartsWithBlank; + } + return !sourceStartsWithBlank || (index > 0 && normalizedSource.get(index - 1).isEmpty()); + } + private static Integer findContextMatchStart(List normalizedSource, List normalizedContext, int maxMismatches, int startIndex) { int mismatchCount = 0; @@ -62,25 +67,18 @@ private static Integer findContextMatchStart(List normalizedSource, List int contextIndex = 0; while (contextIndex < normalizedContext.size() && sourceIndex < normalizedSource.size()) { - String contextLine = normalizedContext.get(contextIndex).trim(); + String contextLine = normalizedContext.get(contextIndex); if (contextLine.isEmpty()) { contextIndex++; continue; } - String sourceLine = normalizedSource.get(sourceIndex).trim(); - while (sourceLine.isEmpty()) { - sourceIndex++; - if (sourceIndex >= normalizedSource.size()) { - break; - } - sourceLine = normalizedSource.get(sourceIndex).trim(); - } + sourceIndex = skipEmptySourceLines(normalizedSource, sourceIndex); if (sourceIndex >= normalizedSource.size()) { break; } - if (!linesSimilar(sourceLine, contextLine)) { + if (!linesSimilar(normalizedSource.get(sourceIndex), contextLine)) { mismatchCount++; if (mismatchCount > maxMismatches) { break; @@ -155,7 +153,7 @@ private static int skipEmptySourceLines(List normalizedSource, int sourc private static List normalizeLines(List lines) { List result = new ArrayList<>(); for (String line : lines) { - result.add(line.trim().replaceAll("\\s+", " ")); + result.add(line.trim().replaceAll("\\s+", " ")); } return result; } diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java index 5fd22435933..145dc6bddfd 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/IssueAuditorTest.java @@ -127,110 +127,110 @@ void testFprInfoMissingBuildIdDefaultsToEmptyString() throws Exception { assertEquals("", fprInfo.getBuildId()); } - @Test - void skipsPreviouslyProcessedAviatorIssueWithoutForceReaudit() throws Exception { + @Test + void skipsPreviouslyProcessedAviatorIssueWithoutForceReaudit() throws Exception { IssueAuditor auditor = createIssueAuditor(false, false, - Map.of(Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR)); + Map.of(Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR)); assertTrue(prepareIssueIds(auditor).isEmpty()); - } + } - @Test - void forceReauditIncludesProcessedAviatorIssueAndIgnoresAviatorOutcomeTag() throws Exception { + @Test + void forceReauditIncludesProcessedAviatorIssueAndIgnoresAviatorOutcomeTag() throws Exception { IssueAuditor auditor = createIssueAuditor(true, false, Map.of( - Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, - Constants.AVIATOR_EXPECTED_OUTCOME_TAG_ID, Constants.EXPLOITABLE)); + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.AVIATOR_EXPECTED_OUTCOME_TAG_ID, Constants.EXPLOITABLE)); assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); - } + } - @Test - void forceReauditIncludesProcessedAviatorIssueWithAnalysisTag() throws Exception { + @Test + void forceReauditIncludesProcessedAviatorIssueWithAnalysisTag() throws Exception { IssueAuditor auditor = createIssueAuditor(true, false, Map.of( - Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, - Constants.ANALYSIS_TAG_ID, Constants.EXPLOITABLE)); + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.ANALYSIS_TAG_ID, Constants.EXPLOITABLE)); assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); - } + } - @Test - void forceReauditStillSkipsSuppressedIssue() throws Exception { + @Test + void forceReauditStillSkipsSuppressedIssue() throws Exception { IssueAuditor auditor = createIssueAuditor(true, true, - Map.of(Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR)); + Map.of(Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR)); assertTrue(prepareIssueIds(auditor).isEmpty()); - } + } - @Test - void forceReauditStillSkipsHumanTriagedIssue() throws Exception { + @Test + void forceReauditStillSkipsHumanTriagedIssue() throws Exception { IssueAuditor auditor = createIssueAuditor(true, false, Map.of( - Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, - Constants.FOD_TAG_ID, Constants.EXPLOITABLE)); + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.FOD_TAG_ID, Constants.EXPLOITABLE)); assertTrue(prepareIssueIds(auditor).isEmpty()); - } + } - @Test - void forceReauditIncludesIssueWithPendingReviewState() throws Exception { - IssueAuditor auditor = createIssueAuditor(true, false, Map.of( - Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, - Constants.FOD_TAG_ID, "Pending Review", - Constants.AUDITOR_STATUS_TAG_ID, Constants.PENDING_REVIEW)); + @Test + void forceReauditIncludesIssueWithPendingReviewState() throws Exception { + IssueAuditor auditor = createIssueAuditor(true, false, Map.of( + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.FOD_TAG_ID, "Pending Review", + Constants.AUDITOR_STATUS_TAG_ID, Constants.PENDING_REVIEW)); - assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); - } + assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); + } - @Test - void treatsAllPendingAnalysisValuesAsUnaudited() throws Exception { - for (String pendingValue : List.of("Pending Review", "Not Set", "Pending Review/Not Set", " pending review ")) { - IssueAuditor auditor = createIssueAuditor(false, false, Map.of(Constants.ANALYSIS_TAG_ID, pendingValue)); + @Test + void treatsAllPendingAnalysisValuesAsUnaudited() throws Exception { + for (String pendingValue : List.of("Pending Review", "Not Set", "Pending Review/Not Set", " pending review ")) { + IssueAuditor auditor = createIssueAuditor(false, false, Map.of(Constants.ANALYSIS_TAG_ID, pendingValue)); - assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor), pendingValue); - } + assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor), pendingValue); } + } - @Test - void forceReauditStillSkipsIssueWithManualAuditorStatus() throws Exception { + @Test + void forceReauditStillSkipsIssueWithManualAuditorStatus() throws Exception { IssueAuditor auditor = createIssueAuditor(true, false, Map.of( - Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, - Constants.AUDITOR_STATUS_TAG_ID, Constants.EXPLOITABLE), - Map.of(Constants.AUDITOR_STATUS_TAG_ID, "analyst.user")); + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.AUDITOR_STATUS_TAG_ID, Constants.EXPLOITABLE), + Map.of(Constants.AUDITOR_STATUS_TAG_ID, "analyst.user")); assertTrue(prepareIssueIds(auditor).isEmpty()); - } + } - @Test - void forceReauditIncludesAviatorWrittenAuditorStatus() throws Exception { + @Test + void forceReauditIncludesAviatorWrittenAuditorStatus() throws Exception { IssueAuditor auditor = createIssueAuditor(true, false, Map.of( - Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, - Constants.AUDITOR_STATUS_TAG_ID, Constants.EXPLOITABLE), - Map.of(Constants.AUDITOR_STATUS_TAG_ID, Constants.USER_NAME)); + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.AUDITOR_STATUS_TAG_ID, Constants.EXPLOITABLE), + Map.of(Constants.AUDITOR_STATUS_TAG_ID, Constants.USER_NAME)); assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); - } + } - @Test - void forceReauditSkipsHumanAnalysisOverrideAfterAviator() throws Exception { + @Test + void forceReauditSkipsHumanAnalysisOverrideAfterAviator() throws Exception { IssueAuditor auditor = createIssueAuditor(true, false, Map.of( - Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, - Constants.ANALYSIS_TAG_ID, Constants.EXPLOITABLE), - Map.of(Constants.ANALYSIS_TAG_ID, "analyst.user")); + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.ANALYSIS_TAG_ID, Constants.EXPLOITABLE), + Map.of(Constants.ANALYSIS_TAG_ID, "analyst.user")); assertTrue(prepareIssueIds(auditor).isEmpty()); - } + } - @Test - void forceReauditIncludesLegacyFortifyAviatorUsername() throws Exception { + @Test + void forceReauditIncludesLegacyFortifyAviatorUsername() throws Exception { IssueAuditor auditor = createIssueAuditor(true, false, Map.of( - Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, - Constants.ANALYSIS_TAG_ID, Constants.EXPLOITABLE), - Map.of(Constants.ANALYSIS_TAG_ID, Constants.USER_NAME_LEGACY_FORTIFY_AVIATOR)); + Constants.AVIATOR_STATUS_TAG_ID, Constants.PROCESSED_BY_AVIATOR, + Constants.ANALYSIS_TAG_ID, Constants.EXPLOITABLE), + Map.of(Constants.ANALYSIS_TAG_ID, Constants.USER_NAME_LEGACY_FORTIFY_AVIATOR)); assertEquals(List.of(TEST_ISSUE_ID), prepareIssueIds(auditor)); - } + } - @Test - void forceReauditIncludesMappedTagWrittenByAviatorWithoutStatusTag() throws Exception { + @Test + void forceReauditIncludesMappedTagWrittenByAviatorWithoutStatusTag() throws Exception { Path mappingFile = writeMappedTagFile(); try { IssueAuditor auditor = createIssueAuditor(true, false, @@ -241,10 +241,10 @@ void forceReauditIncludesMappedTagWrittenByAviatorWithoutStatusTag() throws Exce } finally { Files.deleteIfExists(mappingFile); } - } + } - @Test - void forceReauditSkipsHumanWriterOnMappedTag() throws Exception { + @Test + void forceReauditSkipsHumanWriterOnMappedTag() throws Exception { Path mappingFile = writeMappedTagFile(); try { IssueAuditor auditor = createIssueAuditor(true, false, @@ -255,25 +255,25 @@ void forceReauditSkipsHumanWriterOnMappedTag() throws Exception { } finally { Files.deleteIfExists(mappingFile); } - } + } - private Path writeMappedTagFile() throws IOException { + private Path writeMappedTagFile() throws IOException { Path mappingFile = Files.createTempFile("tag-mapping", ".yaml"); Files.writeString(mappingFile, "tag_id: \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\"\n"); return mappingFile; - } + } - private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags) { + private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags) { return createIssueAuditor(forceReaudit, suppressed, tags, Map.of(), null); - } + } - private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags, - Map lastTagUsernames) { + private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags, + Map lastTagUsernames) { return createIssueAuditor(forceReaudit, suppressed, tags, lastTagUsernames, null); - } + } - private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags, - Map lastTagUsernames, String tagMappingPath) { + private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed, Map tags, + Map lastTagUsernames, String tagMappingPath) { FPRInfo fprInfo = new FPRInfo(fprHandle); FilterTemplate filterTemplate = new FilterTemplate(); filterTemplate.setTagDefinitions(new ArrayList<>()); @@ -282,42 +282,46 @@ private IssueAuditor createIssueAuditor(boolean forceReaudit, boolean suppressed Vulnerability vulnerability = new Vulnerability(); vulnerability.setInstanceID(TEST_ISSUE_ID); AuditIssue auditIssue = AuditIssue.builder() - .instanceId(TEST_ISSUE_ID) - .suppressed(suppressed) - .tags(new HashMap<>(tags)) - .lastTagUsernames(new HashMap<>(lastTagUsernames)) - .build(); - - return new IssueAuditor( - List.of(vulnerability), null, Map.of(TEST_ISSUE_ID, auditIssue), fprInfo, - new FilterSelection(null, null), new SourceLanguageResolver(new FVDLMetadata()), null, - auditOptions(forceReaudit, NO_OP_LOGGER, tagMappingPath)); - } + .instanceId(TEST_ISSUE_ID) + .suppressed(suppressed) + .tags(new HashMap<>(tags)) + .lastTagUsernames(new HashMap<>(lastTagUsernames)) + .build(); + + return IssueAuditor.builder() + .vulnerabilities(List.of(vulnerability)) + .auditIssueMap(Map.of(TEST_ISSUE_ID, auditIssue)) + .fprInfo(fprInfo) + .filterSelection(new FilterSelection(null, null)) + .sourceLanguageResolver(new SourceLanguageResolver(new FVDLMetadata())) + .options(auditOptions(forceReaudit, NO_OP_LOGGER, tagMappingPath)) + .build(); + } - private AuditFprOptions auditOptions(boolean forceReaudit, IAviatorLogger logger) { + private AuditFprOptions auditOptions(boolean forceReaudit, IAviatorLogger logger) { return auditOptions(forceReaudit, logger, null); - } + } - private AuditFprOptions auditOptions(boolean forceReaudit, IAviatorLogger logger, String tagMappingPath) { + private AuditFprOptions auditOptions(boolean forceReaudit, IAviatorLogger logger, String tagMappingPath) { return AuditFprOptions.builder() - .fprHandle(fprHandle) - .logger(logger) - .sscAppName("TestApp") - .sscAppVersion("1.0") - .tagMappingPath(tagMappingPath) - .forceReaudit(forceReaudit) - .build(); - } + .fprHandle(fprHandle) + .logger(logger) + .sscAppName("TestApp") + .sscAppVersion("1.0") + .tagMappingPath(tagMappingPath) + .forceReaudit(forceReaudit) + .build(); + } - @SuppressWarnings("unchecked") - private List prepareIssueIds(IssueAuditor auditor) throws Exception { + @SuppressWarnings("unchecked") + private List prepareIssueIds(IssueAuditor auditor) throws Exception { Method prepareMethod = IssueAuditor.class.getDeclaredMethod("prepareAndFilterPrompts"); prepareMethod.setAccessible(true); ConcurrentLinkedDeque prompts = (ConcurrentLinkedDeque) prepareMethod.invoke(auditor); return prompts.stream() - .map(prompt -> prompt.getIssueData().getInstanceID()) - .collect(Collectors.toList()); - } + .map(prompt -> prompt.getIssueData().getInstanceID()) + .collect(Collectors.toList()); + } @Test void testFilterVulnerabilities_LegacySyntaxWithSpaces() throws Exception { @@ -365,11 +369,14 @@ void testFilterVulnerabilities_LegacySyntaxWithSpaces() throws Exception { List inputList = Arrays.asList(targetVuln, hiddenVuln); - IssueAuditor auditor = new IssueAuditor( - inputList, null, new HashMap<>(), fprInfo, - selection, new SourceLanguageResolver(new FVDLMetadata()), null, - auditOptions(false, dummyLogger) - ); + IssueAuditor auditor = IssueAuditor.builder() + .vulnerabilities(inputList) + .auditIssueMap(new HashMap<>()) + .fprInfo(fprInfo) + .filterSelection(selection) + .sourceLanguageResolver(new SourceLanguageResolver(new FVDLMetadata())) + .options(auditOptions(false, dummyLogger)) + .build(); Method filterMethod = IssueAuditor.class.getDeclaredMethod("filterVulnerabilities", List.class, FilterSet.class); filterMethod.setAccessible(true); @@ -378,8 +385,8 @@ selection, new SourceLanguageResolver(new FVDLMetadata()), null, List results = (List) filterMethod.invoke(auditor, inputList, filterSet); List remainingIds = results.stream() - .map(Vulnerability::getInstanceID) - .collect(Collectors.toList()); + .map(Vulnerability::getInstanceID) + .collect(Collectors.toList()); assertEquals(1, remainingIds.size(), "Should verify that exactly one issue remains"); assertTrue(remainingIds.contains("TARGET_ISSUE"), diff --git a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/util/FuzzyContextSearcherTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/util/FuzzyContextSearcherTest.java index 2cdc19eaacf..5e8cc165ebe 100644 --- a/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/util/FuzzyContextSearcherTest.java +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/util/FuzzyContextSearcherTest.java @@ -82,4 +82,14 @@ void shouldKeepPhysicalStartWhenContextBeginsWithBlankLine() throws Exception { assertEquals(List.of(1), matches); } + + @Test + void shouldMatchContextAcrossBlankSourceLines() throws Exception { + List matches = FuzzyContextSearcher.fuzzySearchContextMatches( + List.of("header", "target", "", "", "after", "target", "", "after"), + List.of("target", "after"), + 0); + + assertEquals(List.of(1, 5), matches); + } } \ 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 989ff38b08a..f0c56048b13 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 @@ -144,10 +144,6 @@ boolean isNoFilterSet() { return noFilterSet; } - boolean isForceReaudit() { - return forceReaudit; - } - 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()); 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 8bbc0f47531..c1041fd5a75 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 @@ -17,6 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.Map; @@ -53,9 +54,11 @@ void testAllowsNoFilterSetOption() { } @Test - void testAllowsForceReauditOption() { + void testAllowsForceReauditOption() throws Exception { var cmd = parse("--force-reaudit"); - assertTrue(cmd.isForceReaudit()); + Field field = AviatorSSCAuditCommand.class.getDeclaredField("forceReaudit"); + field.setAccessible(true); + assertTrue((boolean) field.get(cmd)); } @Test