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 {