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 3fa8be83e6d..cda827f0873 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 00000000000..f230059684b --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueGetCommand.java @@ -0,0 +1,86 @@ +/* + * 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.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; + + @Override + protected IObjectNodeProducer getObjectNodeProducer(UnirestInstance unirest) { + FoDReleaseDescriptor releaseDescriptor = releaseResolver.getReleaseDescriptor(unirest); + ObjectNode issue = findIssue(unirest, releaseDescriptor); + issue.put("releaseName", releaseDescriptor.getReleaseName()); + FoDIssueHelper.transformRecord(issue, IssueAggregationData.forSingleRelease(issue)); + return simpleObjectNodeProducerBuilder(ObjectNodeProducerApplyFrom.SPEC) + .source(issue) + .build(); + } + + private ObjectNode findIssue(UnirestInstance unirest, FoDReleaseDescriptor releaseDescriptor) { + String releaseId = releaseDescriptor.getReleaseId().toString(); + HttpRequest request = unirest.get(FoDUrls.VULNERABILITIES) + .routeParam("relId", releaseId) + .queryString("filters", "vulnId:" + vulnId) + .queryString("includeFixed", "true") + .queryString("includeSuppressed", "true") + .queryString("limit", "2"); + JsonNode body = request.asObject(JsonNode.class).getBody(); + JsonNode items = FoDInputTransformer.getItems(body); + if ( items==null || !items.isArray() || items.isEmpty() ) { + throw new FcliSimpleException(String.format("No vulnerability found for vulnId '%s' in release '%s'", vulnId, releaseDescriptor.getReleaseName())); + } + if ( items.size()>1 ) { + throw new FcliSimpleException(String.format("Multiple vulnerabilities found for vulnId '%s'; please check your input", vulnId)); + } + JsonNode issue = items.get(0); + if ( !(issue instanceof ObjectNode issueObject) ) { + throw new FcliTechnicalException(String.format("Unexpected response for vulnId '%s'; expected a JSON object", vulnId)); + } + return issueObject; + } + + @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 cef7e049915..a44b2941c71 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 @@ -65,7 +65,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(); } @@ -92,7 +92,7 @@ public String getReleaseIdsString() { } public String getIdsString() { - return asString(ids); + return asString(ids); } private String asString(Set values) { 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 970563babbe..9e1539ca68d 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 @@ -929,9 +929,9 @@ fcli.fod.issue.list.usage.description = This command allows for listing FoD issu 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, \ @@ -942,6 +942,9 @@ fcli.fod.issue.list.includeIssue = By default, only visible issues will be retur for example `--include visible,fixed` (to return both visible and fixed issues) or `--include \ fixed` (to return only fixed issues). Allowed values: ${COMPLETION-CANDIDATES}. fcli.fod.issue.list.aggregate = Include aggregation data. +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.update.usage.header = Bulk update issues (vulnerabilities). fcli.fod.issue.update.usage.description = This command allows for updating the audit information \ for multiple issues (vulnerabilities). Note: for --vuln-ids/--issue-ids, you can use either the numeric Id as shown in the FoD UI, \ @@ -1087,6 +1090,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 a3d1b5e40bf..54b55eea3d4 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 00000000000..863b3aea270 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueGetCommand.java @@ -0,0 +1,55 @@ +/* + * 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.cli.util.EnvSuffix; +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 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; + @EnvSuffix("ISSUE_ID") @Parameters(index = "0", arity = "1", descriptionKey = "fcli.ssc.issue.get.id") + private String id; + @Mixin private SSCIssueBulkEmbedMixin bulkEmbedMixin; + + @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)); + } + + @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 2a7a574c158..fcf0f497b2d 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/java/com/fortify/cli/ssc/issue/helper/SSCIssueEmbedderSupplier.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueEmbedderSupplier.java index a7f02ed3fa1..cf0b37f621e 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueEmbedderSupplier.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueEmbedderSupplier.java @@ -39,49 +39,61 @@ public ISSCEntityEmbedder createEntityEmbedder() { } private static abstract class AbstractSSCIssueEmbedder implements ISSCEntityEmbedder { + protected abstract HttpRequest getBaseRequest(UnirestInstance unirest); + + protected abstract void process(ObjectNode record, JsonNode response); + + // Whether the embedder's endpoint supports the 'limit' parameter to retrieve all records + protected boolean supportsLimit() { return true; } + @Override - public void addEmbedRequests(SSCBulkRequestBuilder builder, UnirestInstance unirest, JsonNode record) { + public final void addEmbedRequests(SSCBulkRequestBuilder builder, UnirestInstance unirest, JsonNode record) { var id = record.get("id").asText(); - builder.request(getBaseRequest(unirest) - .routeParam("id", id) - .queryString("limit", "-1"), - response->process((ObjectNode)record, SSCInputTransformer.getDataOrSelf(response))); + var request = getBaseRequest(unirest).routeParam("id", id); + if ( supportsLimit() ) { request.queryString("limit", "-1"); } + builder.request( + request, + response -> process((ObjectNode) record, SSCInputTransformer.getDataOrSelf(response)) + ); } - - protected abstract HttpRequest getBaseRequest(UnirestInstance unirest); - protected abstract void process(ObjectNode record, JsonNode response); } - + private static final class SSCIssueDetailsEmbedder extends AbstractSSCIssueEmbedder { + @Override + protected boolean supportsLimit() { return false; } + @Override protected HttpRequest getBaseRequest(UnirestInstance unirest) { return unirest.get("/api/v1/issueDetails/{id}"); } + @Override protected void process(ObjectNode record, JsonNode response) { - record.set("details", response); + record.set("details", response); } } - + private static final class SSCIssueAuditHistoryEmbedder extends AbstractSSCIssueEmbedder { @Override protected HttpRequest getBaseRequest(UnirestInstance unirest) { - return unirest.get("/api/v1/issues/{id}/auditHistory?limit=-1"); + return unirest.get("/api/v1/issues/{id}/auditHistory"); } + @Override protected void process(ObjectNode record, JsonNode response) { - record.set("auditHistory", response); + record.set("auditHistory", response); } } - + private static final class SSCIssueCommentsEmbedder extends AbstractSSCIssueEmbedder { @Override protected HttpRequest getBaseRequest(UnirestInstance unirest) { - return unirest.get("/api/v1/issues/{id}/comments?limit=-1"); + return unirest.get("/api/v1/issues/{id}/comments"); } + @Override protected void process(ObjectNode record, JsonNode response) { - record.set("comments", response); + record.set("comments", response); } } } 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 d2dd0ebe86a..2e8fb3a297a 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 @@ -514,13 +514,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}. @@ -715,6 +718,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