From 10d0c591f6b27952817546d928a7ab969c858a74 Mon Sep 17 00:00:00 2001 From: kadraman Date: Fri, 1 May 2026 11:50:00 +0100 Subject: [PATCH 1/3] feat: `fcli fod issue update` add `--all` option for updating all issues for a release fix: `fcli fod issue update` corrected description of `--user` option fix: `fcli fod issue update` updated descriptions to use issues (vulnerabilities) consistently --- .../issue/cli/cmd/FoDIssueUpdateCommand.java | 45 ++++++++++++++----- .../helper/FoDBulkIssueUpdateRequest.java | 5 --- .../cli/fod/i18n/FoDMessages.properties | 24 +++++----- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java index 55075a320a..89910db969 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java @@ -37,6 +37,7 @@ import kong.unirest.UnirestInstance; import lombok.Getter; +import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; @@ -66,23 +67,43 @@ public class FoDIssueUpdateCommand extends AbstractFoDJsonNodeOutputCommand impl protected VulnerabilitySeverityType severity; @Option(names = {"--comment"}, required = false) protected String comment; - @Option(names = {"--vuln-ids"}, required = true, split=",") - protected ArrayList vulnIds; + @ArgGroup(exclusive = true, multiplicity = "1") + private VulnSelectionArgs vulnSelection; + + static class VulnSelectionArgs { + @Option(names = {"--vuln-ids"}, required = true, split=",") + ArrayList vulnIds; + @Option(names = {"--include-all", "--all"}, required = true) + boolean includeAllVulnerabilities; + } @Override public JsonNode getJsonNode(UnirestInstance unirest) { FoDReleaseDescriptor releaseDescriptor = releaseResolver.getReleaseDescriptor(unirest); - // If vulnIds are provided, filter them against the release vulnerabilities using a helper. int issueUpdateCount = 0; int totalCount = 0; int skippedCount = 0; - if ( vulnIds != null && !vulnIds.isEmpty() ) { - var vulnFilterResult = FoDIssueHelper.filterRequestedVulnIds(unirest, releaseDescriptor.getReleaseId(), vulnIds); + ArrayList effectiveVulnIds; + + if (vulnSelection.includeAllVulnerabilities) { + var allIds = FoDIssueHelper.getVulnIdsForRelease(unirest, releaseDescriptor.getReleaseId(), null); + effectiveVulnIds = new ArrayList<>(allIds); + totalCount = effectiveVulnIds.size(); + issueUpdateCount = totalCount; + if (effectiveVulnIds.isEmpty()) { + return objectMapper.createObjectNode() + .put("totalCount", 0) + .put("skippedCount", 0) + .put("errorCount", 0) + .put("updateCount", 0); + } + } else { + var vulnFilterResult = FoDIssueHelper.filterRequestedVulnIds(unirest, releaseDescriptor.getReleaseId(), vulnSelection.vulnIds); totalCount = vulnFilterResult.totalCount(); issueUpdateCount = vulnFilterResult.kept().size(); skippedCount = vulnFilterResult.skipped().size(); - vulnIds = new ArrayList<>(vulnFilterResult.kept()); + effectiveVulnIds = new ArrayList<>(vulnFilterResult.kept()); if (!vulnFilterResult.skipped().isEmpty()) { LOG.debug("Skipped vulnerabilities: {}", vulnFilterResult.skipped()); vulnFilterResult.skipped().forEach(vid -> LOG.warn("Vulnerability {} not found in release {}, skipping", vid, releaseDescriptor.getReleaseId())); @@ -95,8 +116,8 @@ public JsonNode getJsonNode(UnirestInstance unirest) { // Validate auditor and developer status values against attribute picklists ResolvedStatuses resolvedStatuses = resolveStatuses(unirest); - FoDBulkIssueUpdateRequest issueUpdateRequest = buildIssueUpdateRequest(unirest, resolvedStatuses.developerStatusValue(), resolvedStatuses.auditorStatusValue(), jsonAttrs); - FoDBulkIssueUpdateResponse resp = performUpdate(unirest, releaseDescriptor.getReleaseId(), issueUpdateRequest, totalCount, skippedCount, issueUpdateCount); + FoDBulkIssueUpdateRequest issueUpdateRequest = buildIssueUpdateRequest(unirest, resolvedStatuses.developerStatusValue(), resolvedStatuses.auditorStatusValue(), jsonAttrs, effectiveVulnIds); + FoDBulkIssueUpdateResponse resp = performUpdate(unirest, releaseDescriptor.getReleaseId(), issueUpdateRequest, totalCount, skippedCount, issueUpdateCount, effectiveVulnIds); return resp.asObjectNode() .put("totalCount", totalCount) .put("skippedCount", skippedCount) @@ -122,20 +143,20 @@ private ResolvedStatuses resolveStatuses(UnirestInstance unirest) { return new ResolvedStatuses(developerStatusValue, auditorStatusValue); } - private FoDBulkIssueUpdateRequest buildIssueUpdateRequest(UnirestInstance unirest, String developerStatusValue, String auditorStatusValue, JsonNode jsonAttrs) { + private FoDBulkIssueUpdateRequest buildIssueUpdateRequest(UnirestInstance unirest, String developerStatusValue, String auditorStatusValue, JsonNode jsonAttrs, ArrayList effectiveVulnIds) { return FoDBulkIssueUpdateRequest.builder() .user(unirest, user) .developerStatus(developerStatusValue) .auditorStatus(auditorStatusValue) .severity(severity != null ? severity.toString() : null) .comment(comment) - .vulnerabilityIds(vulnIds) + .vulnerabilityIds(effectiveVulnIds) .attributes(jsonAttrs) .build().validate(); } - private FoDBulkIssueUpdateResponse performUpdate(UnirestInstance unirest, String releaseId, FoDBulkIssueUpdateRequest request, int totalCount, int skippedCount, int issueUpdateCount) { - LOG.debug("Updating issues: {}", vulnIds); + private FoDBulkIssueUpdateResponse performUpdate(UnirestInstance unirest, String releaseId, FoDBulkIssueUpdateRequest request, int totalCount, int skippedCount, int issueUpdateCount, ArrayList effectiveVulnIds) { + LOG.debug("Updating issues: {}", effectiveVulnIds); FoDBulkIssueUpdateResponse resp = FoDIssueHelper.updateIssues(unirest, releaseId, request); long errorCount = resp.getResults().stream().filter(r -> r.getErrorCode() != 0).count(); resp.setIssueCount(resp.getResults().size()); diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java index 93d6326c69..78f00d62b5 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java @@ -48,11 +48,6 @@ public class FoDBulkIssueUpdateRequest { @JsonIgnore public final FoDBulkIssueUpdateRequest validate(Consumer> validationMessageConsumer) { - var messages = new ArrayList(); - validateRequired(messages, vulnerabilityIds, "Vulnerability Ids not specified"); - if ( !messages.isEmpty() ) { - validationMessageConsumer.accept(messages); - } return this; } diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index ee4c59713f..6b19dc29e6 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -889,8 +889,8 @@ fcli.fod.issue.output.table.header.totalCount = Total Issues fcli.fod.issue.output.table.header.updateCount = Issues Updated fcli.fod.issue.output.table.header.skippedCount = Issues Skipped fcli.fod.issue.output.table.header.errorCount = Errors -fcli.fod.issue.list.usage.header = List vulnerabilities. -fcli.fod.issue.list.usage.description = This command allows for listing FoD vulnerability data \ +fcli.fod.issue.list.usage.header = List issues (vulnerabilities). +fcli.fod.issue.list.usage.description = This command allows for listing FoD issue data \ for a given application or release. By default, only visible issues will be returned; the --include option can \ be used to (also) include suppressed or fixed issues. If any such issues are included, the \ default table output will show (S) and/or (F) for respectively suppressed and fixed issues. \ @@ -922,17 +922,19 @@ fcli.fod.issue.list.includeIssue = By default, only visible issues will be retur for example `--include visible,fixed` (to return both visible and fixed issues) or `--include \ fixed` (to return only fixed issues). Allowed values: ${COMPLETION-CANDIDATES}. fcli.fod.issue.list.aggregate = Include aggregation data. -fcli.fod.issue.update.usage.header = Bulk update vulnerabilities. +fcli.fod.issue.update.usage.header = Bulk update issues (vulnerabilities). fcli.fod.issue.update.usage.description = This command allows for updating the audit information \ - for multiple vulnerabilities. Note: for "vuln-ids" you can use either the numeric Id as shown in the FOD UI, \ + for multiple issues (vulnerabilities). Note: for "vuln-ids" you can use either the numeric Id as shown in the FOD UI, \ or the "vulnId" UUID field that is retrieved using the `fcli fod issue ls` command. -fcli.fod.issue.update.user = The username or user id of the user the update will be recorded as. -fcli.fod.issue.update.dev-status = The Developer Status to set for the vulnerabilities, see the FoD UI for valid values. -fcli.fod.issue.update.auditor-status = The Auditor Status to set for the vulnerabilities, see the FoD UI for valid values. -fcli.fod.issue.update.severity = The Severity to set for the vulnerabilities. Allowed values: ${COMPLETION-CANDIDATES}. -fcli.fod.issue.update.comment = A comment to apply to all of the vulnerabilities that are updated. -fcli.fod.issue.update.vuln-ids = Comma separated list of the vulnerability ids to be updated. -fcli.fod.issue.update.attributes = A comma separated list of "attributeName=attributeValue" pairs to set on the vulnerabilities. \ +fcli.fod.issue.update.user = The username or user id of the user to assign the issue to. +fcli.fod.issue.update.dev-status = The Developer Status to set for the issues, see the FoD UI for valid values. +fcli.fod.issue.update.auditor-status = The Auditor Status to set for the issues, see the FoD UI for valid values. +fcli.fod.issue.update.severity = The Severity to set for the issues. Allowed values: ${COMPLETION-CANDIDATES}. +fcli.fod.issue.update.comment = A comment to apply to all of the issues that are updated. +fcli.fod.issue.update.vuln-ids = Comma separated list of the issue ids to be updated. +fcli.fod.issue.update.include-all = Include all the issues from the specified release in the update. \ + %nWARNING: This will update all issues in the specified release regardless of their current status, so use with caution. +fcli.fod.issue.update.attributes = A comma separated list of "attributeName=attributeValue" pairs to set on the issues. \ It is recommended to provide attribute names as they appear in the FoD UI. For example: "Business Criticality=High,Attribute 2=Value". \ You can also use multiple --attributes options to provide more "attributeName=attributeValue" pairs. From 32e50a116fda5a3f970c0de1a18267f70841092b Mon Sep 17 00:00:00 2001 From: kadraman Date: Fri, 1 May 2026 12:26:28 +0100 Subject: [PATCH 2/3] fix: `fcli fod issue update` for consistency added `--issue-ids` alias for `--vuln-ids` --- .../fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java index 89910db969..3104e645f0 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java @@ -71,7 +71,7 @@ public class FoDIssueUpdateCommand extends AbstractFoDJsonNodeOutputCommand impl private VulnSelectionArgs vulnSelection; static class VulnSelectionArgs { - @Option(names = {"--vuln-ids"}, required = true, split=",") + @Option(names = {"--vuln-ids", "--issue-ids"}, required = true, split=",") ArrayList vulnIds; @Option(names = {"--include-all", "--all"}, required = true) boolean includeAllVulnerabilities; From deb70198f6bc04c7b69765f03310f71b6f027452 Mon Sep 17 00:00:00 2001 From: kadraman Date: Fri, 1 May 2026 14:18:30 +0100 Subject: [PATCH 3/3] chore: updated after PR review --- .../issue/cli/cmd/FoDIssueUpdateCommand.java | 24 ++++++++++---- .../helper/FoDBulkIssueUpdateRequest.java | 9 ++++-- .../cli/fod/issue/helper/FoDIssueHelper.java | 32 +++++++++++++++++++ .../cli/fod/i18n/FoDMessages.properties | 5 +-- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java index 3104e645f0..bb701a130d 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java @@ -87,16 +87,11 @@ public JsonNode getJsonNode(UnirestInstance unirest) { ArrayList effectiveVulnIds; if (vulnSelection.includeAllVulnerabilities) { - var allIds = FoDIssueHelper.getVulnIdsForRelease(unirest, releaseDescriptor.getReleaseId(), null); - effectiveVulnIds = new ArrayList<>(allIds); + effectiveVulnIds = new ArrayList<>(FoDIssueHelper.getAllVulnNumericIdsForRelease(unirest, releaseDescriptor.getReleaseId())); totalCount = effectiveVulnIds.size(); issueUpdateCount = totalCount; if (effectiveVulnIds.isEmpty()) { - return objectMapper.createObjectNode() - .put("totalCount", 0) - .put("skippedCount", 0) - .put("errorCount", 0) - .put("updateCount", 0); + return createNoOpResponse(totalCount, skippedCount, issueUpdateCount); } } else { var vulnFilterResult = FoDIssueHelper.filterRequestedVulnIds(unirest, releaseDescriptor.getReleaseId(), vulnSelection.vulnIds); @@ -108,6 +103,9 @@ public JsonNode getJsonNode(UnirestInstance unirest) { LOG.debug("Skipped vulnerabilities: {}", vulnFilterResult.skipped()); vulnFilterResult.skipped().forEach(vid -> LOG.warn("Vulnerability {} not found in release {}, skipping", vid, releaseDescriptor.getReleaseId())); } + if (effectiveVulnIds.isEmpty()) { + return createNoOpResponse(totalCount, skippedCount, issueUpdateCount); + } } Map attributeUpdates = issueAttrsUpdate.getAttributes(); @@ -125,6 +123,18 @@ public JsonNode getJsonNode(UnirestInstance unirest) { .put("updateCount", issueUpdateCount); } + private JsonNode createNoOpResponse(int totalCount, int skippedCount, int issueUpdateCount) { + lastTotalCount = totalCount; + lastSkippedCount = skippedCount; + lastErrorCount = 0; + lastUpdateCount = issueUpdateCount; + return objectMapper.createObjectNode() + .put("totalCount", totalCount) + .put("skippedCount", skippedCount) + .put("errorCount", 0) + .put("updateCount", issueUpdateCount); + } + private record ResolvedStatuses(String developerStatusValue, String auditorStatusValue) {} private ResolvedStatuses resolveStatuses(UnirestInstance unirest) { diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java index 78f00d62b5..74d0b04c2f 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java @@ -48,16 +48,21 @@ public class FoDBulkIssueUpdateRequest { @JsonIgnore public final FoDBulkIssueUpdateRequest validate(Consumer> validationMessageConsumer) { + var messages = new ArrayList(); + validateRequired(messages, vulnerabilityIds, "Vulnerability Ids not specified"); + if ( !messages.isEmpty() ) { + validationMessageConsumer.accept(messages); + } return this; } @JsonIgnore public final FoDBulkIssueUpdateRequest validate() { - return validate(messages->{throw new FcliSimpleException("Unable to update issues:\n\t"+String.join("\n\t", messages)); }); + return validate(messages -> { throw new FcliSimpleException("Unable to update issues:\n\t"+String.join("\n\t", messages)); }); } @JsonIgnore - private final void validateRequired(List messages, Object obj, String message) { + private void validateRequired(List messages, Object obj, String message) { if ( obj==null || (obj instanceof String && StringUtils.isBlank((String)obj)) ) { messages.add(message); } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java index e6e4a906f8..24a29cd5de 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java @@ -365,6 +365,38 @@ public static java.util.Set getVulnIdsForRelease(UnirestInstance unirest return result; } + /** + * Returns the numeric {@code id} for every vulnerability in the release, with no duplicates. + * Unlike {@link #getVulnIdsForRelease}, which collects both {@code id} and {@code vulnId} per + * item (needed for user-supplied identifier matching), this method collects only the canonical + * numeric id so that the result count matches the actual vulnerability count and the bulk-edit + * API is not sent duplicate identifiers. + */ + public static List getAllVulnNumericIdsForRelease(UnirestInstance unirest, String releaseId) { + var result = new ArrayList(); + try { + var request = unirest.get(FoDUrls.VULNERABILITIES) + .routeParam("relId", releaseId) + .queryString("fields", "id") + .queryString("includeFixed", "true") + .queryString("includeSuppressed", "true"); + var stream = FoDPagingHelper.pagedRequest(request).stream() + .map(HttpResponse::getBody) + .map(FoDInputTransformer::getItems) + .filter(items -> items != null && items.isArray()) + .map(ArrayNode.class::cast) + .flatMap(JsonHelper::stream); + for ( JsonNode item : (Iterable)stream::iterator ) { + if ( item.has("id") && !item.get("id").isNull() ) { + result.add(item.get("id").asText().trim()); + } + } + } catch (Exception e) { + throw new FcliTechnicalException("Error retrieving vulnerabilities for release", e); + } + return result; + } + /** * Resolve a status value (developer/auditor) against one or more FoD attribute picklists. * Returns the canonical picklist name when found, or throws a FcliSimpleException listing allowed values. diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index 6b19dc29e6..411e3cfbd5 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -924,8 +924,9 @@ fcli.fod.issue.list.includeIssue = By default, only visible issues will be retur fcli.fod.issue.list.aggregate = Include aggregation data. fcli.fod.issue.update.usage.header = Bulk update issues (vulnerabilities). fcli.fod.issue.update.usage.description = This command allows for updating the audit information \ - for multiple issues (vulnerabilities). Note: for "vuln-ids" you can use either the numeric Id as shown in the FOD UI, \ - or the "vulnId" UUID field that is retrieved using the `fcli fod issue ls` command. + for multiple issues (vulnerabilities). Note: for --vuln-ids/--issue-ids, you can use either the numeric Id as shown in the FoD UI, \ + or the "vulnId" UUID field that is retrieved using the `fcli fod issue ls` command. Alternatively, \ + use --include-all/--all to update all issues from the specified release without providing issue ids. fcli.fod.issue.update.user = The username or user id of the user to assign the issue to. fcli.fod.issue.update.dev-status = The Developer Status to set for the issues, see the FoD UI for valid values. fcli.fod.issue.update.auditor-status = The Auditor Status to set for the issues, see the FoD UI for valid values.