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,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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

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

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

private String asString(Set<String> values) {
Expand All @@ -192,25 +192,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 = objectMapper.valueToTree(issueUpdateRequest);
var result = unirest.post(FoDUrls.VULNERABILITIES + "/bulk-edit")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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, \
Expand All @@ -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.
Expand Down Expand Up @@ -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__
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 @@ -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}.
Expand Down Expand Up @@ -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
Expand Down
Loading