From 361a7b444a77182b3ecfce46e8ce56e998ab4300 Mon Sep 17 00:00:00 2001 From: Neeta Meshram Date: Wed, 25 Feb 2026 10:01:44 +0530 Subject: [PATCH 1/2] Add issue prioritization by folder for audit commands This commit implements quota-based filtering with folder priority ordering for SSC audit operations. Key changes: - Add quota and quotaLastUpdated fields to InitRequest proto message - Implement QuotaBasedFilter class for filtering user prompts based on available quota and folder priority - Add --folder-priority-order CLI option to AviatorSSCAuditCommand for custom priority ordering - Modify AviatorStreamProcessor to apply quota filtering before processing prompts - Update StreamState to track quota information from server - Add QuotaConfig model for quota configuration - Update bulkaudit.yaml action to support folder priority ordering - Add unit tests for QuotaBasedFilter - Pass custom priority order through IssueAuditor constructor When quota is less than the number of issues to audit, prompts are filtered based on: 1. Custom folder priority order if provided via --folder-priority-order 2. Default priority order (Critical, High, Medium, Low) if no custom order specified 3. Prompts with folders not in the custom order are excluded from filtering This ensures efficient use of limited quota by prioritizing issues based on their folder/severity. # Conflicts: # fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/AuditFPR.java # fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/IssueAuditor.java # fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java --- .../fortify/cli/aviator/audit/AuditFPR.java | 13 +- .../cli/aviator/audit/IssueAuditor.java | 14 +- .../cli/aviator/audit/QuotaBasedFilter.java | 185 ++++++++++++++++++ .../aviator/audit/model/AuditFprOptions.java | 3 +- .../cli/aviator/audit/model/QuotaConfig.java | 114 +++++++++++ .../cli/aviator/grpc/AviatorGrpcClient.java | 3 + .../aviator/grpc/AviatorStreamProcessor.java | 86 +++++++- .../fortify/cli/aviator/grpc/StreamState.java | 12 +- .../src/main/proto/issue.proto | 4 +- .../cli/aviator/audit/IssueAuditorTest.java | 2 +- .../aviator/audit/QuotaBasedFilterTest.java | 160 +++++++++++++++ .../ssc/cli/cmd/AviatorSSCAuditCommand.java | 14 ++ .../aviator/i18n/AviatorMessages.properties | 1 + .../cli/ssc/actions/zip/bulkaudit.yaml | 19 +- 14 files changed, 611 insertions(+), 19 deletions(-) create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/QuotaBasedFilter.java create mode 100644 fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/QuotaConfig.java create mode 100644 fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/QuotaBasedFilterTest.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 f817251971a..3c3dbfde29e 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 @@ -108,10 +108,19 @@ private static AuditOutcome performAviatorAudit( ParsedFprData parsedData, IAviatorLogger logger, String token, String appVersion, String url, String sscAppName, String sscAppVersion, Map auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle) { + Map auditResponsesToFill, FilterSelection filterSelection, + List folderPriorityOrder) { IssueAuditor issueAuditor = new IssueAuditor( - parsedData.vulnerabilities, parsedData.auditProcessor, parsedData.auditIssueMap, - parsedData.fprInfo, sscAppName, sscAppVersion, filterSelection, logger + parsedData.vulnerabilities, + parsedData.auditProcessor, + parsedData.auditIssueMap, + parsedData.fprInfo, + sscAppName, + sscAppVersion, + filterSelection, + logger, + folderPriorityOrder ); 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 703a886afa4..88c8ce3b586 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 @@ -75,15 +75,16 @@ public class IssueAuditor { private final FPRInfo fprInfo; private final FilterSelection filterSelection; - private final TagDefinition analysisTag; private TagDefinition humanAuditTag; private TagDefinition aviatorStatusTag; 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) { + public IssueAuditor(List vulnerabilities, AuditProcessor auditProcessor, Map auditIssueMap, FPRInfo fprInfo, String SSCApplicationName, String SSCApplicationVersion, FilterSelection filterSelection , IAviatorLogger logger, List customPriorityOrder) { this.logger = logger; + this.customPriorityOrder = customPriorityOrder; this.MAX_PER_CATEGORY = Constants.MAX_PER_CATEGORY; this.MAX_TOTAL = Constants.MAX_TOTAL; this.MAX_PER_CATEGORY_EXCEEDED = Constants.MAX_PER_CATEGORY_EXCEEDED; @@ -155,7 +156,7 @@ public AuditOutcome performAudit(Map auditResponses, Stri } else { try (AviatorGrpcClient client = AviatorGrpcClientHelper.createClient(url, logger, DEFAULT_PING_INTERVAL_SECONDS)) { CompletableFuture> future = - client.processBatchRequests(promptsToAudit, projectName, fprInfo.getBuildId(), SSCApplicationName, SSCApplicationVersion, token, fprHandle); + client.processBatchRequests(promptsToAudit, projectName, fprInfo.getBuildId(), SSCApplicationName, SSCApplicationVersion, token, fprHandle, customPriorityOrder); Map responses = future.get(500, TimeUnit.MINUTES); responses.forEach((requestId, response) -> auditResponses.put(response.getIssueId(), response)); logger.progress("Audit completed"); @@ -204,10 +205,11 @@ private ConcurrentLinkedDeque prepareAndFilterPrompts() { .collect(Collectors.toList()); // Apply secondary checks (like 'isAudited') + prompts = prompts.stream() + .filter(this::shouldInclude) + .collect(Collectors.toList()); - return prompts.stream() - .filter(this::shouldInclude).collect(Collectors.toCollection(ConcurrentLinkedDeque::new)); - + return prompts.stream().collect(Collectors.toCollection(ConcurrentLinkedDeque::new)); } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/QuotaBasedFilter.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/QuotaBasedFilter.java new file mode 100644 index 00000000000..5800645935c --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/QuotaBasedFilter.java @@ -0,0 +1,185 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.aviator.audit; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fortify.cli.aviator.audit.model.UserPrompt; +import com.fortify.cli.aviator.util.StringUtil; + +/** + * Utility class for filtering UserPrompts based on quota constraints. + * Supports priority-based filtering with default or custom priority ordering. + */ +public class QuotaBasedFilter { + + /** + * Default priority ordering: Critical > High > Medium > Low + */ + private static final Map DEFAULT_PRIORITY_ORDER = Map.of( + "Critical", 4, + "High", 3, + "Medium", 2, + "Low", 1 + ); + + /** + * Filters UserPrompts to fit within the specified quota based on priority. + * Uses default priority ordering (Critical > High > Medium > Low). + * + * @param prompts List of UserPrompts to filter + * @param quota Maximum number of prompts to retain + * @return Filtered list of UserPrompts, sorted by priority (highest first) + */ + public static List filterByQuota(List prompts, long quota) { + return filterByQuota(prompts, quota, null); + } + + /** + * Filters UserPrompts to fit within the specified quota based on priority. + * Supports custom priority ordering provided by the client. + * + * @param prompts List of UserPrompts to filter + * @param quota Maximum number of prompts to retain + * @param customPriorityOrder Custom priority ordering map (priority name -> rank). + * Higher rank values indicate higher priority. + * If null, uses default ordering. + * When custom priority order is provided, prompts with priorities + * not in the custom order are excluded from filtering. + * This may result in fewer prompts than the quota limit. + * @return Filtered list of UserPrompts, sorted by priority (highest first) + */ + public static List filterByQuota(List prompts, long quota, + Map customPriorityOrder) { + if (prompts == null || prompts.isEmpty()) { + return List.of(); + } + + if (quota <= 0) { + return List.of(); + } + + boolean useCustomPriorityOrder = (customPriorityOrder != null && !customPriorityOrder.isEmpty()); + + Map priorityOrder = useCustomPriorityOrder + ? customPriorityOrder + : DEFAULT_PRIORITY_ORDER; + + // When custom priority order is provided, filter out prompts with unknown priorities + List eligiblePrompts = prompts; + if (useCustomPriorityOrder) { + eligiblePrompts = prompts.stream() + .filter(prompt -> { + String priority = extractPriority(prompt); + return priorityOrder.containsKey(priority); + }) + .collect(Collectors.toList()); + } + + if (eligiblePrompts.size() <= quota) { + return eligiblePrompts; + } + + Comparator priorityComparator = createPriorityComparator(priorityOrder); + + return eligiblePrompts.stream() + .sorted(priorityComparator) + .limit(quota) + .collect(Collectors.toList()); + } + + /** + * Creates a comparator for UserPrompts based on priority ordering. + * Prompts with unknown priorities are ranked lowest. + * + * @param priorityOrder Map of priority names to rank values + * @return Comparator that sorts UserPrompts by priority (descending) + */ + private static Comparator createPriorityComparator(Map priorityOrder) { + return (a, b) -> { + String aPriority = extractPriority(a); + String bPriority = extractPriority(b); + + int aRank = priorityOrder.getOrDefault(aPriority, 0); + int bRank = priorityOrder.getOrDefault(bPriority, 0); + + // Sort in descending order (highest priority first) + int comparison = Integer.compare(bRank, aRank); + + // If priorities are equal, maintain stable sort by instanceID + if (comparison == 0) { + String aId = a.getIssueData().getInstanceID(); + String bId = b.getIssueData().getInstanceID(); + if (aId != null && bId != null) { + return aId.compareTo(bId); + } + } + + return comparison; + }; + } + + /** + * Extracts priority from UserPrompt. + * Handles null or empty priority values gracefully. + * + * @param prompt UserPrompt to extract priority from + * @return Priority string, or "Unknown" if not available + */ + private static String extractPriority(UserPrompt prompt) { + if (prompt == null || prompt.getIssueData() == null) { + return "Unknown"; + } + + String priority = prompt.getIssueData().getPriority(); + return StringUtil.isEmpty(priority) ? "Unknown" : priority; + } + + /** + * Builds a custom priority order map from a list of priority names. + * The order in the list determines the rank (first = highest priority). + * + * @param orderedPriorities List of priority names in descending order of importance + * @return Map of priority names to rank values + */ + public static Map buildCustomPriorityOrder(List orderedPriorities) { + if (orderedPriorities == null || orderedPriorities.isEmpty()) { + return new HashMap<>(); + } + + Map priorityOrder = new HashMap<>(); + int rank = orderedPriorities.size(); + + for (String priority : orderedPriorities) { + if (!StringUtil.isEmpty(priority)) { + priorityOrder.put(priority, rank); + rank--; + } + } + + return priorityOrder; + } + + /** + * Returns the default priority ordering used by the filter. + * + * @return Map of default priority names to rank values + */ + public static Map getDefaultPriorityOrder() { + return new HashMap<>(DEFAULT_PRIORITY_ORDER); + } +} 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 759eaa5df7d..54b8c803038 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 @@ -33,4 +33,5 @@ public class AuditFprOptions { private final String filterSetNameOrId; private final boolean noFilterSet; private final List folderNames; -} \ No newline at end of file + private final List folderPriorityOrder; +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/QuotaConfig.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/QuotaConfig.java new file mode 100644 index 00000000000..321a81debcb --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/audit/model/QuotaConfig.java @@ -0,0 +1,114 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.aviator.audit.model; + +import java.util.List; +import java.util.Map; + +import com.formkiq.graalvm.annotations.Reflectable; + +import lombok.Builder; +import lombok.Getter; + +/** + * Configuration for quota-based filtering of issues. + */ +@Getter +@Builder +@Reflectable +public class QuotaConfig { + /** + * Available quota for processing issues. If null or <= 0, no quota limit is applied. + */ + private final Long availableQuota; + + /** + * Custom priority ordering for filtering. Map of priority name to rank. + * Higher rank values indicate higher priority. + * If null or empty, default ordering (Critical > High > Medium > Low) is used. + */ + private final Map customPriorityOrder; + + /** + * Ordered list of priority names (alternative to customPriorityOrder). + * First element has highest priority. If provided, this will be converted + * to a priority order map internally. + */ + private final List orderedPriorities; + + /** + * Checks if quota filtering should be applied. + * + * @return true if quota is set and greater than 0 + */ + public boolean hasQuota() { + return availableQuota != null && availableQuota > 0; + } + + /** + * Checks if custom priority ordering is configured. + * + * @return true if custom priority order or ordered priorities are set + */ + public boolean hasCustomPriorityOrder() { + return (customPriorityOrder != null && !customPriorityOrder.isEmpty()) || + (orderedPriorities != null && !orderedPriorities.isEmpty()); + } + + /** + * Creates a QuotaConfig with no quota limit. + * + * @return QuotaConfig with unlimited quota + */ + public static QuotaConfig noQuota() { + return QuotaConfig.builder().build(); + } + + /** + * Creates a QuotaConfig with the specified quota and default priority ordering. + * + * @param quota Available quota + * @return QuotaConfig with specified quota + */ + public static QuotaConfig withQuota(long quota) { + return QuotaConfig.builder().availableQuota(quota).build(); + } + + /** + * Creates a QuotaConfig with the specified quota and custom priority ordering. + * + * @param quota Available quota + * @param customPriorityOrder Custom priority order map + * @return QuotaConfig with quota and custom ordering + */ + public static QuotaConfig withCustomOrder(long quota, Map customPriorityOrder) { + return QuotaConfig.builder() + .availableQuota(quota) + .customPriorityOrder(customPriorityOrder) + .build(); + } + + /** + * Creates a QuotaConfig with the specified quota and ordered priority list. + * + * @param quota Available quota + * @param orderedPriorities Ordered list of priority names (highest first) + * @return QuotaConfig with quota and ordered priorities + */ + public static QuotaConfig withOrderedPriorities(long quota, List orderedPriorities) { + return QuotaConfig.builder() + .availableQuota(quota) + .orderedPriorities(orderedPriorities) + .build(); + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java index 69d8ee782b4..5d8343f2631 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java @@ -119,6 +119,9 @@ public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAv public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, FprHandle fprHandle) { AviatorStreamProcessor processor = new AviatorStreamProcessor(this, logger, asyncStub, processingExecutor, pingScheduler, pingIntervalSeconds, defaultTimeoutSeconds, fprHandle); CompletableFuture> future = processor.processBatchRequests(requests, projectName, FPRBuildId, SSCApplicationName, SSCApplicationVersion, token); + public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, List customPriorityOrder) { + AviatorStreamProcessor processor = new AviatorStreamProcessor(this, logger, asyncStub, processingExecutor, pingScheduler, pingIntervalSeconds, defaultTimeoutSeconds); + CompletableFuture> future = processor.processBatchRequests(requests, projectName, FPRBuildId, SSCApplicationName, SSCApplicationVersion, token, customPriorityOrder); future.whenComplete((res, th) -> processor.close()); return future.exceptionally(ex -> { Throwable cause = (ex instanceof CompletionException || ex instanceof ExecutionException) && ex.getCause() != null ? ex.getCause() : ex; diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java index 9b2c4ceafa7..8f89f101839 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorStreamProcessor.java @@ -43,6 +43,7 @@ import com.fortify.aviator.grpc.*; import com.fortify.cli.aviator._common.exception.AviatorSimpleException; import com.fortify.cli.aviator._common.exception.AviatorTechnicalException; +import com.fortify.cli.aviator.audit.QuotaBasedFilter; import com.fortify.cli.aviator.audit.model.AuditResponse; import com.fortify.cli.aviator.audit.model.UserPrompt; import com.fortify.cli.aviator.config.IAviatorLogger; @@ -103,7 +104,7 @@ public AviatorStreamProcessor(AviatorGrpcClient client, IAviatorLogger logger, A this.fprHandle = fprHandle; } - public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token) { + public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, List customPriorityOrder) { if (requests == null || requests.isEmpty()) { LOG.info("No issues to process"); return CompletableFuture.completedFuture(new HashMap<>()); @@ -111,6 +112,7 @@ public CompletableFuture> processBatchRequests(Queue< String streamId = UUID.randomUUID().toString(); currentStreamState = new StreamState(streamId, projectName, FPRBuildId, SSCApplicationName, SSCApplicationVersion, token, requests.size()); + currentStreamState.setCustomPriorityOrder(customPriorityOrder); requests.stream().map(RequestWrapper::new).forEach(wrapper -> { this.processingQueue.add(wrapper); @@ -198,7 +200,9 @@ private void startStreamWithRetry(Map responses, AtomicIn reQueueUnprocessedRequests(); } - processRequestQueue(currentStreamState.totalRequests, processedRequests, responses, resultFuture, this.streamLatch); + applyQuotaFilteringIfNeeded(); + + processRequestQueue(currentStreamState.actualIssuesCount, processedRequests, responses, resultFuture, this.streamLatch); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Check if interruption is due to normal shutdown @@ -287,7 +291,7 @@ public void onNext(AuditorResponse response) { } if ("SERVER_BUSY".equals(response.getStatus())) { - handleServerBusy(response.getRequestId(), currentStreamState.totalRequests, + handleServerBusy(response.getRequestId(), currentStreamState.actualIssuesCount, processedRequests, responses, resultFuture, streamLatch); return; } @@ -324,6 +328,18 @@ public void onNext(AuditorResponse response) { if (completedWrapper == null) { if (!isInitialized.get()) { if ("SUCCESS".equals(response.getStatus())) { + if (response.hasQuota()) { + currentStreamState.quota = response.getQuota(); + if (response.hasQuotaLastUpdated()) { + currentStreamState.quotaLastUpdated = response.getQuotaLastUpdated(); + logger.info("Server quota received: {} (last updated: {})", + currentStreamState.quota, currentStreamState.quotaLastUpdated); + } else { + logger.info("Server quota received: {}", currentStreamState.quota); + } + } else { + LOG.debug("No quota limit received from server (unlimited or feature disabled)"); + } isInitialized.set(true); currentStreamState.isStreamInitialized = true; initLatch.countDown(); @@ -363,7 +379,7 @@ public void onNext(AuditorResponse response) { responses.put(instanceId, auditResponse); int completed = processedRequests.incrementAndGet(); - logger.progress("Processed " + completed + " out of " + currentStreamState.totalRequests + " issues"); + logger.progress("Processed " + completed + " out of " + currentStreamState.actualIssuesCount + " issues"); if (completed >= currentStreamState.totalRequests) { logger.info("All requests accounted for, completing stream."); @@ -519,6 +535,68 @@ private void reQueueUnprocessedRequests() { } } + private void applyQuotaFilteringIfNeeded() { + LOG.debug("Applying Quota based filter Quota for the application is {}", currentStreamState.quota); + //currentStreamState.quota = 5; + if (currentStreamState.quota <= 0) { + LOG.debug("No quota filtering applied (quota not set or unlimited)"); + return; + } + + int currentQueueSize = processingQueue.size(); + if (currentQueueSize <= currentStreamState.quota) { + logger.info("Queue size ({}) within quota ({}), no filtering needed", + currentQueueSize, currentStreamState.quota); + return; + } + + logger.warn("Queue size ({}) exceeds reserved quota ({}). Applying priority-based filtering.", + currentQueueSize, currentStreamState.quota); + + List allPrompts = new ArrayList<>(); + processingQueue.forEach(wrapper -> allPrompts.add(wrapper.userPrompt)); + + Map customPriorityMap = null; + if (currentStreamState.customPriorityOrder != null && + !currentStreamState.customPriorityOrder.isEmpty()) { + + customPriorityMap = QuotaBasedFilter.buildCustomPriorityOrder( + currentStreamState.customPriorityOrder); + + logger.info("Using custom folder priority order for filtering: {} (issues with priorities not in this list will be excluded)", + currentStreamState.customPriorityOrder); + } else { + logger.info("Using default priority order for filtering (Critical > High > Medium > Low)"); + } + + List filteredPrompts = QuotaBasedFilter.filterByQuota( + allPrompts, + currentStreamState.quota, + customPriorityMap + ); + + processingQueue.clear(); + currentStreamState.pendingIssueIds.clear(); + + filteredPrompts.stream() + .map(RequestWrapper::new) + .forEach(wrapper -> { + processingQueue.add(wrapper); + currentStreamState.pendingIssueIds.add(wrapper.userPrompt.getIssueData().getInstanceID()); + }); + + currentStreamState.actualIssuesCount = filteredPrompts.size(); + + int excludedCount = currentQueueSize - filteredPrompts.size(); + if (filteredPrompts.size() < currentStreamState.quota && customPriorityMap != null) { + logger.info("Filtering complete: {} issues retained out of {} (filtered out: {}, including issues with unknown priorities)", + filteredPrompts.size(), currentQueueSize, excludedCount); + } else { + logger.info("Filtering complete: {} issues retained out of {} (filtered out: {})", + filteredPrompts.size(), currentQueueSize, excludedCount); + } + } + private boolean isRetryableError(Throwable t) { if (t instanceof StatusRuntimeException sre) { Status.Code code = sre.getStatus().getCode(); diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamState.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamState.java index 08db25048ef..91ab8269bb3 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamState.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/StreamState.java @@ -12,6 +12,7 @@ */ package com.fortify.cli.aviator.grpc; +import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -23,10 +24,14 @@ class StreamState { final String SSCApplicationVersion; final String token; final int totalRequests; + volatile int actualIssuesCount; final Set processedIssueIds = ConcurrentHashMap.newKeySet(); final Set pendingIssueIds = ConcurrentHashMap.newKeySet(); volatile int streamRetryCount = 0; volatile boolean isStreamInitialized = false; + volatile long quota = -1; + volatile String quotaLastUpdated = null; + volatile List customPriorityOrder = null; StreamState(String streamId, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, @@ -38,5 +43,10 @@ class StreamState { this.SSCApplicationVersion = SSCApplicationVersion; this.token = token; this.totalRequests = totalRequests; + this.actualIssuesCount = totalRequests; } -} \ No newline at end of file + + void setCustomPriorityOrder(List customPriorityOrder) { + this.customPriorityOrder = customPriorityOrder; + } +} diff --git a/fcli-core/fcli-aviator-common/src/main/proto/issue.proto b/fcli-core/fcli-aviator-common/src/main/proto/issue.proto index c2d8e85bce9..5541197facb 100644 --- a/fcli-core/fcli-aviator-common/src/main/proto/issue.proto +++ b/fcli-core/fcli-aviator-common/src/main/proto/issue.proto @@ -123,6 +123,8 @@ message AuditorResponse { string requestId = 12; string streamId = 13; PongResponse pong = 14; + optional int64 quota = 15; + optional string quotaLastUpdated = 16; } message AuditResult { @@ -151,4 +153,4 @@ message Change { string from_line = 2; string to_line = 3; string replace_with = 4; -} \ No newline at end of file +} 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 f233f4d3b84..79a2c5b4ec0 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 @@ -129,7 +129,7 @@ void testFilterVulnerabilities_LegacySyntaxWithSpaces() throws Exception { IssueAuditor auditor = new IssueAuditor( inputList, null, new HashMap<>(), fprInfo, - "TestApp", "1.0", selection, dummyLogger + "TestApp", "1.0", selection, dummyLogger, null ); 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/audit/QuotaBasedFilterTest.java b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/QuotaBasedFilterTest.java new file mode 100644 index 00000000000..1ebbe152dbe --- /dev/null +++ b/fcli-core/fcli-aviator-common/src/test/java/com/fortify/cli/aviator/audit/QuotaBasedFilterTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.aviator.audit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fortify.cli.aviator.audit.model.IssueData; +import com.fortify.cli.aviator.audit.model.UserPrompt; + +/** + * Tests for QuotaBasedFilter functionality. + */ +class QuotaBasedFilterTest { + + private List testPrompts; + + @BeforeEach + void setUp() { + testPrompts = new ArrayList<>(); + + // Create test prompts with different priorities + testPrompts.add(createPrompt("1", "Critical")); + testPrompts.add(createPrompt("2", "High")); + testPrompts.add(createPrompt("3", "Medium")); + testPrompts.add(createPrompt("4", "Low")); + testPrompts.add(createPrompt("5", "Critical")); + testPrompts.add(createPrompt("6", "High")); + testPrompts.add(createPrompt("7", "Medium")); + testPrompts.add(createPrompt("8", "Low")); + } + + @Test + void testFilterByQuota_DefaultOrdering() { + // Filter to top 3 + List filtered = QuotaBasedFilter.filterByQuota(testPrompts, 3); + + assertEquals(3, filtered.size()); + + // Should get the 2 Critical prompts first + assertEquals("Critical", filtered.get(0).getIssueData().getPriority()); + assertEquals("Critical", filtered.get(1).getIssueData().getPriority()); + + // Then 1 High priority + assertEquals("High", filtered.get(2).getIssueData().getPriority()); + } + + @Test + void testFilterByQuota_NoFilteringNeeded() { + // Quota larger than list size + List filtered = QuotaBasedFilter.filterByQuota(testPrompts, 100); + + assertEquals(testPrompts.size(), filtered.size()); + } + + @Test + void testFilterByQuota_ZeroQuota() { + List filtered = QuotaBasedFilter.filterByQuota(testPrompts, 0); + + assertTrue(filtered.isEmpty()); + } + + @Test + void testFilterByQuota_CustomOrdering() { + // Custom order: Low > Medium > High > Critical (reverse of default) + Map customOrder = Map.of( + "Low", 4, + "Medium", 3, + "High", 2, + "Critical", 1 + ); + + List filtered = QuotaBasedFilter.filterByQuota(testPrompts, 3, customOrder); + + assertEquals(3, filtered.size()); + + // Should get Low priority prompts first with custom ordering + assertEquals("Low", filtered.get(0).getIssueData().getPriority()); + assertEquals("Low", filtered.get(1).getIssueData().getPriority()); + assertEquals("Medium", filtered.get(2).getIssueData().getPriority()); + } + + @Test + void testBuildCustomPriorityOrder() { + List orderedPriorities = Arrays.asList("Blocker", "Critical", "High", "Medium", "Low"); + + Map priorityOrder = QuotaBasedFilter.buildCustomPriorityOrder(orderedPriorities); + + assertEquals(5, priorityOrder.size()); + assertTrue(priorityOrder.get("Blocker") > priorityOrder.get("Critical")); + assertTrue(priorityOrder.get("Critical") > priorityOrder.get("High")); + assertTrue(priorityOrder.get("High") > priorityOrder.get("Medium")); + assertTrue(priorityOrder.get("Medium") > priorityOrder.get("Low")); + } + + @Test + void testBuildCustomPriorityOrder_WithCustomCategories() { + List orderedPriorities = Arrays.asList("P0", "P1", "P2", "P3"); + + Map priorityOrder = QuotaBasedFilter.buildCustomPriorityOrder(orderedPriorities); + + assertEquals(4, priorityOrder.size()); + assertEquals(4, priorityOrder.get("P0")); + assertEquals(3, priorityOrder.get("P1")); + assertEquals(2, priorityOrder.get("P2")); + assertEquals(1, priorityOrder.get("P3")); + } + + @Test + void testFilterWithUnknownPriorities() { + testPrompts.add(createPrompt("9", null)); + testPrompts.add(createPrompt("10", "")); + testPrompts.add(createPrompt("11", "Unknown")); + + List filtered = QuotaBasedFilter.filterByQuota(testPrompts, 5); + + assertEquals(5, filtered.size()); + + // Known priorities should come first + assertTrue(filtered.stream().limit(2) + .allMatch(p -> "Critical".equals(p.getIssueData().getPriority()))); + } + + @Test + void testGetDefaultPriorityOrder() { + Map defaultOrder = QuotaBasedFilter.getDefaultPriorityOrder(); + + assertEquals(4, defaultOrder.size()); + assertTrue(defaultOrder.get("Critical") > defaultOrder.get("High")); + assertTrue(defaultOrder.get("High") > defaultOrder.get("Medium")); + assertTrue(defaultOrder.get("Medium") > defaultOrder.get("Low")); + } + + private UserPrompt createPrompt(String id, String priority) { + return UserPrompt.builder() + .issueData(IssueData.builder() + .instanceID(id) + .priority(priority) + .build()) + .build(); + } +} 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 201f01b16fa..08d2110e531 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 @@ -69,6 +69,10 @@ public class AviatorSSCAuditCommand extends AbstractSSCJsonNodeOutputCommand imp @Option(names = {"--tag-mapping"}) private String tagMapping; @Option(names = {"--no-filterset"}) private boolean noFilterSet; @Option(names = {"--folder"}, split = ",") @DisableTest(DisableTest.TestType.MULTI_OPT_PLURAL_NAME) private List folderNames; + @Option(names = {"--folder-priority-order"}, split = ",", + description = "Custom priority order by folder (comma-separated, highest first). Example: Critical,High,Medium,Low") + @DisableTest(DisableTest.TestType.MULTI_OPT_PLURAL_NAME) + private List folderPriorityOrder; private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCAuditCommand.class); @Override @@ -123,6 +127,7 @@ private JsonNode processFpr(UnirestInstance unirest, SSCAppVersionDescriptor av, .filterSetNameOrId(filterSetOptions.getFilterSetTitleOrId()) .noFilterSet(noFilterSet) .folderNames(folderNames) + .folderPriorityOrder(folderPriorityOrder) .build()); } @@ -194,4 +199,13 @@ public String getActionCommandResult() { public boolean isSingular() { return true; } + + private List resolvePriorityOrder() { + if (folderPriorityOrder != null && !folderPriorityOrder.isEmpty()) { + return folderPriorityOrder; + } + + + return null; + } } 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 acf7e3173d6..d300d5f419f 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 @@ -121,6 +121,7 @@ fcli.aviator.ssc.audit.tag-mapping = Custom tag mapping for audit results. fcli.aviator.ssc.audit.filterset = Name or ID of the FilterSet to apply. fcli.aviator.ssc.audit.no-filterset = Do not apply any filter sets, including the default enabled filter set from the FPR. fcli.aviator.ssc.audit.folder = Filter issues by a comma-separated list of specific folder names from the selected FilterSet (e.g., 'Hot,Critical'). This option requires a FilterSet to be active. +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.ssc.audit.refresh = By default, this command will refresh the source application version's metrics when copying from it. \ Note that for large applications this can lead to an error if the timeout expires. fcli.aviator.ssc.audit.refresh-timeout = Time-out, for example 30s (30 seconds), 5m (5 minutes), 1h (1 hour). Default value: ${DEFAULT-VALUE} diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml index 3daba5a1c47..27f0875e679 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml @@ -12,6 +12,11 @@ usage: For application versions that don't already exist in Aviator, the action will automatically create them before submitting for audit. + When quota limits are exceeded for any application, issues are prioritized by + folder using either the default order (Critical > High > Medium > Low) or a + custom priority order specified via --folder-priority-order. The same priority + order is applied to all audited versions in the bulk operation. + Either --tag-mapping or --add-aviator-tags must be specified. The default tag mapping leaves unsure issues unaudited, causing indefinite reselection. Use a custom mapping file or 'fcli ssc aviator prepare' to configure Aviator-specific @@ -71,6 +76,11 @@ cli.options: description: "Timeout period for metric refresh, e.g., 30s (30 seconds), 5m (5 minutes), 1h (1 hour). Default: 60s" required: false default: "60s" + folder-priority-order: + names: --folder-priority-order + description: "Custom priority order for folder-based filtering when quota is exceeded (comma-separated, highest priority first). Example: Critical,High,Medium,Low. Applied to all audited versions. Default: uses server default (Critical > High > Medium > Low)" + required: false + default: "" steps: # Configure module @@ -230,7 +240,10 @@ steps: log.progress: Would create app ${project.project_name} - if: ${cli['add-aviator-tags']} log.progress: Would prepare tags for ${project.project_name}:${project.version_name} - - log.progress: Would audit ${project.project_name}:${project.version_name} + - if: "${cli['folder-priority-order'] != null && cli['folder-priority-order'] != ''}" + log.progress: "Would audit ${project.project_name}:${project.version_name} with custom priority order: ${cli['folder-priority-order']}" + - if: "${cli['folder-priority-order'] == null || cli['folder-priority-order'] == ''}" + log.progress: Would audit ${project.project_name}:${project.version_name} - if: ${!cli['dry-run']} do: - var.set: @@ -284,13 +297,13 @@ steps: - if: ${cli['tag-mapping'] != null && cli['tag-mapping'] != ''} run.fcli: run_audit: - cmd: 'aviator ssc audit --av "${project.project_name}:${project.version_name}" --app "${project.project_name}" --log-level=INFO --tag-mapping="${cli[''tag-mapping'']}" --refresh=${cli.refresh} --refresh-timeout="${cli[''refresh-timeout'']}"' + cmd: "aviator ssc audit --av \"${project.project_name}:${project.version_name}\" --app \"${project.project_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}" status.check: false - if: ${cli['tag-mapping'] == null || cli['tag-mapping'] == ''} run.fcli: run_audit: - cmd: 'aviator ssc audit --av "${project.project_name}:${project.version_name}" --app "${project.project_name}" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout="${cli[''refresh-timeout'']}"' + cmd: "aviator ssc audit --av \"${project.project_name}:${project.version_name}\" --app \"${project.project_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}" status.check: false - if: ${run_audit.exitCode != 0} From 3de405bd8b28d6ba2b049f14902375c87e0fd218 Mon Sep 17 00:00:00 2001 From: Neeta Meshram Date: Thu, 26 Feb 2026 16:33:57 +0530 Subject: [PATCH 2/2] Resolve conflicts after base branch change to feat/v3.x/aviator/26.2 --- .../main/java/com/fortify/cli/aviator/audit/AuditFPR.java | 6 ++---- .../com/fortify/cli/aviator/grpc/AviatorGrpcClient.java | 5 +---- 2 files changed, 3 insertions(+), 8 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 3c3dbfde29e..db924e597b3 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 @@ -65,7 +65,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() + auditResponses, filterSelection, options.getFprHandle(), options.getFolderPriorityOrder() ); // --- STAGE 4: FINALIZATION --- @@ -107,9 +107,7 @@ private static TagMappingConfig loadTagMappingConfig(String tagMappingFilePath) private static AuditOutcome performAviatorAudit( ParsedFprData parsedData, IAviatorLogger logger, String token, String appVersion, String url, String sscAppName, String sscAppVersion, - Map auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle) { - Map auditResponsesToFill, FilterSelection filterSelection, - List folderPriorityOrder) { + Map auditResponsesToFill, FilterSelection filterSelection, FprHandle fprHandle, List folderPriorityOrder) { IssueAuditor issueAuditor = new IssueAuditor( parsedData.vulnerabilities, diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java index 5d8343f2631..f241601463e 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/grpc/AviatorGrpcClient.java @@ -116,11 +116,8 @@ public AviatorGrpcClient(ManagedChannel channel, long defaultTimeoutSeconds, IAv this(channel, defaultTimeoutSeconds, logger, 30); } - public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, FprHandle fprHandle) { + public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, FprHandle fprHandle, List customPriorityOrder) { AviatorStreamProcessor processor = new AviatorStreamProcessor(this, logger, asyncStub, processingExecutor, pingScheduler, pingIntervalSeconds, defaultTimeoutSeconds, fprHandle); - CompletableFuture> future = processor.processBatchRequests(requests, projectName, FPRBuildId, SSCApplicationName, SSCApplicationVersion, token); - public CompletableFuture> processBatchRequests(Queue requests, String projectName, String FPRBuildId, String SSCApplicationName, String SSCApplicationVersion, String token, List customPriorityOrder) { - AviatorStreamProcessor processor = new AviatorStreamProcessor(this, logger, asyncStub, processingExecutor, pingScheduler, pingIntervalSeconds, defaultTimeoutSeconds); CompletableFuture> future = processor.processBatchRequests(requests, projectName, FPRBuildId, SSCApplicationName, SSCApplicationVersion, token, customPriorityOrder); future.whenComplete((res, th) -> processor.close()); return future.exceptionally(ex -> {