Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
@CommandLine.Command(name = "issue",
subcommands = {
FoDIssueListCommand.class,
FoDIssueGetCommand.class,
FoDIssueUpdateCommand.class,
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -92,7 +92,7 @@ public String getReleaseIdsString() {
}

public String getIdsString() {
return asString(ids);
return asString(ids);
}

private String asString(Set<String> values) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand All @@ -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, \
Expand Down Expand Up @@ -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__
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
SSCIssueGroupGetCommand.class,
SSCIssueGroupListCommand.class,
SSCIssueCountCommand.class,
SSCIssueGetCommand.class,
SSCIssueListCommand.class,
SSCIssueUpdateCommand.class,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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="<status>")
@Option(names = {"--include", "-i"}, split = ",", defaultValue = "visible", descriptionKey = "fcli.ssc.issue.includeIssue", paramLabel="<status>")
private Set<SSCIssueInclude> includes;

public HttpRequest<?> updateRequest(HttpRequest<?> request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Comment thread
jmadhur87 marked this conversation as resolved.
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -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
Expand Down
Loading