Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,85 @@
/*
* 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.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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, same is true here as what I wrote for SSC. Although (contrary to SSC) the FoD endpoint used by this command does support the query parameters generated FoDIssueIncludeMixin, the user shouldn't need to explicitly pass --include if the given issue id is hidden/removed/... If user passes an id, we should just gt that id regardless of issue state. In other words, we should automatically pass query parameters to find issues in any state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To confirm my understanding the fix here would be to remove the [--include] option from the [get] command and hardcode includeFixed=true and includeSuppressed=true in [findIssue()] for both ssc and fod, so that any issue can be retrieved by ID regardless of its state, without requiring the user to pass extra flags. Is that what you're expecting?

@rsenden rsenden Jun 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct for FoD; you'd always add the request parameters to have FoD return fixed/suppressed/... issues, such that FoD will return the issue data for the requested id independent of whether it's visible, fixed, suppressed, ...

For SSC, you're using an endpoint to get the issue data for a specific issue id. This endpoint will return that data independent of whether the issue is hidden/removed/suppressed; the endpoint doesn't support the request parameters generated by the SSCIssueIncludeMixin, and doesn't support the qm request parameter as explained in other comments.


@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");
JsonNode body = includeMixin.updateRequest(request).asObject(JsonNode.class).getBody();
JsonNode items = FoDInputTransformer.getItems(body);
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;
}
}
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 All @@ -105,25 +105,29 @@ private String asString(Set<String> 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<String> 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 = JsonHelper.getObjectMapper().valueToTree(issueUpdateRequest);
var result = unirest.post(FoDUrls.VULNERABILITIES + "/bulk-edit")
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,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;
Comment thread
jmadhur87 marked this conversation as resolved.

@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;
}
}
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 @@ -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