From 5c9d806c73e67f031903da97575e3fce5ac85dbf Mon Sep 17 00:00:00 2001 From: mjain6 Date: Wed, 4 Feb 2026 22:34:50 +0530 Subject: [PATCH 1/4] Added support for Auditing SSC using FCLI --- .../ssc/issue/cli/cmd/SSCIssueCommands.java | 1 + .../issue/cli/cmd/SSCIssueUpdateCommand.java | 271 ++++++++++++++++++ .../helper/SSCIssueCustomTagAuditValue.java | 75 +++++ .../issue/helper/SSCIssueCustomTagHelper.java | 176 ++++++++++++ .../ssc/issue/helper/SSCIssueIdentifier.java | 54 ++++ .../cli/ssc/i18n/SSCMessages.properties | 15 + 6 files changed, 592 insertions(+) create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagAuditValue.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagHelper.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueIdentifier.java diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCommands.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCommands.java index bf3f0aaca9..a3d1b5e40b 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCommands.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCommands.java @@ -33,6 +33,7 @@ SSCIssueGroupListCommand.class, SSCIssueCountCommand.class, SSCIssueListCommand.class, + SSCIssueUpdateCommand.class, } ) public class SSCIssueCommands extends AbstractContainerCommand { diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java new file mode 100644 index 0000000000..900484e77a --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java @@ -0,0 +1,271 @@ +/* + * 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.ssc.issue.cli.cmd; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.exception.FcliSimpleException; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; +import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand; +import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; +import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin; +import com.fortify.cli.ssc.issue.helper.SSCIssueCustomTagAuditValue; +import com.fortify.cli.ssc.issue.helper.SSCIssueCustomTagHelper; +import com.fortify.cli.ssc.issue.helper.SSCIssueIdentifier; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command(name = OutputHelperMixins.Update.CMD_NAME) +@Slf4j +public class SSCIssueUpdateCommand extends AbstractSSCJsonNodeOutputCommand implements IActionCommandResultSupplier { + + @Getter @Mixin private OutputHelperMixins.Update outputHelper; + @Mixin private SSCAppVersionResolverMixin.RequiredOption appVersionResolver; + @Option(names = {"--issue-ids"}, required = true, split = ",") + private List issueIds; + @Option(names = {"--custom-tags", "-t"}, split = ",", paramLabel = "TAG=VALUE") + private Map customTags; + @Option(names = {"--suppress"}, arity = "1", paramLabel = "true|false") + private Boolean suppress; + @Option(names = {"--comment"}) + private String comment; + @Option(names = {"--assign-user"}) + private String assignUser; + + @Override + public JsonNode getJsonNode(UnirestInstance unirest) { + validateInput(); + String appVersionId = appVersionResolver.getAppVersionId(unirest); + List issues = fetchIssueRevisionsFromSSC(unirest, appVersionId, issueIds); + + if (StringUtils.isNotBlank(assignUser)) { + executeAssignUserRequest(unirest, appVersionId, issues, assignUser); + if (isAuditRequired()) { + issues = fetchIssueRevisionsFromSSC(unirest, appVersionId, issueIds); + } + } + + if (isAuditRequired()) { + executeAuditRequest(unirest, appVersionId, issues); + } + + return buildResults(); + } + + private void validateInput() { + if (issueIds == null || issueIds.isEmpty()) { + throw new FcliSimpleException("--issue-ids must be specified"); + } + if (!isAuditRequired() && StringUtils.isBlank(assignUser)) { + throw new FcliSimpleException("At least one of --custom-tags, --suppress, --comment, or --assign-user must be specified"); + } + } + + private boolean isAuditRequired() { + return hasCustomTags() || suppress != null || StringUtils.isNotBlank(comment); + } + + private boolean hasCustomTags() { + return customTags != null && !customTags.isEmpty(); + } + + private JsonNode buildResults() { + ArrayNode results = JsonHelper.getObjectMapper().createArrayNode(); + for (String vulnId : issueIds) { + ObjectNode result = JsonHelper.getObjectMapper().createObjectNode(); + result.put("id", vulnId); + result.put("update", buildUpdateDetails()); + result.put("action", "UPDATED"); + addOptionalFields(result); + results.add(result); + } + return results; + } + + private String buildUpdateDetails() { + StringBuilder details = new StringBuilder(); + if (hasCustomTags()) { + customTags.forEach((key, value) -> + appendDetail(details, "CustomTag: " + key + "=" + (StringUtils.isBlank(value) ? "" : value))); + } + if (suppress != null) { + appendDetail(details, "Suppressed: " + suppress); + } + if (StringUtils.isNotBlank(assignUser)) { + appendDetail(details, "User: " + assignUser); + } + if (StringUtils.isNotBlank(comment)) { + appendDetail(details, "Comment: " + comment); + } + return details.toString(); + } + + private void appendDetail(StringBuilder sb, String detail) { + if (sb.length() > 0) { + sb.append("\n"); + } + sb.append(detail); + } + + private void addOptionalFields(ObjectNode result) { + if (hasCustomTags()) { + result.put("customTags", customTags.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(", "))); + } + if (suppress != null) { + result.put("suppressed", suppress); + } + if (StringUtils.isNotBlank(assignUser)) { + result.put("assignedToUser", assignUser); + } + if (StringUtils.isNotBlank(comment)) { + result.put("comment", comment); + } + } + + private void executeAssignUserRequest(UnirestInstance unirest, String appVersionId, + List issues, String user) { + ObjectNode requestBody = JsonHelper.getObjectMapper().createObjectNode(); + ArrayNode issuesArray = requestBody.putArray("issues"); + for (SSCIssueIdentifier issue : issues) { + ObjectNode issueNode = JsonHelper.getObjectMapper().createObjectNode(); + issueNode.put("id", issue.id()); + issueNode.put("revision", issue.revision()); + issuesArray.add(issueNode); + } + requestBody.put("user", user); + + String url = SSCUrls.PROJECT_VERSION_ISSUES_ACTION_ASSIGN_USER(appVersionId); + log.debug("Assign user request: URL={}, user={}", url, user); + + try { + JsonNode response = unirest.post(url) + .body(requestBody) + .asObject(JsonNode.class) + .getBody(); + validateApiResponse(response, "Assign user operation"); + } catch (FcliSimpleException e) { + throw e; + } catch (Exception e) { + throw new FcliSimpleException("Failed to assign user: " + e.getMessage(), e); + } + } + + private void executeAuditRequest(UnirestInstance unirest, String appVersionId, List issues) { + Map request = new HashMap<>(); + request.put("issues", issues); + if (comment != null) { + request.put("comment", comment); + } + if (suppress != null) { + request.put("suppressed", suppress); + } + if (hasCustomTags()) { + var customTagHelper = new SSCIssueCustomTagHelper(unirest, appVersionId); + List processedTags = customTagHelper.processCustomTags(customTags); + request.put("customTagAudit", processedTags); + } + + String url = SSCUrls.PROJECT_VERSION_ISSUES_ACTION_AUDIT(appVersionId); + log.debug("Audit request: URL={}", url); + + try { + JsonNode response = unirest.post(url) + .body(request) + .asObject(JsonNode.class) + .getBody(); + validateApiResponse(response, "Audit operation"); + } catch (FcliSimpleException e) { + throw e; + } catch (Exception e) { + throw new FcliSimpleException("Failed to perform audit operation: " + e.getMessage(), e); + } + } + + private void validateApiResponse(JsonNode response, String operationName) { + if (response == null) { + throw new FcliSimpleException(operationName + " returned null response"); + } + if (response.has("responseCode")) { + int responseCode = response.get("responseCode").asInt(); + if (responseCode >= 400) { + String message = response.has("message") ? response.get("message").asText() : "Unknown error"; + throw new FcliSimpleException(operationName + " failed with response code " + responseCode + ": " + message); + } + } + } + + @Override + public String getActionCommandResult() { + return "UPDATED"; + } + + @Override + public boolean isSingular() { + return true; + } + + private List fetchIssueRevisionsFromSSC(UnirestInstance unirest, String appVersionId, List issueIds) { + String idsParam = String.join(",", issueIds); + log.debug("Fetching issues with ids: {}", idsParam); + + try { + JsonNode response = unirest.get("/api/v1/projectVersions/{appVersionId}/issues") + .routeParam("appVersionId", appVersionId) + .queryString("ids", idsParam) + .asObject(JsonNode.class) + .getBody(); + + JsonNode dataArray = response.get("data"); + if (dataArray == null || !dataArray.isArray()) { + throw new FcliSimpleException("Invalid response from SSC issues API - missing 'data' field"); + } + + Map idToRevisionMap = new HashMap<>(); + for (JsonNode issueNode : dataArray) { + idToRevisionMap.put(issueNode.get("id").asText(), issueNode.get("revision").asInt()); + } + + for (String issueId : issueIds) { + if (!idToRevisionMap.containsKey(issueId)) { + throw new FcliSimpleException("Issue with ID '" + issueId + "' not found in application version"); + } + } + + return issueIds.stream() + .map(id -> SSCIssueIdentifier.fromIdAndRevision(id, idToRevisionMap.get(id))) + .toList(); + + } catch (FcliSimpleException e) { + throw e; + } catch (Exception e) { + throw new FcliSimpleException("Failed to fetch issue revisions from SSC: " + e.getMessage(), e); + } + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagAuditValue.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagAuditValue.java new file mode 100644 index 0000000000..cde2a73a4e --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagAuditValue.java @@ -0,0 +1,75 @@ +/* + * 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.ssc.issue.helper; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.formkiq.graalvm.annotations.Reflectable; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a custom tag value in an SSC issue audit request. + */ +@JsonInclude(Include.NON_NULL) +@Reflectable +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SSCIssueCustomTagAuditValue { + @JsonProperty("customTagGuid") + private String customTagGuid; + + @JsonProperty("textValue") + private String textValue; + + @JsonProperty("newCustomTagIndex") + private Integer newCustomTagIndex; + + @JsonProperty("dateValue") + private String dateValue; + + @JsonProperty("decimalValue") + private Double decimalValue; + + public static SSCIssueCustomTagAuditValue forText(String guid, String value) { + SSCIssueCustomTagAuditValue result = new SSCIssueCustomTagAuditValue(); + result.setCustomTagGuid(guid); + result.setTextValue(value); + return result; + } + + public static SSCIssueCustomTagAuditValue forList(String guid, Integer lookupIndex) { + SSCIssueCustomTagAuditValue result = new SSCIssueCustomTagAuditValue(); + result.setCustomTagGuid(guid); + result.setNewCustomTagIndex(lookupIndex); + return result; + } + + public static SSCIssueCustomTagAuditValue forDate(String guid, String dateValue) { + SSCIssueCustomTagAuditValue result = new SSCIssueCustomTagAuditValue(); + result.setCustomTagGuid(guid); + result.setDateValue(dateValue); + return result; + } + + public static SSCIssueCustomTagAuditValue forDecimal(String guid, Double value) { + SSCIssueCustomTagAuditValue result = new SSCIssueCustomTagAuditValue(); + result.setCustomTagGuid(guid); + result.setDecimalValue(value); + return result; + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagHelper.java new file mode 100644 index 0000000000..8cf115e70c --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagHelper.java @@ -0,0 +1,176 @@ +/* + * 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.ssc.issue.helper; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.exception.FcliSimpleException; +import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; +import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagValueType; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@RequiredArgsConstructor +public class SSCIssueCustomTagHelper { + private final UnirestInstance unirest; + private final String appVersionId; + + @Getter(lazy = true) + private final Map customTagInfoMap = loadCustomTagInfo(); + + public List processCustomTags(Map customTags) { + if (customTags == null || customTags.isEmpty()) { + return List.of(); + } + + Map tagInfoMap = getCustomTagInfoMap(); + + return customTags.entrySet().stream() + .map(entry -> { + String tagName = entry.getKey(); + String tagValue = entry.getValue(); + CustomTagInfo tagInfo = tagInfoMap.get(tagName.toLowerCase()); + if (tagInfo == null) { + throw new FcliSimpleException("Custom tag '" + tagName + "' is not available for this application version"); + } + return createAuditValue(tagName, tagValue, tagInfo); + }) + .collect(Collectors.toList()); + } + + private SSCIssueCustomTagAuditValue createAuditValue(String tagName, String value, CustomTagInfo tagInfo) { + String guid = tagInfo.getGuid(); + boolean isUnset = value == null || value.isBlank(); + switch (tagInfo.getValueType()) { + case TEXT: + if (isUnset) return SSCIssueCustomTagAuditValue.forText(guid, ""); + return SSCIssueCustomTagAuditValue.forText(guid, value); + case DECIMAL: + if (isUnset) return SSCIssueCustomTagAuditValue.forDecimal(guid, null); + try { + Double decimalValue = Double.parseDouble(value); + return SSCIssueCustomTagAuditValue.forDecimal(guid, decimalValue); + } catch (NumberFormatException e) { + throw new FcliSimpleException("Invalid decimal value '" + value + "' for custom tag '" + tagName + "'"); + } + case DATE: + if (isUnset) return SSCIssueCustomTagAuditValue.forDate(guid, ""); + String dateValue = processDateValue(value, tagName); + return SSCIssueCustomTagAuditValue.forDate(guid, dateValue); + case LIST: + if (isUnset) return SSCIssueCustomTagAuditValue.forList(guid, -1); + Integer lookupIndex = getListValueIndex(value, tagName, tagInfo); + return SSCIssueCustomTagAuditValue.forList(guid, lookupIndex); + default: + throw new FcliSimpleException("Unsupported custom tag value type: " + tagInfo.getValueType()); + } + } + + private String processDateValue(String value, String tagName) { + try { + LocalDate date = LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE); + return date.format(DateTimeFormatter.ISO_LOCAL_DATE); + } catch (DateTimeParseException e) { + throw new FcliSimpleException("Invalid date format '" + value + "' for custom tag '" + tagName + "'. Expected format: yyyy-MM-dd"); + } + } + + private Integer getListValueIndex(String value, String tagName, CustomTagInfo tagInfo) { + if (tagInfo.getValueList() == null || tagInfo.getValueList().isEmpty()) { + throw new FcliSimpleException("Custom tag '" + tagName + "' has no valid list values configured"); + } + + for (ValueListItem item : tagInfo.getValueList()) { + if (value.equalsIgnoreCase(item.getLookupValue())) { + return item.getLookupIndex(); + } + } + + String validValues = tagInfo.getValueList().stream() + .map(ValueListItem::getLookupValue) + .collect(Collectors.joining(", ")); + + throw new FcliSimpleException("Invalid value '" + value + "' for list custom tag '" + tagName + "'. " + + "Valid values are: " + validValues); + } + + private Map loadCustomTagInfo() { + try { + JsonNode response = unirest.get(SSCUrls.PROJECT_VERSION_CUSTOM_TAGS(appVersionId)) + .asObject(JsonNode.class) + .getBody(); + + JsonNode dataArray = response.get("data"); + if (dataArray == null || !dataArray.isArray()) { + throw new FcliSimpleException("Invalid response from custom tags API"); + } + + Map result = new HashMap<>(); + + for (JsonNode tagNode : dataArray) { + CustomTagInfo tagInfo = parseCustomTagInfo(tagNode); + result.put(tagInfo.getName().toLowerCase(), tagInfo); + } + + return result; + } catch (Exception e) { + if (e instanceof FcliSimpleException) { + throw e; + } + throw new FcliSimpleException("Failed to load custom tag information: " + e.getMessage(), e); + } + } + + private CustomTagInfo parseCustomTagInfo(JsonNode tagNode) { + CustomTagInfo tagInfo = new CustomTagInfo(); + tagInfo.setGuid(tagNode.get("guid").asText()); + tagInfo.setName(tagNode.get("name").asText()); + tagInfo.setValueType(SSCCustomTagValueType.valueOf(tagNode.get("valueType").asText())); + + JsonNode valueListNode = tagNode.get("valueList"); + if (valueListNode != null && valueListNode.isArray()) { + for (JsonNode valueNode : valueListNode) { + ValueListItem item = new ValueListItem(); + item.setLookupIndex(valueNode.get("lookupIndex").asInt()); + item.setLookupValue(valueNode.get("lookupValue").asText()); + tagInfo.getValueList().add(item); + } + } + + return tagInfo; + } + + @Getter @Setter + public static class CustomTagInfo { + private String guid; + private String name; + private SSCCustomTagValueType valueType; + private List valueList = new java.util.ArrayList<>(); + } + + @Getter @Setter + public static class ValueListItem { + private int lookupIndex; + private String lookupValue; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueIdentifier.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueIdentifier.java new file mode 100644 index 0000000000..4ed6b2850b --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueIdentifier.java @@ -0,0 +1,54 @@ +/* + * 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.ssc.issue.helper; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.json.JsonHelper; + +import lombok.Data; +import lombok.experimental.Accessors; + +@JsonInclude(Include.NON_NULL) +@Reflectable +@Data @Accessors(fluent=true) +public final class SSCIssueIdentifier { + @JsonProperty("id") + private String id; + + @JsonProperty("revision") + private Integer revision; + + public static final SSCIssueIdentifier fromIdAndRevision(String id, Integer revision) { + return new SSCIssueIdentifier().id(id).revision(revision); + } + + public static final List fromIdList(List ids) { + return ids.stream() + .map(id -> SSCIssueIdentifier.fromIdAndRevision(id, null)) + .toList(); + } + + @Override + public String toString() { + try { + return JsonHelper.getObjectMapper().writeValueAsString(this); + } catch (Exception e) { + return super.toString(); + } + } +} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties index dd5f7c5507..1c39463381 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties @@ -503,6 +503,21 @@ fcli.ssc.issue.list.includeIssue = By default, only visible issues will be retur accepts a comma-separated list to allow (also) removed, suppressed and/or hidden issues to be returned, \ for example `--include visible,removed` (to return both visible and removed issues) or `--include \ removed` (to return only removed issues). Allowed values: ${COMPLETION-CANDIDATES}. +fcli.ssc.issue.update.usage.header = Update application version issues. +fcli.ssc.issue.update.usage.description = This command allows for updating SSC vulnerability data \ + for a given application version. You can assign issues to users, perform audit actions with \ + comments, custom tag updates, and suppression status. At least one of --custom-tags, --suppress, \ + --comment, or --assign-user must be specified along with the required --issue-ids. \ + To see the allowed tags for the specific application version, use: fcli ssc tag get +fcli.ssc.issue.update.issue-ids = Comma separated list of the vulnerability ids to be updated. +fcli.ssc.issue.update.custom-tags = Custom tag to set for the vulnerabilities. Format: tagName=value. \ + Can be specified multiple times or as comma-separated list (e.g., tag1=value1,tag2=value2). \ + For list type tags, use the exact lookup value. Pass empty value to unset (tagName=). \ + For date tags, use format: yyyy-MM-dd. +fcli.ssc.issue.update.suppress = Set the suppression status of the vulnerability. Use true to suppress or false to unsuppress. +fcli.ssc.issue.update.comment = A comment to apply to all the vulnerabilities that are updated. +fcli.ssc.issue.update.assign-user = The username or user id of the user to assign the issues to. +fcli.ssc.issue.update.output.table.args = id,update,action fcli.ssc.issue.get-filter.usage.header = Get issue filter details. fcli.ssc.issue.filter = Technical or friendly filter as returned by the 'fcli ssc issue list-filters' command. fcli.ssc.issue.list-filters.usage.header = List application version issue filters. From ba2de4a9ceae147636b56cde1da39921c576f945 Mon Sep 17 00:00:00 2001 From: mjain6 Date: Wed, 11 Feb 2026 15:17:08 +0530 Subject: [PATCH 2/4] Added changes in Json Output format --- .../issue/cli/cmd/SSCIssueUpdateCommand.java | 71 +++++++++++-------- .../issue/helper/SSCIssueCustomTagHelper.java | 48 +++++++++++++ .../cli/ssc/i18n/SSCMessages.properties | 2 +- 3 files changed, 91 insertions(+), 30 deletions(-) diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java index 900484e77a..10eb553d92 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java @@ -15,7 +15,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -57,8 +56,11 @@ public class SSCIssueUpdateCommand extends AbstractSSCJsonNodeOutputCommand impl @Option(names = {"--assign-user"}) private String assignUser; + private UnirestInstance unirestInstance; + @Override public JsonNode getJsonNode(UnirestInstance unirest) { + this.unirestInstance = unirest; validateInput(); String appVersionId = appVersionResolver.getAppVersionId(unirest); List issues = fetchIssueRevisionsFromSSC(unirest, appVersionId, issueIds); @@ -95,16 +97,43 @@ private boolean hasCustomTags() { } private JsonNode buildResults() { - ArrayNode results = JsonHelper.getObjectMapper().createArrayNode(); + ObjectNode result = JsonHelper.getObjectMapper().createObjectNode(); + + String updatesSummary = buildUpdateDetails(); + + ArrayNode issueIdsArray = result.putArray("issueIds"); for (String vulnId : issueIds) { - ObjectNode result = JsonHelper.getObjectMapper().createObjectNode(); - result.put("id", vulnId); - result.put("update", buildUpdateDetails()); - result.put("action", "UPDATED"); - addOptionalFields(result); - results.add(result); + issueIdsArray.add(vulnId); + } + + result.put("updatesString", updatesSummary); + + // Add customTagUpdates array if custom tags exist + if (hasCustomTags()) { + ArrayNode customTagsArray = result.putArray("customTagUpdates"); + String appVersionId = appVersionResolver.getAppVersionId(unirestInstance); + var customTagHelper = new SSCIssueCustomTagHelper(unirestInstance, appVersionId); + customTagHelper.populateCustomTagUpdates(customTags, customTagsArray); + } + + // Add newComment at top level (renamed from comment) + if (StringUtils.isNotBlank(comment)) { + result.put("newComment", comment); + } + + // Add assignedUser at top level + if (StringUtils.isNotBlank(assignUser)) { + result.put("assignedUser", assignUser); } - return results; + + // Add suppressed at top level + if (suppress != null) { + result.put("suppressed", suppress); + } + + ArrayNode resultsArray = JsonHelper.getObjectMapper().createArrayNode(); + resultsArray.add(result); + return resultsArray; } private String buildUpdateDetails() { @@ -122,32 +151,16 @@ private String buildUpdateDetails() { if (StringUtils.isNotBlank(comment)) { appendDetail(details, "Comment: " + comment); } - return details.toString(); + String result = details.toString(); + return result.isEmpty() ? "No updates" : result; } - + private void appendDetail(StringBuilder sb, String detail) { if (sb.length() > 0) { sb.append("\n"); } sb.append(detail); } - - private void addOptionalFields(ObjectNode result) { - if (hasCustomTags()) { - result.put("customTags", customTags.entrySet().stream() - .map(e -> e.getKey() + "=" + e.getValue()) - .collect(Collectors.joining(", "))); - } - if (suppress != null) { - result.put("suppressed", suppress); - } - if (StringUtils.isNotBlank(assignUser)) { - result.put("assignedToUser", assignUser); - } - if (StringUtils.isNotBlank(comment)) { - result.put("comment", comment); - } - } private void executeAssignUserRequest(UnirestInstance unirest, String appVersionId, List issues, String user) { @@ -268,4 +281,4 @@ private List fetchIssueRevisionsFromSSC(UnirestInstance unir throw new FcliSimpleException("Failed to fetch issue revisions from SSC: " + e.getMessage(), e); } } -} +} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagHelper.java index 8cf115e70c..5ce0396f9f 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagHelper.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueCustomTagHelper.java @@ -21,7 +21,10 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.exception.FcliSimpleException; +import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagValueType; @@ -58,6 +61,51 @@ public List processCustomTags(Map cu .collect(Collectors.toList()); } + public void populateCustomTagUpdates(Map customTags, ArrayNode customTagsArray) { + if (customTags == null || customTags.isEmpty()) { + return; + } + + Map tagInfoMap = getCustomTagInfoMap(); + + customTags.forEach((tagName, tagValue) -> { + CustomTagInfo tagInfo = tagInfoMap.get(tagName.toLowerCase()); + if (tagInfo == null) { + throw new FcliSimpleException("Custom tag '" + tagName + "' is not available for this application version"); + } + + String displayValue = tagValue == null || tagValue.isBlank() ? "" : tagValue; + String valueGuid = getValueGuidForTag(tagValue, tagInfo); + + ObjectNode tagNode = JsonHelper.getObjectMapper().createObjectNode(); + tagNode.put("customTagName", tagInfo.getName()); + tagNode.put("customTagGuid", tagInfo.getGuid()); + tagNode.put("value", displayValue); + if (valueGuid != null) { + tagNode.put("valueGuid", valueGuid); + } + customTagsArray.add(tagNode); + }); + } + + private String getValueGuidForTag(String value, CustomTagInfo tagInfo) { + if (value == null || value.isBlank()) { + return null; + } + + if (tagInfo.getValueType() == SSCCustomTagValueType.LIST) { + if (tagInfo.getValueList() != null) { + for (ValueListItem item : tagInfo.getValueList()) { + if (value.equalsIgnoreCase(item.getLookupValue())) { + return String.valueOf(item.getLookupIndex()); + } + } + } + } + + return null; + } + private SSCIssueCustomTagAuditValue createAuditValue(String tagName, String value, CustomTagInfo tagInfo) { String guid = tagInfo.getGuid(); boolean isUnset = value == null || value.isBlank(); diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties index 1c39463381..b617d1fc2f 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties @@ -517,7 +517,7 @@ fcli.ssc.issue.update.custom-tags = Custom tag to set for the vulnerabilities. F fcli.ssc.issue.update.suppress = Set the suppression status of the vulnerability. Use true to suppress or false to unsuppress. fcli.ssc.issue.update.comment = A comment to apply to all the vulnerabilities that are updated. fcli.ssc.issue.update.assign-user = The username or user id of the user to assign the issues to. -fcli.ssc.issue.update.output.table.args = id,update,action +fcli.ssc.issue.update.output.table.args = issueIds,updatesString,action fcli.ssc.issue.get-filter.usage.header = Get issue filter details. fcli.ssc.issue.filter = Technical or friendly filter as returned by the 'fcli ssc issue list-filters' command. fcli.ssc.issue.list-filters.usage.header = List application version issue filters. From 61b676a2aae42aaa106af4f55ad504434e8b3608 Mon Sep 17 00:00:00 2001 From: mjain6 Date: Fri, 13 Feb 2026 11:06:44 +0530 Subject: [PATCH 3/4] Changed Column name for output to 'Issue Id's' --- .../cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java | 10 +--------- .../com/fortify/cli/ssc/i18n/SSCMessages.properties | 3 ++- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java index 10eb553d92..ce19d1f887 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java @@ -34,13 +34,12 @@ import kong.unirest.UnirestInstance; import lombok.Getter; -import lombok.extern.slf4j.Slf4j; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; @Command(name = OutputHelperMixins.Update.CMD_NAME) -@Slf4j +//@Slf4j public class SSCIssueUpdateCommand extends AbstractSSCJsonNodeOutputCommand implements IActionCommandResultSupplier { @Getter @Mixin private OutputHelperMixins.Update outputHelper; @@ -108,7 +107,6 @@ private JsonNode buildResults() { result.put("updatesString", updatesSummary); - // Add customTagUpdates array if custom tags exist if (hasCustomTags()) { ArrayNode customTagsArray = result.putArray("customTagUpdates"); String appVersionId = appVersionResolver.getAppVersionId(unirestInstance); @@ -116,17 +114,14 @@ private JsonNode buildResults() { customTagHelper.populateCustomTagUpdates(customTags, customTagsArray); } - // Add newComment at top level (renamed from comment) if (StringUtils.isNotBlank(comment)) { result.put("newComment", comment); } - // Add assignedUser at top level if (StringUtils.isNotBlank(assignUser)) { result.put("assignedUser", assignUser); } - // Add suppressed at top level if (suppress != null) { result.put("suppressed", suppress); } @@ -175,7 +170,6 @@ private void executeAssignUserRequest(UnirestInstance unirest, String appVersion requestBody.put("user", user); String url = SSCUrls.PROJECT_VERSION_ISSUES_ACTION_ASSIGN_USER(appVersionId); - log.debug("Assign user request: URL={}, user={}", url, user); try { JsonNode response = unirest.post(url) @@ -206,7 +200,6 @@ private void executeAuditRequest(UnirestInstance unirest, String appVersionId, L } String url = SSCUrls.PROJECT_VERSION_ISSUES_ACTION_AUDIT(appVersionId); - log.debug("Audit request: URL={}", url); try { JsonNode response = unirest.post(url) @@ -246,7 +239,6 @@ public boolean isSingular() { private List fetchIssueRevisionsFromSSC(UnirestInstance unirest, String appVersionId, List issueIds) { String idsParam = String.join(",", issueIds); - log.debug("Fetching issues with ids: {}", idsParam); try { JsonNode response = unirest.get("/api/v1/projectVersions/{appVersionId}/issues") diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties index b617d1fc2f..0e2a7da53c 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties @@ -508,7 +508,7 @@ fcli.ssc.issue.update.usage.description = This command allows for updating SSC v for a given application version. You can assign issues to users, perform audit actions with \ comments, custom tag updates, and suppression status. At least one of --custom-tags, --suppress, \ --comment, or --assign-user must be specified along with the required --issue-ids. \ - To see the allowed tags for the specific application version, use: fcli ssc tag get + To see the allowed tags for the specific application version, use: `fcli ssc tag get` fcli.ssc.issue.update.issue-ids = Comma separated list of the vulnerability ids to be updated. fcli.ssc.issue.update.custom-tags = Custom tag to set for the vulnerabilities. Format: tagName=value. \ Can be specified multiple times or as comma-separated list (e.g., tag1=value1,tag2=value2). \ @@ -518,6 +518,7 @@ fcli.ssc.issue.update.suppress = Set the suppression status of the vulnerability fcli.ssc.issue.update.comment = A comment to apply to all the vulnerabilities that are updated. fcli.ssc.issue.update.assign-user = The username or user id of the user to assign the issues to. fcli.ssc.issue.update.output.table.args = issueIds,updatesString,action +fcli.ssc.issue.update.output.table.header.issueIds = Issue Id's fcli.ssc.issue.get-filter.usage.header = Get issue filter details. fcli.ssc.issue.filter = Technical or friendly filter as returned by the 'fcli ssc issue list-filters' command. fcli.ssc.issue.list-filters.usage.header = List application version issue filters. From 3971923141a0fd95f3dc74c1c7b304af86e6970a Mon Sep 17 00:00:00 2001 From: mjain6 Date: Fri, 20 Feb 2026 12:34:03 +0530 Subject: [PATCH 4/4] Added changes for storing unirest --- .../issue/cli/cmd/SSCIssueUpdateCommand.java | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java index ce19d1f887..bcd34ea6c8 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueUpdateCommand.java @@ -55,39 +55,36 @@ public class SSCIssueUpdateCommand extends AbstractSSCJsonNodeOutputCommand impl @Option(names = {"--assign-user"}) private String assignUser; - private UnirestInstance unirestInstance; - @Override public JsonNode getJsonNode(UnirestInstance unirest) { - this.unirestInstance = unirest; validateInput(); String appVersionId = appVersionResolver.getAppVersionId(unirest); List issues = fetchIssueRevisionsFromSSC(unirest, appVersionId, issueIds); if (StringUtils.isNotBlank(assignUser)) { executeAssignUserRequest(unirest, appVersionId, issues, assignUser); - if (isAuditRequired()) { + if (isUpdateRequired()) { issues = fetchIssueRevisionsFromSSC(unirest, appVersionId, issueIds); } } - if (isAuditRequired()) { + if (isUpdateRequired()) { executeAuditRequest(unirest, appVersionId, issues); } - return buildResults(); + return buildResults(unirest); } private void validateInput() { if (issueIds == null || issueIds.isEmpty()) { throw new FcliSimpleException("--issue-ids must be specified"); } - if (!isAuditRequired() && StringUtils.isBlank(assignUser)) { + if (!isUpdateRequired() && StringUtils.isBlank(assignUser)) { throw new FcliSimpleException("At least one of --custom-tags, --suppress, --comment, or --assign-user must be specified"); } } - private boolean isAuditRequired() { + private boolean isUpdateRequired() { return hasCustomTags() || suppress != null || StringUtils.isNotBlank(comment); } @@ -95,7 +92,7 @@ private boolean hasCustomTags() { return customTags != null && !customTags.isEmpty(); } - private JsonNode buildResults() { + private JsonNode buildResults(UnirestInstance unirest) { ObjectNode result = JsonHelper.getObjectMapper().createObjectNode(); String updatesSummary = buildUpdateDetails(); @@ -109,8 +106,8 @@ private JsonNode buildResults() { if (hasCustomTags()) { ArrayNode customTagsArray = result.putArray("customTagUpdates"); - String appVersionId = appVersionResolver.getAppVersionId(unirestInstance); - var customTagHelper = new SSCIssueCustomTagHelper(unirestInstance, appVersionId); + String appVersionId = appVersionResolver.getAppVersionId(unirest); + var customTagHelper = new SSCIssueCustomTagHelper(unirest, appVersionId); customTagHelper.populateCustomTagUpdates(customTags, customTagsArray); } @@ -126,9 +123,7 @@ private JsonNode buildResults() { result.put("suppressed", suppress); } - ArrayNode resultsArray = JsonHelper.getObjectMapper().createArrayNode(); - resultsArray.add(result); - return resultsArray; + return result; } private String buildUpdateDetails() {