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,87 @@
/*
* 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.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.common.util.DisableTest;
import com.fortify.cli.common.util.DisableTest.TestType;
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.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin;

import kong.unirest.HttpRequest;
import kong.unirest.UnirestInstance;
import lombok.Getter;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Parameters;

@DisableTest(TestType.CMD_DEFAULT_TABLE_OPTIONS_PRESENT)
@Command(name = OutputHelperMixins.Get.CMD_NAME)
public class FoDIssueGetCommand extends AbstractFoDOutputCommand {
Comment on lines +37 to +39
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

FoDIssueGetCommand disables CMD_DEFAULT_TABLE_OPTIONS_PRESENT, but there is no *.output.table.args defined for this command in FoDMessages.properties. Rather than disabling the command-tree test, add a default table column definition (for example fcli.fod.issue.get.output.table.args or a shared fcli.fod.issue.output.table.args that get can inherit) and remove the @DisableTest annotation so this command adheres to the same output conventions as other leaf commands.

Copilot uses AI. Check for mistakes.
@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.id")
private String vulnId;
@Mixin private FoDIssueEmbedMixin embedMixin;
@Mixin private FoDIssueIncludeMixin includeMixin;

@Override
protected IObjectNodeProducer getObjectNodeProducer(UnirestInstance unirest) {
String releaseId = releaseResolver.getReleaseId(unirest);
JsonNode issue = getIssue(unirest, releaseId);
return simpleObjectNodeProducerBuilder(ObjectNodeProducerApplyFrom.SPEC)
.source(issue)
.build();
}

private JsonNode getIssue(UnirestInstance unirest, String releaseId) {
boolean numericId = vulnId!=null && vulnId.chars().allMatch(Character::isDigit);
JsonNode issue = numericId
? getIssueByFilter(unirest, releaseId, "id", vulnId)
: getIssueByFilter(unirest, releaseId, "vulnId", vulnId);
if ( issue==null ) {
issue = numericId
? getIssueByFilter(unirest, releaseId, "vulnId", vulnId)
: getIssueByFilter(unirest, releaseId, "id", vulnId);
}
if ( issue==null ) {
throw new FcliSimpleException(String.format("No issue found for id or vulnId '%s' in the specified release", vulnId));
}
return issue;
}

private JsonNode getIssueByFilter(UnirestInstance unirest, String releaseId, String fieldName, String value) {
HttpRequest<?> request = unirest.get(FoDUrls.VULNERABILITIES)
.routeParam("relId", releaseId)
.queryString("filters", fieldName+":"+value)
.queryString("limit", "1");
Comment on lines +73 to +77
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

This get implementation still calls the vulnerabilities list endpoint (FoDUrls.VULNERABILITIES) with filters + limit=1 and then takes the first result. That contradicts the PR description/issue goal of avoiding list+filter for single-issue retrieval. Consider switching to the single-vulnerability endpoint described in #974 (GET /api/v3/releases/{releaseId}/vulnerabilities/{vulnId}), or at minimum document why the list endpoint is required here.

Copilot uses AI. Check for mistakes.
JsonNode body = includeMixin.updateRequest(request).asObject(JsonNode.class).getBody();
JsonNode items = FoDInputTransformer.getItems(body);
return items!=null && items.isArray() && !items.isEmpty() ? items.get(0) : null;
Comment on lines +74 to +80
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

getIssueByFilter() ignores HTTP status codes and treats any response without items as "not found". For example, authentication/authorization errors or an invalid release id could result in an error response body that then gets converted into the misleading "No issue found..." exception. Consider using Unirest's ifFailure(IfFailureHandler::handle) (or explicitly checking response.getStatus()) before attempting to parse items, so real API errors are surfaced correctly.

Copilot uses AI. Check for mistakes.
}

@Override
public boolean isSingular() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,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.id = Vulnerability id or vulnId.
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 \
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;

@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 @@ -504,13 +504,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 application version vulnerability details.
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

The new fcli.ssc.issue.get.usage.header text says "Get application version vulnerability details", which reads like an application version command rather than an issue command. Consider aligning the header with the actual command (ssc issue get) to avoid confusing help output (for example, "Get vulnerability details" or "Get issue details").

Suggested change
fcli.ssc.issue.get.usage.header = Get application version vulnerability details.
fcli.ssc.issue.get.usage.header = Get vulnerability details.

Copilot uses AI. Check for mistakes.
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 @@ -704,6 +707,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