From 87d87a024fd23125b0c7a2c81416273b36c039af Mon Sep 17 00:00:00 2001 From: kadraman Date: Thu, 6 Mar 2025 11:57:21 +0000 Subject: [PATCH] FoD: Implement `fcli fod issue update` command (resolves #669) --- .../fortify/cli/fod/_common/rest/FoDUrls.java | 1 + .../cli/fod/_common/util/FoDEnums.java | 100 ++++++++++++++++++ .../fod/issue/cli/cmd/FoDIssueCommands.java | 1 + .../issue/cli/cmd/FoDIssueUpdateCommand.java | 99 +++++++++++++++++ .../helper/FoDBulkIssueUpdateRequest.java | 80 ++++++++++++++ .../helper/FoDBulkIssueUpdateResponse.java | 36 +++++++ .../cli/fod/issue/helper/FoDIssueHelper.java | 44 ++++++++ .../cli/fod/i18n/FoDMessages.properties | 19 +++- 8 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateResponse.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/FoDUrls.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/FoDUrls.java index 6ff229b9b5..b2c04ad060 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/FoDUrls.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/FoDUrls.java @@ -57,4 +57,5 @@ public class FoDUrls { public static final String REPORTS = ApiBase + "/reports"; public static final String REPORT = ApiBase + "/reports/{reportId}"; public static final String SCAN_POLLING_SUMMARY = ApiBase + "/releases/{relId}/scans/{scanId}/polling-summary"; + public static final String VULNERABILITIES = ApiBase + "/releases/{relId}/vulnerabilities"; } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/util/FoDEnums.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/util/FoDEnums.java index 4754218578..c2f0d83e69 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/util/FoDEnums.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/util/FoDEnums.java @@ -506,4 +506,104 @@ public static AttributeTypes fromInt(int val) { } } + public enum VulnerabilitySeverityType { + // use same integer values as FoD uses internally + Low(1), + Medium(2), + High(3), + Critical(4), + Info(-2), + BestPractice(-1); + + private final int _val; + + VulnerabilitySeverityType(int val) { + this._val = val; + } + + public int getValue() { + return this._val; + } + + public String toString() { + switch (this._val) { + case 1: + return "Low"; + case 2: + return "Medium"; + case 3: + return "High"; + case 4: + return "Critical"; + case -1: + return "Best Practice"; + case -2: + return "Info"; + default: + return "Low"; + } + } + + public static VulnerabilitySeverityType fromInt(int val) { + switch (val) { + case 1: + return Low; + case 2: + return Medium; + case 3: + return High; + case 4: + return Critical; + case -1: + return BestPractice; + case -2: + return Info; + default: + return Info; + } + } + } + + public enum DeveloperStatusType { + // no internal integer id representation + Open("Open"), + InRemediation("In Remediation"), + Remediated("Remediated"), + WillNotFix("Will Not Fix"), + ThirdPartyComponent("Third Party Component"); + + public final String value; + + DeveloperStatusType(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + } + + public enum AuditorStatusType { + // no internal integer id representation + PendingReview("Pending Review"), + RemediationRequired("Remediation Required"), + RemediationDeferred("Remediation Deferred"), + RiskMitigated("Risk Mitigated"), + // the following are used by Aviator and should not be set by a user + //Suspicious("Suspicious"), + //ProposedNotAnIssue("Proposed Not an Issue"), + RiskAccepted("Risk Accepted"), + NotAnIssue("Not an Issues"); + + public final String value; + + AuditorStatusType(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + } + } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueCommands.java index df459bc3ae..40aaea901a 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueCommands.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueCommands.java @@ -19,6 +19,7 @@ @CommandLine.Command(name = "issue", subcommands = { FoDIssueListCommand.class, + FoDIssueUpdateCommand.class, } ) //@DefaultVariablePropertyName("applicationId") 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 new file mode 100644 index 0000000000..8d52b99afb --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueUpdateCommand.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright 2021, 2025 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.fod.issue.cli.cmd; + +import java.util.ArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; +import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.util.FoDEnums.DeveloperStatusType; +import com.fortify.cli.fod._common.util.FoDEnums.AuditorStatusType; +import com.fortify.cli.fod._common.util.FoDEnums.VulnerabilitySeverityType; +import com.fortify.cli.fod.issue.helper.FoDBulkIssueUpdateRequest; +import com.fortify.cli.fod.issue.helper.FoDBulkIssueUpdateResponse; +import com.fortify.cli.fod.issue.helper.FoDIssueHelper; +import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; +import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command(name = OutputHelperMixins.Update.CMD_NAME) +public class FoDIssueUpdateCommand extends AbstractFoDJsonNodeOutputCommand implements IActionCommandResultSupplier { + private static final Logger LOG = LoggerFactory.getLogger(FoDIssueUpdateCommand.class); + @Getter @Mixin private OutputHelperMixins.Update outputHelper; + @Mixin private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins + @Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver; + + @Option(names = {"--user"}, required = true) + protected String user; + @Option(names = {"--dev-status"}, required = false) + protected DeveloperStatusType developerStatus; + @Option(names = {"--auditor-status"}, required = false) + protected AuditorStatusType auditorStatus; + @Option(names = {"--severity"}, required = false) + protected VulnerabilitySeverityType severity; + @Option(names = {"--comment"}, required = false) + protected String comment; + @Option(names = {"--ids"}, required = true, split=",") + protected ArrayList vulnIds; + + private long errorCount = 0; + + @Override + public JsonNode getJsonNode(UnirestInstance unirest) { + FoDReleaseDescriptor releaseDescriptor = releaseResolver.getReleaseDescriptor(unirest); + + FoDBulkIssueUpdateRequest issueUpdateRequest = FoDBulkIssueUpdateRequest.builder() + .user(unirest, user) + .developerStatus(developerStatus.getValue()) + .auditorStatus(auditorStatus.getValue()) + .severity(severity.toString()) + .comment(comment) + .vulnerabilityIds(vulnIds) + .build().validate(); + + LOG.debug("Updating issues: {}", vulnIds.toString()); + FoDBulkIssueUpdateResponse resp = FoDIssueHelper.updateIssues(unirest, releaseDescriptor.getReleaseId(), issueUpdateRequest); + errorCount = resp.getResults() + .stream() + .filter(r -> r.getErrorCode() != 0) + .count(); + resp.setIssueCount(resp.getResults().size()); + resp.setErrorCount(errorCount); + LOG.debug("Response: {}", resp.getResults().toString()); + + return resp.asObjectNode().put("issueCount", resp.getResults().size()).put("errorCount", errorCount); + } + + @Override + public String getActionCommandResult() { + return (errorCount == 0 ? "ISSUES_UPDATED" : "ISSUES_UPDATED_WITH_ERRORS"); + } + + @Override + public boolean isSingular() { + return true; + } + +} 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 new file mode 100644 index 0000000000..df6e0cf4ba --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateRequest.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright 2021, 2025 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.fod.issue.helper; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.exception.FcliSimpleException; +import com.fortify.cli.common.util.StringUtils; +import com.fortify.cli.fod.access_control.helper.FoDUserHelper; +import kong.unirest.UnirestInstance; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Reflectable @NoArgsConstructor @AllArgsConstructor +@Getter +@ToString +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FoDBulkIssueUpdateRequest { + private Integer userId; + private String developerStatus; + private String auditorStatus; + private String severity; + private String comment; + private ArrayList vulnerabilityIds; + + @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)); }); + } + + @JsonIgnore + private final void validateRequired(List messages, Object obj, String message) { + if ( obj==null || (obj instanceof String && StringUtils.isBlank((String)obj)) ) { + messages.add(message); + } + } + + public static class FoDBulkIssueUpdateRequestBuilder { + public FoDBulkIssueUpdateRequestBuilder user(UnirestInstance unirest, String user) { + int userId = 0; + if (user == null) return userId(null); + try { + userId = Integer.parseInt(user); + } catch (NumberFormatException nfe) { + userId = FoDUserHelper.getUserDescriptor(unirest, user, true).getUserId(); + } + return userId(userId); + } + } +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateResponse.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateResponse.java new file mode 100644 index 0000000000..f13ff4b002 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDBulkIssueUpdateResponse.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright 2021, 2025 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.fod.issue.helper; + +import java.util.ArrayList; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.json.JsonNodeHolder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public class FoDBulkIssueUpdateResponse extends JsonNodeHolder { + private ArrayList results; + private long issueCount; + private long errorCount; + + @Reflectable @NoArgsConstructor + @Data + public static final class VulnerabilityBulkUpdateResult { + private String vulnerabilityId; + private Integer errorCode; + } +} 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 new file mode 100644 index 0000000000..fb2c194674 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.fod.issue.helper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.output.transform.fields.RenameFieldsTransformer; +import com.fortify.cli.fod._common.rest.FoDUrls; +import kong.unirest.UnirestInstance; +import lombok.Getter; + +public class FoDIssueHelper { + @Getter + private static ObjectMapper objectMapper = new ObjectMapper(); + + public static final JsonNode transformRecord(JsonNode record) { + return new RenameFieldsTransformer(new String[]{}).transform(record); + } + + public static final FoDBulkIssueUpdateResponse updateIssues(UnirestInstance unirest, String releaseId, FoDBulkIssueUpdateRequest issueUpdateRequest) { + ObjectNode body = objectMapper.valueToTree(issueUpdateRequest); + var result = unirest.post(FoDUrls.VULNERABILITIES + "/bulk-edit") + .routeParam("relId", releaseId) + .body(body).asObject(JsonNode.class).getBody(); + return getResponse(result); + } + + private static final FoDBulkIssueUpdateResponse getResponse(JsonNode node) { + return node==null ? null : JsonHelper.treeToValue(node, FoDBulkIssueUpdateResponse.class); + } + +} 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 e79124ee83..9acb852ef2 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 @@ -839,6 +839,11 @@ fcli.fod.oss-scan.download-latest.file = File path and name where to save the SB # fcli fod issue fcli.fod.issue.usage.header = Manage FoD issues (vulnerabilities) and related entities. +fcli.fod.issue.output.table.header.vulnId = Vuln Id +fcli.fod.issue.output.table.header.errorCode = Error Code +fcli.fod.issue.output.table.header.issueCount = Issues +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 \ for a given release. By default, only visible issues will be returned; the --include option can \ @@ -865,6 +870,17 @@ fcli.fod.issue.list.includeIssue = By default, only visible issues will be retur accepts a comma-separated list to allow (also) fixed and/or suppressed issues to be returned, \ 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.update.usage.header = Bulk update vulnerabilities. +fcli.fod.issue.update.usage.description = This command allows for updating the audit information \ + for multiple vulnerabilities. Note: the "id" here refers to the "vulnId" field which is not displayed \ + in the FoD UI but 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. Allowed values: ${COMPLETION-CANDIDATES}. +fcli.fod.issue.update.auditor-status = The Auditor Status to set for the vulnerabilities. Allowed values: ${COMPLETION-CANDIDATES}. +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 the vulnerabilities that are updated. +fcli.fod.issue.update.ids = Comma separate list of the vulnerability ids to be updated. + # fcli fod report fcli.fod.report.usage.header = Manage FoD reports. @@ -948,4 +964,5 @@ fcli.fod.session.output.table.args = name,type,url,created,expires,expired fcli.fod.rest.lookup.output.table.args = group,text,value fcli.fod.report.output.table.args = reportId,reportName,reportStatusType,reportType fcli.fod.report.report-template.output.table.args = value,text,group -fcli.fod.issue.list.output.table.args = id,visibilityMarker,primaryLocation,lineNumber,category +fcli.fod.issue.list.output.table.args = id,vulnId,visibilityMarker,primaryLocation,lineNumber,category,severity +fcli.fod.issue.update.output.table.args = issueCount,errorCount