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 3fa8be83e6..cda827f087 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, + FoDIssueGetCommand.class, FoDIssueUpdateCommand.class, } ) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueGetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueGetCommand.java new file mode 100644 index 0000000000..bf868cc6d6 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueGetCommand.java @@ -0,0 +1,89 @@ +/* + * 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.fod.issue.cli.cmd; + +import com.fasterxml.jackson.databind.JsonNode; +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.producer.IObjectNodeProducer; +import com.fortify.cli.common.json.producer.ObjectNodeProducerApplyFrom; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDOutputCommand; +import com.fortify.cli.fod._common.rest.FoDUrls; +import com.fortify.cli.fod._common.rest.helper.FoDInputTransformer; +import com.fortify.cli.fod.issue.cli.mixin.FoDIssueEmbedMixin; +import com.fortify.cli.fod.issue.cli.mixin.FoDIssueIncludeMixin; +import com.fortify.cli.fod.issue.helper.FoDIssueHelper; +import com.fortify.cli.fod.issue.helper.FoDIssueHelper.IssueAggregationData; +import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; +import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; + +import kong.unirest.HttpRequest; +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Parameters; + +@Command(name = OutputHelperMixins.Get.CMD_NAME) +public class FoDIssueGetCommand extends AbstractFoDOutputCommand { + @Getter @Mixin private OutputHelperMixins.Get outputHelper; + @Mixin private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins + @Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver; + @Parameters(index = "0", arity = "1", descriptionKey = "fcli.fod.issue.get.vulnId") + private String vulnId; + @Mixin private FoDIssueEmbedMixin embedMixin; + @Mixin private FoDIssueIncludeMixin includeMixin; + + @Override + protected IObjectNodeProducer getObjectNodeProducer(UnirestInstance unirest) { + FoDReleaseDescriptor releaseDescriptor = releaseResolver.getReleaseDescriptor(unirest); + String releaseId = releaseDescriptor.getReleaseId().toString(); + JsonNode issue = findIssue(unirest, releaseId); + if ( issue==null ) { + throw new FcliSimpleException(String.format("No vulnerability found for vulnId '%s' in release '%s'", vulnId, releaseDescriptor.getReleaseName())); + } + if ( issue instanceof ObjectNode issueObject ) { + issueObject.put("releaseId", releaseId); + issueObject.put("releaseName", releaseDescriptor.getReleaseName()); + FoDIssueHelper.transformRecord(issueObject, IssueAggregationData.forSingleRelease(issueObject)); + } + return simpleObjectNodeProducerBuilder(ObjectNodeProducerApplyFrom.SPEC) + .source(issue) + .build(); + } + + private JsonNode findIssue(UnirestInstance unirest, String releaseId) { + HttpRequest request = unirest.get(FoDUrls.VULNERABILITIES) + .routeParam("relId", releaseId) + .queryString("filters", "vulnId:" + vulnId) + .queryString("limit", "2"); + var response = includeMixin.updateRequest(request).asObject(JsonNode.class); + if ( response.getStatus() >= 400 ) { + throw new FcliTechnicalException(String.format("FoD API returned HTTP %d while searching for vulnerability '%s'", response.getStatus(), vulnId)); + } + JsonNode items = FoDInputTransformer.getItems(response.getBody()); + if ( items==null || !items.isArray() ) { return null; } + if ( items.size()>1 ) { + throw new FcliSimpleException(String.format("Multiple vulnerabilities found for vulnId '%s'; please check your input", vulnId)); + } + return items.isEmpty() ? null : items.get(0); + } + + @Override + public boolean isSingular() { + return true; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java index e6e4a906f8..9749832dcf 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/helper/FoDIssueHelper.java @@ -152,7 +152,7 @@ public static IssueAggregationData forSingleRelease(ObjectNode issue) { .releaseNames(Set.of(releaseName)) .releaseIds(Set.of(releaseId)) .ids(Set.of(id)) - .vulnIds(Set.of(vulnId)) + .vulnIds(vulnId!=null ? Set.of(vulnId) : Collections.emptySet()) .build(); } @@ -179,7 +179,7 @@ public String getReleaseIdsString() { } public String getIdsString() { - return asString(ids); + return asString(ids); } private String asString(Set values) { @@ -192,25 +192,29 @@ private String asString(Set values) { /** Overload adding aggregation fields to an ObjectNode using provided data. */ public static final ObjectNode transformRecord(ObjectNode record, IssueAggregationData data) { transformRecord(record); // apply generic transformations first (rename etc.) - ArrayNode vulnIdsArray = JsonHelper.getObjectMapper().createArrayNode(); - data.getVulnIds().forEach(vulnIdsArray::add); - ArrayNode releaseNamesArray = JsonHelper.getObjectMapper().createArrayNode(); - data.getReleaseNames().forEach(releaseNamesArray::add); - ArrayNode releaseIdsArray = JsonHelper.getObjectMapper().createArrayNode(); - data.getReleaseIds().forEach(releaseIdsArray::add); - ArrayNode idsArray = JsonHelper.getObjectMapper().createArrayNode(); - data.getIds().forEach(idsArray::add); - record.set("vulnIds", vulnIdsArray); + record.set("vulnIds", toJsonNode(data.getVulnIds())); record.put("vulnIdsString", data.getVulnIdsString()); - record.set("foundInReleases", releaseNamesArray); + record.set("foundInReleases", toJsonNode(data.getReleaseNames())); record.put("foundInReleasesString", data.getReleaseNamesString()); - record.set("foundInReleaseIds", releaseIdsArray); + record.set("foundInReleaseIds", toJsonNode(data.getReleaseIds())); record.put("foundInReleaseIdsString", data.getReleaseIdsString()); - record.set("ids", idsArray); + record.set("ids", toJsonNode(data.getIds())); record.put("idsString", data.getIdsString()); return record; } + private static JsonNode toJsonNode(Set values) { + if ( values == null || values.isEmpty() ) { + return JsonHelper.getObjectMapper().getNodeFactory().textNode("N/A"); + } else if ( values.size() == 1 ) { + return JsonHelper.getObjectMapper().getNodeFactory().textNode(values.iterator().next()); + } else { + var array = JsonHelper.getObjectMapper().createArrayNode(); + values.forEach(array::add); + return array; + } + } + public static final FoDBulkIssueUpdateResponse updateIssues(UnirestInstance unirest, String releaseId, FoDBulkIssueUpdateRequest issueUpdateRequest) { ObjectNode body = objectMapper.valueToTree(issueUpdateRequest); var result = unirest.post(FoDUrls.VULNERABILITIES + "/bulk-edit") diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index ee4c59713f..9fb3a8c755 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 @@ -890,6 +890,9 @@ fcli.fod.issue.output.table.header.updateCount = Issues Updated fcli.fod.issue.output.table.header.skippedCount = Issues Skipped fcli.fod.issue.output.table.header.errorCount = Errors fcli.fod.issue.list.usage.header = List vulnerabilities. +fcli.fod.issue.get.usage.header = Get vulnerability details. +fcli.fod.issue.get.usage.description = Get detailed data for a single FoD vulnerability in a given release. +fcli.fod.issue.get.vulnId = Issue vulnerability id. fcli.fod.issue.list.usage.description = This command allows for listing FoD vulnerability data \ for a given application or release. By default, only visible issues will be returned; the --include option can \ be used to (also) include suppressed or fixed issues. If any such issues are included, the \ @@ -909,9 +912,9 @@ fcli.fod.issue.list.usage.description = This command allows for listing FoD vuln recommended to use server-side filtering, via use of the --filters-param or --query options. \ For example, if you are only interested in issues with a specific severity, you \ can use a query like --filters-param "severityString:Critical" or --query "severityString='Critical'". -fcli.fod.issue.list.output.table.header.visibilityMarker = -fcli.fod.issue.list.output.table.header.foundInReleases = Releases -fcli.fod.issue.list.output.table.header.foundInReleasesString = Releases +fcli.fod.issue.output.table.header.visibilityMarker = +fcli.fod.issue.output.table.header.foundInReleases = Releases +fcli.fod.issue.output.table.header.foundInReleasesString = Releases fcli.fod.issue.embed = Embed extra issue data. Due to FoD rate limits, this may significantly \ affect performance. Allowed values: ${COMPLETION-CANDIDATES}. \ Using the --output option, this extra data can be included in the output. Using the --query option, \ @@ -924,8 +927,7 @@ fcli.fod.issue.list.includeIssue = By default, only visible issues will be retur fcli.fod.issue.list.aggregate = Include aggregation data. 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: for "vuln-ids" you can use either the numeric Id as shown in the FOD UI, \ - or the "vulnId" UUID field that is retrieved using the `fcli fod issue ls` command. + for multiple vulnerabilities. Note: for "vuln-ids" use the vulnId as shown in the FOD UI. fcli.fod.issue.update.user = The username or user id of the user the update will be recorded as. fcli.fod.issue.update.dev-status = The Developer Status to set for the vulnerabilities, see the FoD UI for valid values. fcli.fod.issue.update.auditor-status = The Auditor Status to set for the vulnerabilities, see the FoD UI for valid values. @@ -1063,6 +1065,7 @@ 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 = instanceId,visibilityMarker,severityString,category,location,foundInReleasesString +fcli.fod.issue.get.output.table.args = instanceId,visibilityMarker,severityString,category,location,foundInReleasesString fcli.fod.issue.update.output.table.args = totalCount,updateCount,skippedCount,errorCount fcli.fod.attribute.output.table.args = id,name,attributeType,attributeDataType,isRequired,isRestricted fcli.fod.aviator.apply-remediations.output.table.args = releaseId,totalRemediation,appliedRemediation,skippedRemediation,__action__ 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 a3d1b5e40b..54b55eea3d 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 @@ -32,6 +32,7 @@ SSCIssueGroupGetCommand.class, SSCIssueGroupListCommand.class, SSCIssueCountCommand.class, + SSCIssueGetCommand.class, SSCIssueListCommand.class, SSCIssueUpdateCommand.class, } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueGetCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueGetCommand.java new file mode 100644 index 0000000000..a25e80b505 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueGetCommand.java @@ -0,0 +1,56 @@ +/* + * 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 com.fortify.cli.common.json.producer.IObjectNodeProducer; +import com.fortify.cli.common.json.producer.ObjectNodeProducerApplyFrom; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCOutputCommand; +import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; +import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin; +import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueBulkEmbedMixin; +import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueIncludeMixin; + +import kong.unirest.HttpRequest; +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Parameters; + +@Command(name = OutputHelperMixins.Get.CMD_NAME) +public class SSCIssueGetCommand extends AbstractSSCOutputCommand { + @Getter @Mixin private OutputHelperMixins.Get outputHelper; + @Mixin private SSCAppVersionResolverMixin.RequiredOption parentResolver; + @Parameters(index = "0", arity = "1", descriptionKey = "fcli.ssc.issue.get.id") + private String id; + @Mixin private SSCIssueBulkEmbedMixin bulkEmbedMixin; + @Mixin private SSCIssueIncludeMixin includeMixin; + + @Override + protected IObjectNodeProducer getObjectNodeProducer(UnirestInstance unirest) { + String appVersionId = parentResolver.getAppVersionId(unirest); + return requestObjectNodeProducerBuilder(ObjectNodeProducerApplyFrom.SPEC) + .baseRequest(getBaseRequest(unirest, appVersionId)) + .build(); + } + + private HttpRequest getBaseRequest(UnirestInstance unirest, String appVersionId) { + return unirest.get(SSCUrls.PROJECT_VERSION_ISSUE(appVersionId, id)).queryString("qm", "issues"); + } + + @Override + public boolean isSingular() { + return true; + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueIncludeMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueIncludeMixin.java index 2a7a574c15..fcf0f497b2 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueIncludeMixin.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueIncludeMixin.java @@ -31,7 +31,7 @@ public class SSCIssueIncludeMixin implements IHttpRequestUpdater, IRecordTransformer { @DisableTest(TestType.MULTI_OPT_PLURAL_NAME) - @Option(names = {"--include", "-i"}, split = ",", defaultValue = "visible", descriptionKey = "fcli.ssc.issue.list.includeIssue", paramLabel="") + @Option(names = {"--include", "-i"}, split = ",", defaultValue = "visible", descriptionKey = "fcli.ssc.issue.includeIssue", paramLabel="") private Set includes; public HttpRequest updateRequest(HttpRequest request) { 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 3b69d2212a..05f451078d 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,13 +508,16 @@ fcli.ssc.issue.list.usage.description = This command allows for listing SSC vuln more immediate output. fcli.ssc.issue.list.output.table.header.visibilityMarker = fcli.ssc.issue.list.output.table.header.friority = Priority +fcli.ssc.issue.get.usage.header = Get vulnerability details. +fcli.ssc.issue.get.usage.description = Get detailed data for a single SSC vulnerability in a given application version. +fcli.ssc.issue.get.id = Issue id. fcli.ssc.issue.list.filter = Filter issues using the given (friendly or technical) filter. \ See 'fcli ssc issue list-filters' for allowed values. -fcli.ssc.issue.list.embed = Embed extra application version data. Allowed values: ${COMPLETION-CANDIDATES}. \ +fcli.ssc.issue.embed = Embed extra application version data. Allowed values: ${COMPLETION-CANDIDATES}. \ Using the --output option, this extra data can be included in the output. Using the --query option, \ this extra data can be queried upon. To get an understanding of the structure and contents of the \ embedded data, use the --output json or --output yaml options. -fcli.ssc.issue.list.includeIssue = By default, only visible issues will be returned. This option \ +fcli.ssc.issue.includeIssue = By default, only visible issues will be returned. This option \ 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}. @@ -708,6 +711,9 @@ fcli.ssc.attribute.definition.output.table.args = id,category,guid,name,type,req fcli.ssc.aviator.output.table.args = id,application.name,name,artifactId fcli.ssc.custom-tag.output.table.args = guid,name,valueType fcli.ssc.issue.count.output.table.args = cleanName,totalCount,auditedCount +fcli.ssc.issue.get.output.table.args = id,visibilityMarker,friority,location,issueName +fcli.ssc.issue.get.output.table.header.visibilityMarker = +fcli.ssc.issue.get.output.table.header.friority = Priority fcli.ssc.issue.list.output.table.args = id,visibilityMarker,friority,location,issueName fcli.ssc.issue.filter-set.output.table.args = guid,title,defaultFilterSet fcli.ssc.issue.group.output.table.args = guid,displayName,entityType