diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCustomTagUpdater.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCustomTagUpdater.java index ce9e828d111..ce1d696748a 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCustomTagUpdater.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCustomTagUpdater.java @@ -22,8 +22,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; +import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagAssignmentHelper; import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagDescriptor; -import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagUpdateHelper; import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; @@ -42,7 +42,7 @@ public HttpRequest buildRequest(String appVersionId, List addCustomTa return null; } ArrayNode current = getInitialTags(appVersionId); - SSCCustomTagUpdateHelper tagUpdateHelper = new SSCCustomTagUpdateHelper(unirest); + SSCCustomTagAssignmentHelper tagUpdateHelper = new SSCCustomTagAssignmentHelper(unirest); List currentDescriptors = JsonHelper.stream(current) .map(tag -> JsonHelper.treeToValue(tag, SSCCustomTagDescriptor.class)) .collect(Collectors.toList()); diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/cmd/SSCCustomTagCreateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/cmd/SSCCustomTagCreateCommand.java index 9c3ae812fa3..4c0f1b2a9a3 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/cmd/SSCCustomTagCreateCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/cmd/SSCCustomTagCreateCommand.java @@ -13,13 +13,11 @@ package com.fortify.cli.ssc.custom_tag.cli.cmd; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fortify.cli.common.exception.FcliSimpleException; 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.custom_tag.helper.SSCCustomTagDefinitionHelper; import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagValueType; import kong.unirest.UnirestInstance; @@ -51,7 +49,8 @@ public class SSCCustomTagCreateCommand extends AbstractSSCJsonNodeOutputCommand @Override public JsonNode getJsonNode(UnirestInstance unirest) { - ObjectNode body = buildBody(); + ObjectNode body = new SSCCustomTagDefinitionHelper(unirest).buildCreateBody( + name, valueType, description, restriction, hidden, requiresComment, extensible, values); var response = unirest.post("/api/v1/customTags") .body(body) .asObject(JsonNode.class).getBody(); @@ -67,42 +66,4 @@ public String getActionCommandResult() { public boolean isSingular() { return true; } - - // --- Private body-building helpers below --- - private ObjectNode buildBody() { - ObjectNode body = JsonNodeFactory.instance.objectNode(); - body.put("name", name); - body.put("description", description != null ? description : ""); - body.put("valueType", valueType.name()); - body.put("restriction", restriction); - body.put("hidden", hidden); - body.put("requiresComment", requiresComment); - body.put("customTagType", "CUSTOM"); - if (valueType == SSCCustomTagValueType.LIST) { - body.put("extensible", extensible); - body.set("valueList", buildValueList()); - } - return body; - } - - private ArrayNode buildValueList() { - if (values == null || values.isBlank()) { - throw new FcliSimpleException("At least one value must be specified for LIST type using --values"); - } - var valueList = JsonNodeFactory.instance.arrayNode(); - String[] vals = values.split(","); - for (int i = 0; i < vals.length; i++) { - String val = vals[i].trim(); - ObjectNode entry = JsonNodeFactory.instance.objectNode(); - entry.put("lookupIndex", i+1); - entry.put("deletable", true); - entry.put("lookupValue", val); - entry.put("description", ""); - entry.putNull("auditAssistantTrainingLabel"); - entry.put("hidden", false); - entry.put("seqNumber", i+1); - valueList.add(entry); - } - return valueList; - } } \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/cmd/SSCCustomTagUpdateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/cmd/SSCCustomTagUpdateCommand.java index 282ab96987b..a5f9cd47b47 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/cmd/SSCCustomTagUpdateCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/cmd/SSCCustomTagUpdateCommand.java @@ -12,19 +12,14 @@ */ package com.fortify.cli.ssc.custom_tag.cli.cmd; -import java.util.LinkedHashMap; - import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fortify.cli.common.exception.FcliSimpleException; 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.custom_tag.cli.mixin.SSCCustomTagResolverMixin; +import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagDefinitionHelper; import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagDescriptor; -import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagHelper; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -57,12 +52,14 @@ public class SSCCustomTagUpdateCommand extends AbstractSSCJsonNodeOutputCommand @Override public JsonNode getJsonNode(UnirestInstance unirest) { + SSCCustomTagDefinitionHelper customTagDefinitionHelper = new SSCCustomTagDefinitionHelper(unirest); SSCCustomTagDescriptor desc = customTagResolver.getCustomTagDescriptor(unirest); - ObjectNode updateData = buildBody(desc); + ObjectNode updateData = customTagDefinitionHelper.buildUpdateBody( + desc, name, description, restriction, hidden, requiresComment, extensible, values, addValues, rmValues); unirest.put("/api/v1/customTags/{id}") .routeParam("id", desc.getId()) .body(updateData).asObject(JsonNode.class).getBody(); - return new SSCCustomTagHelper(unirest).getDescriptorByCustomTagSpec(desc.getGuid(), true).asJsonNode(); + return customTagDefinitionHelper.getDescriptorByCustomTagSpec(desc.getGuid(), true).asJsonNode(); } @Override @@ -74,78 +71,4 @@ public String getActionCommandResult() { public boolean isSingular() { return true; } - - // --- Private body-building helpers below --- - private ObjectNode buildBody(SSCCustomTagDescriptor desc) { - ObjectNode body = (ObjectNode)desc.asJsonNode().deepCopy(); - if (name != null && !name.isBlank()) { body.put("name", name); } - if (description != null) { body.put("description", description); } - if (restriction != null) { body.put("restriction", restriction); } - if (hidden != null) { body.put("hidden", hidden); } - if (requiresComment != null) { body.put("requiresComment", requiresComment); } - if (extensible != null && "LIST".equalsIgnoreCase(body.path("valueType").asText())) { body.put("extensible", extensible); } - if ("LIST".equalsIgnoreCase(body.path("valueType").asText())) { - body.set("valueList", buildValueList(body)); - } - return body; - } - - private ArrayNode buildValueList(ObjectNode body) { - LinkedHashMap valueMap = buildValueMap(body); - if (values != null) { - valueMap.clear(); - addValuesToMap(valueMap, values); - } - if (addValues != null) { - addValuesToMap(valueMap, addValues); - } - if (rmValues != null) { - removeValuesFromMap(valueMap, rmValues); - } - if (valueMap.isEmpty()) { - throw new FcliSimpleException("At least one value must be specified for LIST type"); - } - var newValueList = JsonNodeFactory.instance.arrayNode(); - int idx = 1; - for (ObjectNode entry : valueMap.values()) { - entry.put("lookupIndex", idx); - entry.put("seqNumber", idx); - newValueList.add(entry); - idx++; - } - return newValueList; - } - - private LinkedHashMap buildValueMap(ObjectNode body) { - var valueList = body.withArray("valueList"); - LinkedHashMap valueMap = new LinkedHashMap<>(); - for (JsonNode v : valueList) { - valueMap.put(v.path("lookupValue").asText(), (ObjectNode)v); - } - return valueMap; - } - - private void addValuesToMap(LinkedHashMap valueMap, String valuesStr) { - String[] vals = valuesStr.split(","); - for (String val : vals) { - val = val.trim(); - if (!valueMap.containsKey(val)) { - ObjectNode entry = JsonNodeFactory.instance.objectNode(); - entry.put("lookupValue", val); - entry.put("deletable", true); - entry.put("description", ""); - entry.putNull("auditAssistantTrainingLabel"); - entry.put("hidden", false); - valueMap.put(val, entry); - } - } - } - - private void removeValuesFromMap(LinkedHashMap valueMap, String valuesStr) { - String[] vals = valuesStr.split(","); - for (String val : vals) { - val = val.trim(); - valueMap.remove(val); - } - } } \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/mixin/SSCCustomTagResolverMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/mixin/SSCCustomTagResolverMixin.java index b98c1e3d488..9d028836461 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/mixin/SSCCustomTagResolverMixin.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/cli/mixin/SSCCustomTagResolverMixin.java @@ -15,8 +15,8 @@ import org.apache.commons.lang3.StringUtils; import com.fortify.cli.common.cli.util.EnvSuffix; +import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagDefinitionHelper; import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagDescriptor; -import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagHelper; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -30,7 +30,7 @@ public SSCCustomTagDescriptor getCustomTagDescriptor(UnirestInstance unirest) { String customTagNameOrGuid = getCustomTagNameOrGuid(); return StringUtils.isBlank(customTagNameOrGuid) ? null - : new SSCCustomTagHelper(unirest).getDescriptorByCustomTagSpec(customTagNameOrGuid, true); + : new SSCCustomTagDefinitionHelper(unirest).getDescriptorByCustomTagSpec(customTagNameOrGuid, true); } } public static class OptionalOption extends AbstractSSCCustomTagResolverMixin { diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagUpdateHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagAssignmentHelper.java similarity index 62% rename from fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagUpdateHelper.java rename to fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagAssignmentHelper.java index 34f3a6592a7..cdaaaed0b5f 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagUpdateHelper.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagAssignmentHelper.java @@ -21,39 +21,30 @@ import kong.unirest.UnirestInstance; -public class SSCCustomTagUpdateHelper { - private final SSCCustomTagHelper tagHelper; +public class SSCCustomTagAssignmentHelper { + private final SSCCustomTagDefinitionHelper tagDefinitionHelper; - public SSCCustomTagUpdateHelper(UnirestInstance unirest) { - this.tagHelper = new SSCCustomTagHelper(unirest); + public SSCCustomTagAssignmentHelper(UnirestInstance unirest) { + this.tagDefinitionHelper = new SSCCustomTagDefinitionHelper(unirest); } - /** - * Resolves tag specs (name, guid, id) to descriptors using SSCCustomTagHelper. - */ public Set resolveTagSpecs(List tagSpecs) { - return tagHelper.getDescriptorsByCustomTagSpec(tagSpecs, false).collect(Collectors.toSet()); + return tagDefinitionHelper.getDescriptorsByCustomTagSpec(tagSpecs, false).collect(Collectors.toSet()); } - /** - * Computes the updated stream of custom tag descriptors given current, add, and remove specs. - */ public Stream computeUpdatedTagDescriptors(List currentTags, List addSpecs, List rmSpecs) { var currentTagsStream = currentTags.stream(); - var addDescriptorsStream = tagHelper.getDescriptorsByCustomTagSpec(addSpecs, false); - var rmDescriptors = tagHelper.getDescriptorsByCustomTagSpec(rmSpecs, false).toList(); + var addDescriptorsStream = tagDefinitionHelper.getDescriptorsByCustomTagSpec(addSpecs, false); + var rmDescriptors = tagDefinitionHelper.getDescriptorsByCustomTagSpec(rmSpecs, false).toList(); return Stream.concat( currentTagsStream.filter(tag -> rmDescriptors.stream().noneMatch(rmTag -> rmTag.isEqualById(tag))), addDescriptorsStream ).distinct(); } - /** - * Overload: Accepts current custom tags as json nodes, resolves to descriptors, then computes updated descriptors. - */ public Stream computeUpdatedTagDescriptors(JsonNode currentTagsNode, List addSpecs, List rmSpecs) { return computeUpdatedTagDescriptors( - SSCCustomTagHelper.toDescriptors(currentTagsNode), + SSCCustomTagDefinitionHelper.toDescriptors(currentTagsNode), addSpecs, rmSpecs); } } \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagDefinitionHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagDefinitionHelper.java new file mode 100644 index 00000000000..75662446b85 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagDefinitionHelper.java @@ -0,0 +1,255 @@ +/* + * 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.custom_tag.helper; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.exception.FcliSimpleException; +import com.fortify.cli.common.exception.FcliTechnicalException; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; + +import kong.unirest.UnirestInstance; +import lombok.Getter; + +public class SSCCustomTagDefinitionHelper { + private final UnirestInstance unirest; + @Getter(lazy = true) private final List descriptors = loadDescriptors(); + + public SSCCustomTagDefinitionHelper(UnirestInstance unirest) { + this.unirest = unirest; + } + + public static final List toDescriptors(JsonNode tagsNode) { + if ( tagsNode!=null && tagsNode instanceof ObjectNode ) { + tagsNode = tagsNode.get("data"); + } + if ( tagsNode==null || !(tagsNode instanceof ArrayNode)) { + throw new FcliTechnicalException("Invalid custom tags data: "+tagsNode); + } + return JsonHelper.stream((ArrayNode)tagsNode) + .map(tag -> JsonHelper.treeToValue(tag, SSCCustomTagDescriptor.class)) + .toList(); + } + + private List loadDescriptors() { + return toDescriptors( + unirest.get(SSCUrls.CUSTOM_TAGS).queryString("limit", "-1").asObject(JsonNode.class).getBody() + ); + } + + public SSCCustomTagDescriptor getDescriptorByCustomTagSpec(String customTagSpec, boolean failIfNotFound) { + if (customTagSpec == null || customTagSpec.isBlank()) { + if (failIfNotFound) { + throw new FcliSimpleException("Custom tag not found: null or empty"); + } + return null; + } + return getDescriptors().stream() + .filter(desc -> customTagSpec.equalsIgnoreCase(desc.getGuid()) + || customTagSpec.equalsIgnoreCase(desc.getName()) + || customTagSpec.equalsIgnoreCase(desc.getId())) + .findFirst() + .orElseGet(() -> { + if (failIfNotFound) { + throw new FcliSimpleException("Custom tag not found: " + customTagSpec); + } + return null; + }); + } + + public Stream getDescriptorsByCustomTagSpec(List customTagSpecs, boolean failIfNotFound) { + if (customTagSpecs == null || customTagSpecs.isEmpty()) { + return Stream.empty(); + } + return customTagSpecs.stream() + .map(spec -> getDescriptorByCustomTagSpec(spec, failIfNotFound)) + .filter(Objects::nonNull) + .distinct(); + } + + public ObjectNode buildCreateBody(String name, SSCCustomTagValueType valueType, String description, + boolean restriction, boolean hidden, boolean requiresComment, boolean extensible, String values) { + ObjectNode body = JsonNodeFactory.instance.objectNode(); + body.put("name", name); + body.put("description", description != null ? description : ""); + body.put("valueType", valueType.name()); + body.put("restriction", restriction); + body.put("hidden", hidden); + body.put("requiresComment", requiresComment); + body.put("customTagType", "CUSTOM"); + if (valueType == SSCCustomTagValueType.LIST) { + body.put("extensible", extensible); + body.set("valueList", buildCreateValueList(values)); + } + return body; + } + + public ObjectNode buildUpdateBody(SSCCustomTagDescriptor desc, String name, String description, + Boolean restriction, Boolean hidden, Boolean requiresComment, Boolean extensible, + String values, String addValues, String rmValues) { + ObjectNode body = (ObjectNode)desc.asJsonNode().deepCopy(); + if (name != null && !name.isBlank()) { body.put("name", name); } + if (description != null) { body.put("description", description); } + if (restriction != null) { body.put("restriction", restriction); } + if (hidden != null) { body.put("hidden", hidden); } + if (requiresComment != null) { body.put("requiresComment", requiresComment); } + + String valueType = body.path("valueType").asText(); + boolean hasValueModifications = values != null || addValues != null || rmValues != null; + + if (hasValueModifications && !"LIST".equalsIgnoreCase(valueType)) { + throw new FcliSimpleException("Cannot modify values for custom tag '" + desc.getName() + + "': value operations (--values, --add-values, --rm-values) only apply to LIST type tags, not " + valueType); + } + + if ("LIST".equalsIgnoreCase(valueType)) { + if (extensible != null) { + body.put("extensible", extensible); + } + body.set("valueList", buildUpdatedValueList(body, values, addValues, rmValues)); + } + return body; + } + + public int addValueToListTag(String tagGuid, String newValue) { + SSCCustomTagDescriptor desc = getDescriptorByCustomTagSpec(tagGuid, true); + ObjectNode body = (ObjectNode) desc.asJsonNode().deepCopy(); + LinkedHashMap valueMap = buildValueMap(body); + if (!valueMap.containsKey(newValue)) { + valueMap.put(newValue, newValueListEntry(newValue)); + } + int newLookupIndex = -1; + ArrayNode newValueList = JsonNodeFactory.instance.arrayNode(); + int idx = 1; + for (Map.Entry e : valueMap.entrySet()) { + e.getValue().put("lookupIndex", idx); + e.getValue().put("seqNumber", idx); + newValueList.add(e.getValue()); + if (e.getKey().equalsIgnoreCase(newValue)) { + newLookupIndex = idx; + } + idx++; + } + body.set("valueList", newValueList); + unirest.put(SSCUrls.CUSTOM_TAG(desc.getId())) + .body(body) + .asObject(JsonNode.class) + .getBody(); + return newLookupIndex; + } + + private ArrayNode buildCreateValueList(String values) { + if (values == null || values.isBlank()) { + throw new FcliSimpleException("At least one value must be specified for LIST type using --values"); + } + var valueList = JsonNodeFactory.instance.arrayNode(); + String[] vals = values.split(","); + int idx = 1; + for (int i = 0; i < vals.length; i++) { + String val = vals[i].trim(); + if (val.isBlank()) { + continue; + } + ObjectNode entry = newValueListEntry(val); + entry.put("lookupIndex", idx); + entry.put("seqNumber", idx); + valueList.add(entry); + idx++; + } + if (valueList.isEmpty()) { + throw new FcliSimpleException("At least one non-blank value must be specified for LIST type using --values"); + } + return valueList; + } + + private ArrayNode buildUpdatedValueList(ObjectNode body, String values, String addValues, String rmValues) { + LinkedHashMap valueMap = buildValueMap(body); + if (values != null) { + valueMap.clear(); + addValuesToMap(valueMap, values); + } + if (addValues != null) { + addValuesToMap(valueMap, addValues); + } + if (rmValues != null) { + removeValuesFromMap(valueMap, rmValues); + } + if (valueMap.isEmpty()) { + throw new FcliSimpleException("At least one value must remain after update; cannot remove all LIST values"); + } + var newValueList = JsonNodeFactory.instance.arrayNode(); + int idx = 1; + for (ObjectNode entry : valueMap.values()) { + entry.put("lookupIndex", idx); + entry.put("seqNumber", idx); + newValueList.add(entry); + idx++; + } + return newValueList; + } + + private LinkedHashMap buildValueMap(ObjectNode body) { + var valueList = body.withArray("valueList"); + LinkedHashMap valueMap = new LinkedHashMap<>(); + for (JsonNode v : valueList) { + String key = v.path("lookupValue").asText().toLowerCase(Locale.ROOT); + valueMap.put(key, (ObjectNode)v); + } + return valueMap; + } + + private void addValuesToMap(LinkedHashMap valueMap, String valuesStr) { + String[] vals = valuesStr.split(","); + for (String val : vals) { + val = val.trim(); + if (val.isBlank()) { + continue; + } + String key = val.toLowerCase(Locale.ROOT); + if (!valueMap.containsKey(key)) { + valueMap.put(key, newValueListEntry(val)); + } + } + } + + private void removeValuesFromMap(LinkedHashMap valueMap, String valuesStr) { + String[] vals = valuesStr.split(","); + for (String val : vals) { + val = val.trim(); + if (val.isBlank()) { + continue; + } + valueMap.remove(val.toLowerCase(Locale.ROOT)); + } + } + + private ObjectNode newValueListEntry(String value) { + ObjectNode entry = JsonNodeFactory.instance.objectNode(); + entry.put("lookupValue", value); + entry.put("deletable", true); + entry.put("description", ""); + entry.putNull("auditAssistantTrainingLabel"); + entry.put("hidden", false); + return entry; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagHelper.java deleted file mode 100644 index 89cffb10ca4..00000000000 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/custom_tag/helper/SSCCustomTagHelper.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.custom_tag.helper; - -import java.util.List; -import java.util.Objects; -import java.util.stream.Stream; - -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.exception.FcliTechnicalException; -import com.fortify.cli.common.json.JsonHelper; -import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; - -import kong.unirest.UnirestInstance; -import lombok.Getter; - -public class SSCCustomTagHelper { - private final UnirestInstance unirest; - @Getter(lazy = true) private final List descriptors = loadDescriptors(); - - public SSCCustomTagHelper(UnirestInstance unirest) { - this.unirest = unirest; - } - - public static final List toDescriptors(JsonNode tagsNode) { - if ( tagsNode!=null && tagsNode instanceof ObjectNode ) { - tagsNode = tagsNode.get("data"); - } - if ( tagsNode==null || !(tagsNode instanceof ArrayNode)) { - throw new FcliTechnicalException("Invalid custom tags data: "+tagsNode); - } - return JsonHelper.stream((ArrayNode)tagsNode) - .map(tag -> JsonHelper.treeToValue(tag, SSCCustomTagDescriptor.class)) - .toList(); - } - - private List loadDescriptors() { - return toDescriptors( - unirest.get(SSCUrls.CUSTOM_TAGS).queryString("limit", "-1").asObject(JsonNode.class).getBody() - ); - } - - public SSCCustomTagDescriptor getDescriptorByCustomTagSpec(String customTagSpec, boolean failIfNotFound) { - if (customTagSpec == null || customTagSpec.isEmpty()) { - if (failIfNotFound) { - throw new FcliSimpleException("Custom tag not found: null or empty"); - } - return null; - } - return getDescriptors().stream() - .filter(desc -> customTagSpec.equalsIgnoreCase(desc.getGuid()) - || customTagSpec.equalsIgnoreCase(desc.getName()) - || customTagSpec.equalsIgnoreCase(desc.getId())) - .findFirst() - .orElseGet(() -> { - if (failIfNotFound) { - throw new FcliSimpleException("Custom tag not found: " + customTagSpec); - } - return null; - }); - } - - public Stream getDescriptorsByCustomTagSpec(List customTagSpecs, boolean failIfNotFound) { - if (customTagSpecs == null || customTagSpecs.isEmpty()) { - return Stream.empty(); - } - return customTagSpecs.stream() - .map(spec -> getDescriptorByCustomTagSpec(spec, failIfNotFound)) - .filter(Objects::nonNull) - .distinct(); - } -} \ No newline at end of file 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 cdbd7ddada2..38944cedd92 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 @@ -56,6 +56,8 @@ public class SSCIssueUpdateCommand extends AbstractSSCJsonNodeOutputCommand impl private String comment; @Option(names = {"--assign-user"}) private String assignUser; + @Option(names = {"--extend"}, defaultValue = "false") + private boolean extend; @Override public JsonNode getJsonNode(UnirestInstance unirest) { @@ -192,7 +194,7 @@ private void executeAuditRequest(UnirestInstance unirest, String appVersionId, L } if (hasCustomTags()) { var customTagHelper = new SSCIssueCustomTagHelper(unirest, appVersionId); - List processedTags = customTagHelper.processCustomTags(customTags); + List processedTags = customTagHelper.processCustomTags(customTags, extend); request.put("customTagAudit", processedTags); } 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 b9e478d75cd..d8e48554a64 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 @@ -27,6 +27,7 @@ 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.SSCCustomTagDefinitionHelper; import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagValueType; import kong.unirest.UnirestInstance; @@ -42,7 +43,7 @@ public class SSCIssueCustomTagHelper { @Getter(lazy = true) private final Map customTagInfoMap = loadCustomTagInfo(); - public List processCustomTags(Map customTags) { + public List processCustomTags(Map customTags, boolean extend) { if (customTags == null || customTags.isEmpty()) { return List.of(); } @@ -57,7 +58,7 @@ public List processCustomTags(Map cu if (tagInfo == null) { throw new FcliSimpleException("Custom tag '" + tagName + "' is not available for this application version"); } - return createAuditValue(tagName, tagValue, tagInfo); + return createAuditValue(tagName, tagValue, tagInfo, extend); }) .collect(Collectors.toList()); } @@ -107,7 +108,7 @@ private String getValueGuidForTag(String value, CustomTagInfo tagInfo) { return null; } - private SSCIssueCustomTagAuditValue createAuditValue(String tagName, String value, CustomTagInfo tagInfo) { + private SSCIssueCustomTagAuditValue createAuditValue(String tagName, String value, CustomTagInfo tagInfo, boolean extend) { String guid = tagInfo.getGuid(); boolean isUnset = value == null || value.isBlank(); switch (tagInfo.getValueType()) { @@ -128,7 +129,7 @@ private SSCIssueCustomTagAuditValue createAuditValue(String tagName, String valu return SSCIssueCustomTagAuditValue.forDate(guid, dateValue); case LIST: if (isUnset) return SSCIssueCustomTagAuditValue.forList(guid, -1); - Integer lookupIndex = getListValueIndex(value, tagName, tagInfo); + Integer lookupIndex = getListValueIndex(value, tagName, tagInfo, extend); return SSCIssueCustomTagAuditValue.forList(guid, lookupIndex); default: throw new FcliSimpleException("Unsupported custom tag value type: " + tagInfo.getValueType()); @@ -144,23 +145,38 @@ private String processDateValue(String value, String tagName) { } } - 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(); + private Integer getListValueIndex(String value, String tagName, CustomTagInfo tagInfo, boolean extend) { + var valueList = tagInfo.getValueList(); + if (valueList != null) { + for (ValueListItem item : valueList) { + if (value.equalsIgnoreCase(item.getLookupValue())) { + return item.getLookupIndex(); + } } } - - String validValues = tagInfo.getValueList().stream() + if (tagInfo.isExtensible() && extend) { + return extendTagWithValue(tagInfo, value); + } + String hint = tagInfo.isExtensible() + ? " Use --extend to add new values." + : " This tag is not extensible."; + if (valueList == null || valueList.isEmpty()) { + throw new FcliSimpleException("Custom tag '" + tagName + "' has no valid list values configured." + hint); + } + String validValues = valueList.stream() .map(ValueListItem::getLookupValue) .collect(Collectors.joining(", ")); - - throw new FcliSimpleException("Invalid value '" + value + "' for list custom tag '" + tagName + "'. " + - "Valid values are: " + validValues); + throw new FcliSimpleException("Invalid value '" + value + "' for custom tag '" + tagName + "'." + + " Supported values: " + validValues + "." + hint); + } + + private int extendTagWithValue(CustomTagInfo tagInfo, String newValue) { + int newIndex = new SSCCustomTagDefinitionHelper(unirest).addValueToListTag(tagInfo.getGuid(), newValue); + ValueListItem newItem = new ValueListItem(); + newItem.setLookupIndex(newIndex); + newItem.setLookupValue(newValue); + tagInfo.getValueList().add(newItem); + return newIndex; } private Map loadCustomTagInfo() { @@ -195,7 +211,7 @@ private CustomTagInfo parseCustomTagInfo(JsonNode tagNode) { tagInfo.setGuid(tagNode.get("guid").asText()); tagInfo.setName(tagNode.get("name").asText()); tagInfo.setValueType(SSCCustomTagValueType.valueOf(tagNode.get("valueType").asText())); - + tagInfo.setExtensible(tagNode.path("extensible").asBoolean(false)); JsonNode valueListNode = tagNode.get("valueList"); if (valueListNode != null && valueListNode.isArray()) { for (JsonNode valueNode : valueListNode) { @@ -214,6 +230,7 @@ public static class CustomTagInfo { private String guid; private String name; private SSCCustomTagValueType valueType; + private boolean extensible; private List valueList = new ArrayList<>(); } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue_template/cli/cmd/AbstractSSCIssueTemplateUpdateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue_template/cli/cmd/AbstractSSCIssueTemplateUpdateCommand.java index 140e81c6804..1ec99fa9144 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue_template/cli/cmd/AbstractSSCIssueTemplateUpdateCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue_template/cli/cmd/AbstractSSCIssueTemplateUpdateCommand.java @@ -23,7 +23,7 @@ import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand; import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; import com.fortify.cli.ssc.custom_tag.cli.mixin.SSCCustomTagAddRemoveMixin; -import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagUpdateHelper; +import com.fortify.cli.ssc.custom_tag.helper.SSCCustomTagAssignmentHelper; import com.fortify.cli.ssc.issue_template.cli.mixin.SSCIssueTemplateResolverMixin; import com.fortify.cli.ssc.issue_template.helper.SSCIssueTemplateDescriptor; import com.fortify.cli.ssc.issue_template.helper.SSCIssueTemplateHelper; @@ -57,7 +57,7 @@ public JsonNode getJsonNode(UnirestInstance unirest) { } protected ArrayNode getCustomTagIds(UnirestInstance unirest, SSCIssueTemplateDescriptor descriptor) { - SSCCustomTagUpdateHelper tagUpdateHelper = new SSCCustomTagUpdateHelper(unirest); + SSCCustomTagAssignmentHelper tagUpdateHelper = new SSCCustomTagAssignmentHelper(unirest); var currentTags = SSCIssueTemplateHelper.getCustomTagsRequest(unirest, descriptor.getId()).asObject(JsonNode.class).getBody(); return tagUpdateHelper.computeUpdatedTagDescriptors( currentTags, 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 3b69d2212ae..93be4622ab8 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 @@ -532,6 +532,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.extend = For LIST tags, adds the value if it doesn’t exist (requires extensible tag). 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.