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 f817251971..db924e597b 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,11 +107,18 @@ 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, FprHandle fprHandle, 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 703a886afa..88c8ce3b58 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 0000000000..5800645935 --- /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 759eaa5df7..54b8c80303 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 0000000000..321a81debc --- /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 69d8ee782b..f241601463 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,9 +116,9 @@ 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); + 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 9b2c4ceafa..8f89f10183 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 08db25048e..91ab8269bb 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 c2d8e85bce..5541197fac 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 f233f4d3b8..79a2c5b4ec 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 0000000000..1ebbe152db --- /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 201f01b16f..08d2110e53 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 acf7e3173d..d300d5f419 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 3daba5a1c4..27f0875e67 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}