-
Notifications
You must be signed in to change notification settings - Fork 33
Add issue get command for FoD and SSC #978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev/v3.x
Are you sure you want to change the base?
Changes from all commits
ca7f542
6b40ff9
6ada3b4
f945c6f
4c69521
ebfd029
cd0d1c9
9353886
c81fe68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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.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); | ||
| 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("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() ) { 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 |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| /* | ||
| * 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 java.util.Arrays; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| 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.common.rest.unirest.IHttpRequestUpdater; | ||
| 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 implements IHttpRequestUpdater { | ||
| @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)) | ||
| .queryString("showHidden", "true") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the |
||
| .queryString("showRemoved", "true") | ||
| .queryString("showSuppressed", "true"); | ||
| } | ||
|
|
||
| @Override | ||
| public HttpRequest<?> updateRequest(HttpRequest<?> request) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this code:
|
||
| var embedSuppliers = bulkEmbedMixin.getEmbedSuppliers(); | ||
| if (embedSuppliers == null || embedSuppliers.length == 0) { | ||
| return request.queryString("qm", "issues"); | ||
| } | ||
| var embedNames = Arrays.stream(embedSuppliers).map(Enum::name).collect(Collectors.joining(",")); | ||
| return request.queryString("qm", "issues," + embedNames); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isSingular() { | ||
| return true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,49 +39,74 @@ public ISSCEntityEmbedder createEntityEmbedder() { | |
| } | ||
|
|
||
| private static abstract class AbstractSSCIssueEmbedder implements ISSCEntityEmbedder { | ||
| @Override | ||
| public 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))); | ||
| } | ||
| protected abstract String getEmbedFieldName(); | ||
|
|
||
| protected abstract HttpRequest<?> getBaseRequest(UnirestInstance unirest); | ||
|
|
||
| protected abstract void process(ObjectNode record, JsonNode response); | ||
|
|
||
| @Override | ||
| public final void addEmbedRequests(SSCBulkRequestBuilder builder, UnirestInstance unirest, JsonNode record) { | ||
| // Skip if data is already present in the record (e.g. returned by SSC via qm parameter) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assumption is nonsense; SSC |
||
| if (record.has(getEmbedFieldName()) && !record.get(getEmbedFieldName()).isNull()) { | ||
| return; | ||
| } | ||
| var id = record.get("id").asText(); | ||
| builder.request( | ||
| getBaseRequest(unirest).routeParam("id", id), | ||
| response -> process((ObjectNode) record, SSCInputTransformer.getDataOrSelf(response)) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| private static final class SSCIssueDetailsEmbedder extends AbstractSSCIssueEmbedder { | ||
| @Override | ||
| protected String getEmbedFieldName() { | ||
| return "details"; | ||
| } | ||
|
|
||
| @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 String getEmbedFieldName() { | ||
| return "auditHistory"; | ||
| } | ||
|
|
||
| @Override | ||
| protected HttpRequest<?> getBaseRequest(UnirestInstance unirest) { | ||
| return unirest.get("/api/v1/issues/{id}/auditHistory?limit=-1"); | ||
| return unirest.get("/api/v1/issues/{id}/auditHistory").queryString("limit", "-1"); | ||
| } | ||
|
|
||
| @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 String getEmbedFieldName() { | ||
| return "comments"; | ||
| } | ||
|
|
||
| @Override | ||
| protected HttpRequest<?> getBaseRequest(UnirestInstance unirest) { | ||
| return unirest.get("/api/v1/issues/{id}/comments?limit=-1"); | ||
| return unirest.get("/api/v1/issues/{id}/comments").queryString("limit", "-1"); | ||
| } | ||
|
|
||
| @Override | ||
| protected void process(ObjectNode record, JsonNode response) { | ||
| record.set("comments", response); | ||
| record.set("comments", response); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand how the code changes in this method are related to the goal of this PR (adding
issue getcommand), and currenttoJsonNodeimplementation is wrong. Data types in fcli output must be stable to allow for external processing, i.e., not switch between array or text node depending on the number of available entries. Also, in general,N/Ashould be modelled asnullor empty array, notN/Atext node as that's difficult to differentiate from actual values.So, all the array fields must be either a proper array (which may be empty, single value, or multiple values array), or possibly
null, like it was in the original code.