diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/mixin/SSCOutputHelperMixins.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/mixin/SSCOutputHelperMixins.java index 0e7785e24f..b95f4e0465 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/mixin/SSCOutputHelperMixins.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/mixin/SSCOutputHelperMixins.java @@ -59,6 +59,15 @@ public static class GetFilterSet extends DetailsNoQuery { public static final String CMD_NAME = "get-filterset"; } + @Command(aliases = {"lsf"}) + public static class ListFilters extends TableWithQuery { + public static final String CMD_NAME = "list-filters"; + } + + public static class GetFilter extends DetailsNoQuery { + public static final String CMD_NAME = "get-filter"; + } + @Command(aliases = {"lsg"}) public static class ListGroups extends TableWithQuery { public static final String CMD_NAME = "list-groups"; 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 edb9bc269c..77f520cac0 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 @@ -21,6 +21,8 @@ subcommands = { SSCIssueFilterSetGetCommand.class, SSCIssueFilterSetListCommand.class, + SSCIssueFilterGetCommand.class, + SSCIssueFiltersListCommand.class, SSCIssueGroupGetCommand.class, SSCIssueGroupListCommand.class, SSCIssueCountCommand.class, diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCountCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCountCommand.java index bcdbd03b2b..4d8439dece 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCountCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCountCommand.java @@ -18,6 +18,7 @@ import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin; import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueFilterSetResolverMixin; import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueGroupResolverMixin; +import com.fortify.cli.ssc.issue.helper.SSCIssueFilterHelper; import com.fortify.cli.ssc.issue.helper.SSCIssueFilterSetDescriptor; import com.fortify.cli.ssc.issue.helper.SSCIssueGroupDescriptor; @@ -27,6 +28,7 @@ import lombok.Getter; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; @Command(name = SSCOutputHelperMixins.VulnCount.CMD_NAME) public class SSCIssueCountCommand extends AbstractSSCBaseRequestOutputCommand { @@ -34,6 +36,7 @@ public class SSCIssueCountCommand extends AbstractSSCBaseRequestOutputCommand { @Mixin private SSCAppVersionResolverMixin.RequiredOption parentResolver; @Mixin private SSCIssueGroupResolverMixin.GroupByOption groupSetResolver; @Mixin private SSCIssueFilterSetResolverMixin.FilterSetOption filterSetResolver; + @Option(names="--filter", required=false) private String filter; // TODO Include options for includeRemoved/Hidden/Suppressed? @@ -49,6 +52,9 @@ public HttpRequest getBaseRequest(UnirestInstance unirest) { if ( filterSetDescriptor!=null ) { request.queryString("filterset", filterSetDescriptor.getGuid()); } + if ( filter!=null ) { + request.queryString("filter", new SSCIssueFilterHelper(unirest, appVersionId).getFilter(filter)); + } return request; } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueFilterGetCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueFilterGetCommand.java new file mode 100644 index 0000000000..33a6f94da5 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueFilterGetCommand.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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.ssc.issue.cli.cmd; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.cli.util.CommandGroup; +import com.fortify.cli.common.cli.util.EnvSuffix; +import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand; +import com.fortify.cli.ssc._common.output.cli.mixin.SSCOutputHelperMixins; +import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin; +import com.fortify.cli.ssc.issue.helper.SSCIssueFilterHelper; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Parameters; + +@Command(name = SSCOutputHelperMixins.GetFilter.CMD_NAME) @CommandGroup("filter") +public class SSCIssueFilterGetCommand extends AbstractSSCJsonNodeOutputCommand { + @Getter @Mixin private SSCOutputHelperMixins.GetFilter outputHelper; + @Mixin private SSCAppVersionResolverMixin.RequiredOption parentResolver; + @EnvSuffix("FILTER") @Parameters(index = "0", arity = "1", descriptionKey = "fcli.ssc.issue.filter") + @Getter private String filter; + + @Override + public JsonNode getJsonNode(UnirestInstance unirest) { + return new SSCIssueFilterHelper(unirest, parentResolver.getAppVersionId(unirest)).getFilterNode(filter); + } + + @Override + public boolean isSingular() { + return true; + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueFiltersListCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueFiltersListCommand.java new file mode 100644 index 0000000000..d166b0e030 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueFiltersListCommand.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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.ssc.issue.cli.cmd; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.cli.util.CommandGroup; +import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand; +import com.fortify.cli.ssc._common.output.cli.mixin.SSCOutputHelperMixins; +import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin; +import com.fortify.cli.ssc.issue.helper.SSCIssueFilterHelper; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = SSCOutputHelperMixins.ListFilters.CMD_NAME) @CommandGroup("filter") +public class SSCIssueFiltersListCommand extends AbstractSSCJsonNodeOutputCommand { + @Getter @Mixin private SSCOutputHelperMixins.ListFilters outputHelper; + @Mixin private SSCAppVersionResolverMixin.RequiredOption parentResolver; + + @Override + public JsonNode getJsonNode(UnirestInstance unirest) { + return new SSCIssueFilterHelper(unirest, parentResolver.getAppVersionId(unirest)).getFilterNodes(); + } + + @Override + public boolean isSingular() { + return false; + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueFilterHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueFilterHelper.java new file mode 100644 index 0000000000..ca4b6ac668 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueFilterHelper.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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.ssc.issue.helper; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +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.json.JsonHelper; +import com.fortify.cli.ssc._common.rest.SSCUrls; + +import kong.unirest.UnirestInstance; +import lombok.Getter; + +public final class SSCIssueFilterHelper { + private final MultiValueMap technicalFiltersByFriendlyFilter = new LinkedMultiValueMap<>(); + private final Map filterNodesByTechnicalFilter = new LinkedHashMap<>(); + @Getter private final ArrayNode filterNodes = JsonHelper.getObjectMapper().createArrayNode(); + + public SSCIssueFilterHelper(UnirestInstance unirest, String applicationVersionId) { + JsonNode data = unirest.get(SSCUrls.PROJECT_VERSION_ISSUE_SELECTOR_SET(applicationVersionId)) + .asObject(JsonNode.class).getBody() + .get("data") + .get("filterBySet"); + data.forEach(this::processEntity); + } + + public final String getFilter(String technicalOrFriendlyFilter) { + if ( filterNodesByTechnicalFilter.containsKey(technicalOrFriendlyFilter) ) { + return technicalOrFriendlyFilter; + } else { + var matchingFriendlyFilters = technicalFiltersByFriendlyFilter.get(technicalOrFriendlyFilter); + if ( matchingFriendlyFilters==null || matchingFriendlyFilters.size()==0 ) { + throw new IllegalArgumentException(technicalOrFriendlyFilter+" is not a supported filter"); + } else if ( matchingFriendlyFilters.size()>1 ) { + throw new IllegalArgumentException(technicalOrFriendlyFilter+" is ambiguous.\n" + + "please use one of the following filters:\n"+ + matchingFriendlyFilters.stream().map(s->s.indent(2)).collect(Collectors.joining("\n"))); + } else { + return matchingFriendlyFilters.get(0); + } + } + } + + public final JsonNode getFilterNode(String technicalOrFriendlyFilter) { + return filterNodesByTechnicalFilter.get(getFilter(technicalOrFriendlyFilter)); + } + + private final void processEntity(JsonNode entity) { + processSelectors(entity, entity.get("selectorOptions")); + } + + private final void processSelectors(JsonNode entity, JsonNode selectors) { + selectors.forEach(selector->processSelector(entity, selector)); + } + + private final void processSelector(JsonNode entity, JsonNode selector) { + var newEntity = (ObjectNode)entity.deepCopy(); + newEntity.remove("selectorOptions"); + newEntity.set("selector", selector); + var filter = JsonHelper.evaluateSpelExpression(newEntity, "entityType+'['+value+']:'+selector.value", String.class); + var friendlyFilter = JsonHelper.evaluateSpelExpression(newEntity, "displayName+':'+selector.displayName?:'NONE'", String.class); + newEntity.put("technicalFilter", filter); + newEntity.put("friendlyFilter", friendlyFilter); + filterNodes.add(newEntity); + filterNodesByTechnicalFilter.put(filter, newEntity); + technicalFiltersByFriendlyFilter.add(friendlyFilter, filter); + } +} 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 17edee67de..998b4f641f 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 @@ -272,8 +272,12 @@ fcli.ssc.event.list.usage.header = List system events. # fcli ssc issue fcli.ssc.issue.usage.header = Manage SSC issues (vulnerabilities) and related entities like filter sets and issue groups. fcli.ssc.issue.count.usage.header = Count application version vulnerabilities by grouping. -fcli.ssc.issue.count.by = Vulnerability grouping type. See 'fcli ssc issue-group list' for \ +fcli.ssc.issue.count.by = Vulnerability grouping type. See 'fcli ssc issue list-groups' for \ allowed values. Default value: ${DEFAULT-VALUE}. +fcli.ssc.issue.count.filter = Filter issue counts using the given (friendly or technical) filter. \ + See 'fcli ssc issue list-filters' for allowed values. +fcli.ssc.issue.get-filter.usage.header = Get issue filter details. +fcli.ssc.issue.list-filters.usage.header = List application version issue filters. fcli.ssc.issue.get-filterset.usage.header = Get filter set details. fcli.ssc.issue.list-filtersets.usage.header = List application version filter sets. fcli.ssc.issue.filterset.resolver.titleOrId = Filter set title (name) or id. @@ -432,6 +436,7 @@ fcli.ssc.event.output.table.options = eventDate,userName,eventType,detailedNote, fcli.ssc.issue.count.output.table.options = cleanName,totalCount,auditedCount fcli.ssc.issue.filter-set.output.table.options = guid,title,defaultFilterSet,description fcli.ssc.issue.group.output.table.options = guid,displayName,entityType,description +fcli.ssc.issue.filter.output.table.options = entityType,friendlyFilter,technicalFilter fcli.ssc.issue-template.output.table.options = id,name,inUse,defaultTemplate,publishVersion,originalFileName,description fcli.ssc.job.output.table.options = jobName,jobGroup,jobClass,state,cancellable,priority,createTime,startTime,finishTime fcli.ssc.performance-indicator.output.table.options = id,name,timestamp,valueString