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..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 @@ -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,27 +67,45 @@ 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", "--issue-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) { + effectiveVulnIds = new ArrayList<>(FoDIssueHelper.getAllVulnNumericIdsForRelease(unirest, releaseDescriptor.getReleaseId())); + totalCount = effectiveVulnIds.size(); + issueUpdateCount = totalCount; + if (effectiveVulnIds.isEmpty()) { + return createNoOpResponse(totalCount, skippedCount, issueUpdateCount); + } + } 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())); } + if (effectiveVulnIds.isEmpty()) { + return createNoOpResponse(totalCount, skippedCount, issueUpdateCount); + } } Map attributeUpdates = issueAttrsUpdate.getAttributes(); @@ -95,8 +114,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) @@ -104,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) { @@ -122,20 +153,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..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 @@ -58,11 +58,11 @@ public final FoDBulkIssueUpdateRequest validate(Consumer> validatio @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 ee4c59713f..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 @@ -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,20 @@ 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, \ - 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. \ + 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. +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.