From 701ef7377901fab30691bf2f20402be0ab795a45 Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Fri, 6 Mar 2026 17:44:21 +0530 Subject: [PATCH 1/2] fix: enforce FoD app/release exclusivity and correct session login options --- .../issue/cli/cmd/FoDIssueListCommand.java | 59 +++++++++++-------- .../cli/cmd/FoDOssComponentsListCommand.java | 45 +++++++++----- ...sueListCommandEffectiveFastOutputTest.java | 23 +++----- 3 files changed, 73 insertions(+), 54 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java index c017709fe5..3dda2fa84a 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fortify.cli.common.exception.FcliSimpleException; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.json.producer.AbstractObjectNodeProducer.AbstractObjectNodeProducerBuilder; import com.fortify.cli.common.json.producer.IObjectNodeProducer; @@ -49,6 +48,7 @@ import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; +import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; @@ -57,8 +57,8 @@ public class FoDIssueListCommand extends AbstractFoDOutputCommand implements IServerSideQueryParamGeneratorSupplier { @Getter @Mixin private OutputHelperMixins.List outputHelper; @Mixin private FoDDelimiterMixin delimiterMixin; // injected in resolvers - @Mixin private FoDAppResolverMixin.OptionalOption appResolver; - @Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.OptionalOption releaseResolver; + @ArgGroup(exclusive = true, multiplicity = "1", order = 1) + @Getter private TargetSpecifierArgGroup targetSpecifier = new TargetSpecifierArgGroup(); @Mixin private FoDFiltersParamMixin filterParamMixin; @Mixin private FoDIssueEmbedMixin embedMixin; @Mixin private FoDIssueIncludeMixin includeMixin; @@ -75,23 +75,34 @@ public class FoDIssueListCommand extends AbstractFoDOutputCommand implements ISe .add("severityString","severityString") .add("category","category"); + public static class TargetSpecifierArgGroup { + @ArgGroup(exclusive = false, multiplicity = "1", order = 1) @Getter private AppTarget app = new AppTarget(); + @ArgGroup(exclusive = false, multiplicity = "1", order = 2) @Getter private ReleaseTarget release = new ReleaseTarget(); + } + + public static class AppTarget extends FoDAppResolverMixin.AbstractFoDAppResolverMixin { + @Option(names = { "--app" }, required = true, descriptionKey = "fcli.fod.app.app-name-or-id") @Getter private String appNameOrId; + } + + public static class ReleaseTarget extends FoDReleaseByQualifiedNameOrIdResolverMixin.AbstractFoDQualifiedReleaseNameOrIdResolverMixin { + @Option(names = { "--release", "--rel" }, required = true, paramLabel = "id|app[:ms]:rel", descriptionKey = "fcli.fod.release.resolver.name-or-id") @Getter private String qualifiedReleaseNameOrId; + } + @Override protected IObjectNodeProducer getObjectNodeProducer(UnirestInstance unirest) { - boolean releaseSpecified = releaseResolver.getQualifiedReleaseNameOrId() != null; - boolean appSpecified = appResolver.getAppNameOrId() != null; - if ( releaseSpecified && appSpecified ) { - throw new FcliSimpleException("Cannot specify both an application and release"); - } - if ( !releaseSpecified && !appSpecified ) { - throw new FcliSimpleException("Either an application or release must be specified"); + var appGroup = targetSpecifier.getApp(); + var releaseGroup = targetSpecifier.getRelease(); + + boolean appSpecified = appGroup != null && appGroup.getAppNameOrId() != null; + boolean releaseSpecified = releaseGroup != null && releaseGroup.getQualifiedReleaseNameOrId() != null; + + if (releaseSpecified) { + releaseGroup.setDelimiterMixin(delimiterMixin); } + var result = releaseSpecified - ? singleReleaseProducerBuilder(unirest, releaseResolver.getReleaseId(unirest)) - : applicationProducerBuilder(unirest, appResolver.getAppId(unirest)); - // For consistent output, we should remove releaseId/releaseName when listing across multiple releases, - // but that breaks existing scripts that may rely on those fields, so for now, we only do this in - // applicationProducerBuilder(). TODO: Change in in fcli v4.0. - // return result.recordTransformer(this::removeReleaseProperties).build(); + ? singleReleaseProducerBuilder(unirest, releaseGroup.getReleaseId(unirest)) + : applicationProducerBuilder(unirest, appGroup.getAppId(unirest)); return result.build(); } @@ -225,17 +236,15 @@ private JsonNode enrichIssueRecord(UnirestInstance unirest, String releaseName, } private boolean isEffectiveFastOutput() { - boolean appSpecified = appResolver.getAppNameOrId() != null; - boolean releaseSpecified = releaseResolver.getQualifiedReleaseNameOrId() != null; - if ( !appSpecified || releaseSpecified ) { return false; } + var appGroup = targetSpecifier.getApp(); + var releaseGroup = targetSpecifier.getRelease(); + + boolean appSpecified = appGroup != null && appGroup.getAppNameOrId() != null; + boolean releaseSpecified = releaseGroup != null && releaseGroup.getQualifiedReleaseNameOrId() != null; + if (!appSpecified || releaseSpecified) { return false; } boolean fastOutputStyle = outputHelper.getRecordWriterStyle().isFastOutput(); boolean streamingSupported = outputHelper.isStreamingOutputSupported(); - boolean recordConsumerConfigured = getRecordConsumer()!=null; - // Effective fast output requires: - // - application specified (multiple releases) - // - fast output style - // - no aggregation (merging requires full set) - // - streaming output or record consumer configured + boolean recordConsumerConfigured = getRecordConsumer() != null; return fastOutputStyle && !aggregate && (streamingSupported || recordConsumerConfigured); } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java index 7fb80227dc..2ba7cf06af 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java @@ -39,6 +39,7 @@ import kong.unirest.HttpResponse; import kong.unirest.UnirestInstance; import lombok.Getter; +import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; @@ -47,24 +48,40 @@ @CommandGroup("oss-components") public final class FoDOssComponentsListCommand extends AbstractFoDJsonNodeOutputCommand { private static final Logger LOG = LoggerFactory.getLogger(FoDOssComponentsListCommand.class); - @Getter - @Mixin - private OutputHelperMixins.TableWithQuery outputHelper; - @Mixin - private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins - @Mixin - private FoDAppResolverMixin.OptionalOption appResolver; - @Mixin - private FoDReleaseByQualifiedNameOrIdResolverMixin.OptionalOption releaseResolver; - @Option(names = "--scan-types", required = true, split = ",", defaultValue = "Debricked") - private FoDOpenSourceScanType[] scanTypes; + @Getter @Mixin private OutputHelperMixins.TableWithQuery outputHelper; + @Mixin private FoDDelimiterMixin delimiterMixin; + @ArgGroup(exclusive = true, multiplicity = "1", order = 1) @Getter private TargetSpecifierArgGroup targetSpecifier = new TargetSpecifierArgGroup(); + @Option(names = "--scan-types", required = true, split = ",", defaultValue = "Debricked") private FoDOpenSourceScanType[] scanTypes; + + public static class TargetSpecifierArgGroup { + @ArgGroup(exclusive = false, multiplicity = "1", order = 1) @Getter private AppTarget app = new AppTarget(); + @ArgGroup(exclusive = false, multiplicity = "1", order = 2) @Getter private ReleaseTarget release = new ReleaseTarget(); + } + + public static class AppTarget extends FoDAppResolverMixin.AbstractFoDAppResolverMixin { + @Option(names = { "--app" }, required = true, descriptionKey = "fcli.fod.app.app-name-or-id") @Getter private String appNameOrId; + } + + public static class ReleaseTarget extends FoDReleaseByQualifiedNameOrIdResolverMixin.AbstractFoDQualifiedReleaseNameOrIdResolverMixin { + @Option(names = { "--release", "--rel" }, required = true, paramLabel = "id|app[:ms]:rel", descriptionKey = "fcli.fod.release.resolver.name-or-id") @Getter private String qualifiedReleaseNameOrId; + } @Override public JsonNode getJsonNode(UnirestInstance unirest) { ArrayNode result = JsonHelper.getObjectMapper().createArrayNode(); + + var appGroup = targetSpecifier.getApp(); + var releaseGroup = targetSpecifier.getRelease(); + + final String applicationId = (appGroup != null && appGroup.getAppNameOrId() != null) + ? appGroup.getAppId(unirest) + : null; + final String releaseId = (releaseGroup != null && releaseGroup.getQualifiedReleaseNameOrId() != null) + ? releaseGroup.getReleaseId(unirest) + : null; + Stream.of(scanTypes) - .map(t -> getForOpenSourceScanType(unirest, t, releaseResolver.getReleaseId(unirest), - appResolver.getAppId(unirest), false)) + .map(t -> getForOpenSourceScanType(unirest, t, releaseId, applicationId, false)) .forEach(result::addAll); return result; } @@ -100,7 +117,7 @@ private ArrayNode getForOpenSourceScanType(UnirestInstance unirest, FoDOpenSourc if (failOnError) { throw e; } - LOG.error("Error retrieving OSS components for release " + releaseResolver.getReleaseId(unirest) + LOG.error("Error retrieving OSS components for release " + releaseId + " and scan type " + scanType.name() + ": " + e.getMessage()); return JsonHelper.getObjectMapper().createArrayNode(); } diff --git a/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java index 1de2141541..ba27485aa7 100644 --- a/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java +++ b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java @@ -23,8 +23,6 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.writer.record.RecordWriterStyle; import com.fortify.cli.common.output.writer.record.RecordWriterStyle.RecordWriterStyleElement; -import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; -import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; /** * Tests for FoDIssueListCommand.isEffectiveFastOutput logic after migration to style-based fast-output. @@ -39,10 +37,7 @@ public class FoDIssueListCommandEffectiveFastOutputTest { void init() throws Exception { cmd = new FoDIssueListCommand(); streamingStub = new StreamingStubOutputHelper(); - setField(cmd, "outputHelper", streamingStub); // inject stub - // Provide empty mixins so reflection can set their private fields - setField(cmd, "appResolver", new FoDAppResolverMixin.OptionalOption()); - setField(cmd, "releaseResolver", new FoDReleaseByQualifiedNameOrIdResolverMixin.OptionalOption()); + setField(cmd, "outputHelper", streamingStub); } @Test @@ -94,17 +89,15 @@ private void setField(Object target, String fieldName, Object value) throws Exce } private void setApp(String app) throws Exception { - Object appResolver = getField(cmd, "appResolver"); - setField(appResolver, "appNameOrId", app); + var target = cmd.getTargetSpecifier(); + var appGroup = target.getApp(); + setField(appGroup, "appNameOrId", app); } + private void setRelease(String rel) throws Exception { - Object relResolver = getField(cmd, "releaseResolver"); - setField(relResolver, "qualifiedReleaseNameOrId", rel); - } - private Object getField(Object target, String fieldName) throws Exception { - Field f = target.getClass().getDeclaredField(fieldName); - f.setAccessible(true); - return f.get(target); + var target = cmd.getTargetSpecifier(); + var releaseGroup = target.getRelease(); + setField(releaseGroup, "qualifiedReleaseNameOrId", rel); } private boolean invokeIsEffectiveFastOutput() throws Exception { From 94cd41d813d40b03702946529cd32ee3e2504cf6 Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Sat, 14 Mar 2026 09:28:08 +0530 Subject: [PATCH 2/2] fix: enforce FoD app/release exclusivity and correct session login options --- .../cli/mixin/FoDAppOrReleaseArgGroup.java | 62 ++++++++++++++++++ .../cli/mixin/FoDAppOrReleaseMixin.java | 64 +++++++++++++++++++ .../issue/cli/cmd/FoDIssueListCommand.java | 46 +++---------- .../cli/cmd/FoDOssComponentsListCommand.java | 30 ++------- ...sueListCommandEffectiveFastOutputTest.java | 8 ++- 5 files changed, 147 insertions(+), 63 deletions(-) create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseArgGroup.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseMixin.java diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseArgGroup.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseArgGroup.java new file mode 100644 index 0000000000..850299ece3 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseArgGroup.java @@ -0,0 +1,62 @@ +/* + * 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._common.cli.mixin; + +import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; +import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; + +import lombok.Getter; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; + +/** + * Argument group for commands that require either an application or release as + * target. This is a common pattern in FoD commands, where some commands can + * target either an application or a release, and the user can specify either + * one of them. This argument group allows for defining both options in a + * mutually exclusive way, and provides getters for both the app and release + * targets. The app target is resolved using the FoDAppResolverMixin, while the + * release target is resolved using the + * FoDReleaseByQualifiedNameOrIdResolverMixin. This argument group can be used + * in any command that needs to support both app and release targeting, without + * having to duplicate the logic for resolving the targets. + * + * @author Sangamesh Vijaykumar + */ +public final class FoDAppOrReleaseArgGroup { + @ArgGroup(exclusive = false, multiplicity = "1", order = 1) + @Getter private FoDAppArgGroup app = new FoDAppArgGroup(); + + @ArgGroup(exclusive = false, multiplicity = "1", order = 2) + @Getter private FoDReleaseArgGroup release = new FoDReleaseArgGroup(); + + public static class FoDAppArgGroup extends FoDAppResolverMixin.AbstractFoDAppResolverMixin { + @Option( + names = { "--app" }, + required = true, + descriptionKey = "fcli.fod.app.app-name-or-id" + ) + @Getter private String appNameOrId; + } + + public static class FoDReleaseArgGroup + extends FoDReleaseByQualifiedNameOrIdResolverMixin.AbstractFoDQualifiedReleaseNameOrIdResolverMixin { + @Option( + names = { "--release", "--rel" }, + required = true, + paramLabel = "id|app[:ms]:rel", + descriptionKey = "fcli.fod.release.resolver.name-or-id" + ) + @Getter private String qualifiedReleaseNameOrId; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseMixin.java new file mode 100644 index 0000000000..4018e8ff00 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/FoDAppOrReleaseMixin.java @@ -0,0 +1,64 @@ +/* + * 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._common.cli.mixin; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.ArgGroup; + +/** + * Mixin that allows specifying either an application or a release as target for + * a command. The application and release are specified using the same syntax as + * the corresponding resolvers (FoDAppResolverMixin and + * FoDReleaseByQualifiedNameOrIdResolverMixin, respectively), and the mixin + * ensures that exactly one of them is specified. The mixin also propagates the + * delimiter mixin to the release resolver, so that the same delimiter can be + * used for both application and release specification. This mixin is intended + * to be used in commands that can target either an application or a release, + * and that need to resolve the corresponding IDs based on the specified names + * or IDs. + * + * @author Sangamesh Vijaykumar + */ +public final class FoDAppOrReleaseMixin implements IFoDDelimiterMixinAware { + @ArgGroup(exclusive = true, multiplicity = "1", order = 1) + @Getter private FoDAppOrReleaseArgGroup fodAppOrReleaseArgGroup = new FoDAppOrReleaseArgGroup(); + + @Override + public void setDelimiterMixin(FoDDelimiterMixin delimiterMixin) { + FoDAppOrReleaseArgGroup.FoDReleaseArgGroup release = fodAppOrReleaseArgGroup.getRelease(); + if (release != null) { + release.setDelimiterMixin(delimiterMixin); + } + } + + public boolean isAppSpecified() { + FoDAppOrReleaseArgGroup.FoDAppArgGroup app = fodAppOrReleaseArgGroup.getApp(); + return app != null && app.getAppNameOrId() != null; + } + + public boolean isReleaseSpecified() { + FoDAppOrReleaseArgGroup.FoDReleaseArgGroup release = fodAppOrReleaseArgGroup.getRelease(); + return release != null && release.getQualifiedReleaseNameOrId() != null; + } + + public String getAppId(UnirestInstance unirest) { + FoDAppOrReleaseArgGroup.FoDAppArgGroup app = fodAppOrReleaseArgGroup.getApp(); + return app == null ? null : app.getAppId(unirest); + } + + public String getReleaseId(UnirestInstance unirest) { + FoDAppOrReleaseArgGroup.FoDReleaseArgGroup release = fodAppOrReleaseArgGroup.getRelease(); + return release == null ? null : release.getReleaseId(unirest); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java index 3dda2fa84a..dc60abb40d 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java @@ -31,24 +31,22 @@ import com.fortify.cli.common.rest.query.IServerSideQueryParamGeneratorSupplier; import com.fortify.cli.common.rest.query.IServerSideQueryParamValueGenerator; import com.fortify.cli.common.util.Break; +import com.fortify.cli.fod._common.cli.mixin.FoDAppOrReleaseMixin; 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.query.FoDFiltersParamGenerator; import com.fortify.cli.fod._common.rest.query.cli.mixin.FoDFiltersParamMixin; -import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; 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 com.fortify.cli.fod.release.helper.FoDReleaseHelper; import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; -import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; @@ -56,9 +54,8 @@ @Command(name = OutputHelperMixins.List.CMD_NAME) public class FoDIssueListCommand extends AbstractFoDOutputCommand implements IServerSideQueryParamGeneratorSupplier { @Getter @Mixin private OutputHelperMixins.List outputHelper; - @Mixin private FoDDelimiterMixin delimiterMixin; // injected in resolvers - @ArgGroup(exclusive = true, multiplicity = "1", order = 1) - @Getter private TargetSpecifierArgGroup targetSpecifier = new TargetSpecifierArgGroup(); + @Mixin private FoDDelimiterMixin delimiterMixin; + @Mixin @Getter private FoDAppOrReleaseMixin appOrRelease; @Mixin private FoDFiltersParamMixin filterParamMixin; @Mixin private FoDIssueEmbedMixin embedMixin; @Mixin private FoDIssueIncludeMixin includeMixin; @@ -75,34 +72,12 @@ public class FoDIssueListCommand extends AbstractFoDOutputCommand implements ISe .add("severityString","severityString") .add("category","category"); - public static class TargetSpecifierArgGroup { - @ArgGroup(exclusive = false, multiplicity = "1", order = 1) @Getter private AppTarget app = new AppTarget(); - @ArgGroup(exclusive = false, multiplicity = "1", order = 2) @Getter private ReleaseTarget release = new ReleaseTarget(); - } - - public static class AppTarget extends FoDAppResolverMixin.AbstractFoDAppResolverMixin { - @Option(names = { "--app" }, required = true, descriptionKey = "fcli.fod.app.app-name-or-id") @Getter private String appNameOrId; - } - - public static class ReleaseTarget extends FoDReleaseByQualifiedNameOrIdResolverMixin.AbstractFoDQualifiedReleaseNameOrIdResolverMixin { - @Option(names = { "--release", "--rel" }, required = true, paramLabel = "id|app[:ms]:rel", descriptionKey = "fcli.fod.release.resolver.name-or-id") @Getter private String qualifiedReleaseNameOrId; - } - @Override protected IObjectNodeProducer getObjectNodeProducer(UnirestInstance unirest) { - var appGroup = targetSpecifier.getApp(); - var releaseGroup = targetSpecifier.getRelease(); - - boolean appSpecified = appGroup != null && appGroup.getAppNameOrId() != null; - boolean releaseSpecified = releaseGroup != null && releaseGroup.getQualifiedReleaseNameOrId() != null; - - if (releaseSpecified) { - releaseGroup.setDelimiterMixin(delimiterMixin); - } - + boolean releaseSpecified = appOrRelease.isReleaseSpecified(); var result = releaseSpecified - ? singleReleaseProducerBuilder(unirest, releaseGroup.getReleaseId(unirest)) - : applicationProducerBuilder(unirest, appGroup.getAppId(unirest)); + ? singleReleaseProducerBuilder(unirest, appOrRelease.getReleaseId(unirest)) + : applicationProducerBuilder(unirest, appOrRelease.getAppId(unirest)); return result.build(); } @@ -236,12 +211,9 @@ private JsonNode enrichIssueRecord(UnirestInstance unirest, String releaseName, } private boolean isEffectiveFastOutput() { - var appGroup = targetSpecifier.getApp(); - var releaseGroup = targetSpecifier.getRelease(); - - boolean appSpecified = appGroup != null && appGroup.getAppNameOrId() != null; - boolean releaseSpecified = releaseGroup != null && releaseGroup.getQualifiedReleaseNameOrId() != null; - if (!appSpecified || releaseSpecified) { return false; } + if (!appOrRelease.isAppSpecified() || appOrRelease.isReleaseSpecified()) { + return false; + } boolean fastOutputStyle = outputHelper.getRecordWriterStyle().isFastOutput(); boolean streamingSupported = outputHelper.isStreamingOutputSupported(); boolean recordConsumerConfigured = getRecordConsumer() != null; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java index 2ba7cf06af..53830d51fe 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssComponentsListCommand.java @@ -26,6 +26,7 @@ import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; +import com.fortify.cli.fod._common.cli.mixin.FoDAppOrReleaseMixin; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; @@ -33,13 +34,10 @@ import com.fortify.cli.fod._common.rest.helper.FoDPagingHelper; import com.fortify.cli.fod._common.scan.helper.FoDOpenSourceScanType; import com.fortify.cli.fod._common.scan.helper.oss.FoDScanOssHelper; -import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; -import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import kong.unirest.HttpResponse; import kong.unirest.UnirestInstance; import lombok.Getter; -import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; @@ -50,34 +48,18 @@ public final class FoDOssComponentsListCommand extends AbstractFoDJsonNodeOutput private static final Logger LOG = LoggerFactory.getLogger(FoDOssComponentsListCommand.class); @Getter @Mixin private OutputHelperMixins.TableWithQuery outputHelper; @Mixin private FoDDelimiterMixin delimiterMixin; - @ArgGroup(exclusive = true, multiplicity = "1", order = 1) @Getter private TargetSpecifierArgGroup targetSpecifier = new TargetSpecifierArgGroup(); + @Mixin @Getter private FoDAppOrReleaseMixin appOrRelease; @Option(names = "--scan-types", required = true, split = ",", defaultValue = "Debricked") private FoDOpenSourceScanType[] scanTypes; - public static class TargetSpecifierArgGroup { - @ArgGroup(exclusive = false, multiplicity = "1", order = 1) @Getter private AppTarget app = new AppTarget(); - @ArgGroup(exclusive = false, multiplicity = "1", order = 2) @Getter private ReleaseTarget release = new ReleaseTarget(); - } - - public static class AppTarget extends FoDAppResolverMixin.AbstractFoDAppResolverMixin { - @Option(names = { "--app" }, required = true, descriptionKey = "fcli.fod.app.app-name-or-id") @Getter private String appNameOrId; - } - - public static class ReleaseTarget extends FoDReleaseByQualifiedNameOrIdResolverMixin.AbstractFoDQualifiedReleaseNameOrIdResolverMixin { - @Option(names = { "--release", "--rel" }, required = true, paramLabel = "id|app[:ms]:rel", descriptionKey = "fcli.fod.release.resolver.name-or-id") @Getter private String qualifiedReleaseNameOrId; - } - @Override public JsonNode getJsonNode(UnirestInstance unirest) { ArrayNode result = JsonHelper.getObjectMapper().createArrayNode(); - var appGroup = targetSpecifier.getApp(); - var releaseGroup = targetSpecifier.getRelease(); - - final String applicationId = (appGroup != null && appGroup.getAppNameOrId() != null) - ? appGroup.getAppId(unirest) + final String applicationId = appOrRelease.isAppSpecified() + ? appOrRelease.getAppId(unirest) : null; - final String releaseId = (releaseGroup != null && releaseGroup.getQualifiedReleaseNameOrId() != null) - ? releaseGroup.getReleaseId(unirest) + final String releaseId = appOrRelease.isReleaseSpecified() + ? appOrRelease.getReleaseId(unirest) : null; Stream.of(scanTypes) diff --git a/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java index ba27485aa7..a0cff2a4c4 100644 --- a/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java +++ b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommandEffectiveFastOutputTest.java @@ -23,6 +23,7 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.writer.record.RecordWriterStyle; import com.fortify.cli.common.output.writer.record.RecordWriterStyle.RecordWriterStyleElement; +import com.fortify.cli.fod._common.cli.mixin.FoDAppOrReleaseMixin; /** * Tests for FoDIssueListCommand.isEffectiveFastOutput logic after migration to style-based fast-output. @@ -38,6 +39,7 @@ void init() throws Exception { cmd = new FoDIssueListCommand(); streamingStub = new StreamingStubOutputHelper(); setField(cmd, "outputHelper", streamingStub); + setField(cmd, "appOrRelease", new FoDAppOrReleaseMixin()); } @Test @@ -89,13 +91,15 @@ private void setField(Object target, String fieldName, Object value) throws Exce } private void setApp(String app) throws Exception { - var target = cmd.getTargetSpecifier(); + var appOrRelease = cmd.getAppOrRelease(); + var target = appOrRelease.getFodAppOrReleaseArgGroup(); var appGroup = target.getApp(); setField(appGroup, "appNameOrId", app); } private void setRelease(String rel) throws Exception { - var target = cmd.getTargetSpecifier(); + var appOrRelease = cmd.getAppOrRelease(); + var target = appOrRelease.getFodAppOrReleaseArgGroup(); var releaseGroup = target.getRelease(); setField(releaseGroup, "qualifiedReleaseNameOrId", rel); }