From fa362948218fd9e823560ab517ab5fb6dea5e857 Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:23:51 +0100 Subject: [PATCH] chore: Partial data-extract implementation --- .../cli/cmd/AbstractActionGetCommand.java | 30 + .../cli/cmd/AbstractActionListCommand.java | 37 + .../cli/cmd/AbstractActionRunCommand.java | 87 +++ .../action/helper/ActionDescriptor.java | 448 ++++++++++++ .../common/action/helper/ActionHelper.java | 132 ++++ .../common/action/helper/ActionRunner.java | 646 ++++++++++++++++++ .../action/helper/ActionSpelFunctions.java | 157 +++++ .../common/cli/mixin/CommonOptionMixins.java | 169 +++++ .../cli/common/json/AbstractJsonWalker.java | 75 ++ .../common/json/JSONDateTimeConverter.java | 85 +++ .../fortify/cli/common/json/JsonHelper.java | 116 +++- .../output/standard/StandardOutputWriter.java | 9 +- .../progress/helper/ProgressWriterType.java | 18 +- .../cli/common/rest/paging/PagingHelper.java | 19 + .../IConfigurableSpelEvaluator.java | 21 + .../spring/expression/ISpelEvaluator.java | 20 + .../spring/expression/SpelEvaluator.java | 64 +- ...ctions.java => SpelFunctionsStandard.java} | 2 +- .../common/spring/expression/SpelHelper.java | 14 + .../expression/wrapper/SimpleExpression.java | 47 ++ .../wrapper/SimpleExpressionDeserializer.java | 55 ++ .../wrapper/TemplateExpression.java | 47 ++ .../TemplateExpressionDeserializer.java | 55 ++ .../expression/wrapper/WrappedExpression.java | 292 ++++++++ .../fortify/cli/common/util/FileUtils.java | 5 + .../fortify/cli/common/util/StringUtils.java | 2 +- .../json/JsonPropertyAccessor.java | 20 +- .../integration/json/package-info.java | 6 +- .../common/i18n/FortifyCLIMessages.properties | 4 + .../json/JsonNodeDeepCopyWalkerTest.java | 105 +++ fcli-core/fcli-fod/build.gradle | 6 + .../cmd/AbstractFoDScanFileUploadCommand.java | 1 + .../cli/cmd/AbstractFoDScanSetupCommand.java | 6 +- .../scan/cli/mixin/FoDDastFileTypeMixins.java | 1 + .../scan/helper/FoDFileUploadDescriptor.java | 1 + .../FoDScanAssessmentTypeDescriptor.java | 6 +- .../_common/scan/helper/FoDScanHelper.java | 27 +- .../_common/scan/helper/FoDScanStatus.java | 6 +- .../dast/FoDScanDastAutomatedHelper.java | 2 +- .../FoDScanDastAutomatedSetupBaseRequest.java | 8 +- ...DScanDastAutomatedSetupGraphQlRequest.java | 1 + .../FoDScanDastAutomatedSetupGrpcRequest.java | 1 + ...DScanDastAutomatedSetupOpenApiRequest.java | 1 + ...DScanDastAutomatedSetupPostmanRequest.java | 5 +- ...DScanDastAutomatedSetupWebsiteRequest.java | 14 +- ...ScanDastAutomatedSetupWorkflowRequest.java | 14 +- .../cli/fod/_main/cli/cmd/FoDCommands.java | 2 + .../fod/action/cli/cmd/FoDActionCommands.java | 28 + .../action/cli/cmd/FoDActionGetCommand.java | 26 + .../action/cli/cmd/FoDActionListCommand.java | 30 + .../action/cli/cmd/FoDActionRunCommand.java | 88 +++ .../FoDDastAutomatedScanGetConfigCommand.java | 1 + .../FoDDastAutomatedScanSetupApiCommand.java | 8 +- ...DDastAutomatedScanSetupWebsiteCommand.java | 7 +- ...DastAutomatedScanSetupWorkflowCommand.java | 5 +- .../cmd/FoDDastAutomatedScanStartCommand.java | 2 +- .../cmd/FoDDastLegacyScanStartCommand.java | 2 +- .../cli/cmd/FoDDastScanFileUploadCommand.java | 1 + .../FoDScanConfigDastAutomatedDescriptor.java | 1 + .../fod/report/cli/cmd/FoDReportCommands.java | 1 + .../cli/cmd/FoDReportCreateCommand.java | 2 +- .../cli/cmd/FoDReportDeleteCommand.java | 2 +- .../cli/cmd/FoDReportDownloadCommand.java | 6 +- .../report/cli/cmd/FoDReportGetCommand.java | 3 +- .../report/cli/cmd/FoDReportListCommand.java | 4 +- .../cli/cmd/FoDReportTemplateListCommand.java | 1 + .../cli/cmd/FoDReportWaitForCommand.java | 6 +- .../cli/mixin/FoDReportResolverMixin.java | 10 +- ...ReportTemplateByNameOrIdResolverMixin.java | 1 + .../report/helper/FoDReportCreateRequest.java | 7 +- .../report/helper/FoDReportDescriptor.java | 1 + .../fod/report/helper/FoDReportHelper.java | 1 + .../fod/report/helper/FoDReportStatus.java | 6 +- .../helper/FoDReportTemplateDescriptor.java | 1 + .../helper/FoDReportTemplateHelper.java | 7 +- .../actions/zip/bitbucket-sast-report.yaml | 108 +++ .../fod/actions/zip/github-sast-report.yaml | 126 ++++ .../fod/actions/zip/gitlab-dast-report.yaml | 150 ++++ .../fod/actions/zip/gitlab-sast-report.yaml | 104 +++ .../actions/zip/sonarqube-sast-report.yaml | 61 ++ .../cli/fod/i18n/FoDMessages.properties | 31 +- .../cli/data_extract/templates/SC-SAST.zip | Bin 200 -> 0 bytes .../sc_sast/i18n/SCSastMessages.properties | 3 +- fcli-core/fcli-ssc/build.gradle | 5 + .../rest/helper/SSCInputTransformer.java | 2 +- .../cli/ssc/_main/cli/cmd/SSCCommands.java | 2 + .../ssc/action/cli/cmd/SSCActionCommands.java | 28 + .../action/cli/cmd/SSCActionGetCommand.java | 26 + .../action/cli/cmd/SSCActionListCommand.java | 30 + .../action/cli/cmd/SSCActionRunCommand.java | 136 ++++ .../actions/zip/bitbucket-sast-report.yaml | 141 ++++ .../ssc/actions/zip/github-pr-decoration.yaml | 136 ++++ .../ssc/actions/zip/github-sast-report.yaml | 134 ++++ .../ssc/actions/zip/gitlab-dast-report.yaml | 155 +++++ .../actions/zip/gitlab-debricked-report.yaml | 115 ++++ .../ssc/actions/zip/gitlab-sast-report.yaml | 113 +++ .../actions/zip/gitlab-sonatype-report.yaml | 115 ++++ .../actions/zip/sonarqube-sast-report.yaml | 65 ++ .../cli/ssc/i18n/SSCMessages.properties | 9 +- fcli-other/fcli-bom/build.gradle | 6 +- fcli-other/fcli-gradle/fcli-java.gradle | 51 +- 101 files changed, 4885 insertions(+), 114 deletions(-) create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionGetCommand.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionListCommand.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionHelper.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSpelFunctions.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/AbstractJsonWalker.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JSONDateTimeConverter.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/IConfigurableSpelEvaluator.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/ISpelEvaluator.java rename fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/{StandardSpelFunctions.java => SpelFunctionsStandard.java} (98%) create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpression.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpressionDeserializer.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpression.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpressionDeserializer.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/WrappedExpression.java create mode 100644 fcli-core/fcli-common/src/test/java/com/fortify/cli/common/json/JsonNodeDeepCopyWalkerTest.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionCommands.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionGetCommand.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionListCommand.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionRunCommand.java create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/bitbucket-sast-report.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-sast-report.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-dast-report.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-sast-report.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sonarqube-sast-report.yaml delete mode 100644 fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/data_extract/templates/SC-SAST.zip create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionCommands.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionGetCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionListCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionRunCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bitbucket-sast-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-decoration.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-sast-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-dast-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-debricked-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sast-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sonatype-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sonarqube-sast-report.yaml diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionGetCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionGetCommand.java new file mode 100644 index 0000000000..35150b7804 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionGetCommand.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.common.action.cli.cmd; + +import com.fortify.cli.common.action.helper.ActionHelper; +import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; + +import picocli.CommandLine.Parameters; + +public abstract class AbstractActionGetCommand extends AbstractRunnableCommand implements Runnable { + @Parameters(arity="1", descriptionKey="fcli.action.run.action") private String action; + + @Override + public final void run() { + initMixins(); + System.out.println(ActionHelper.loadContents(getType(), action)); + } + + protected abstract String getType(); +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionListCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionListCommand.java new file mode 100644 index 0000000000..72305ebb71 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionListCommand.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.common.action.cli.cmd; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.action.helper.ActionHelper; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; +import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier; + +public abstract class AbstractActionListCommand extends AbstractOutputCommand implements IJsonNodeSupplier { + @Override + public final JsonNode getJsonNode() { + return ActionHelper.list(getType()) + .map(JsonHelper.getObjectMapper()::valueToTree) + .map(ObjectNode.class::cast) + .collect(JsonHelper.arrayNodeCollector()); + } + @Override + public final boolean isSingular() { + return false; + } + protected abstract String getType(); + + +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java new file mode 100644 index 0000000000..e5317c331c --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.common.action.cli.cmd; + +import java.util.List; + +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +import com.fortify.cli.common.action.helper.ActionDescriptor; +import com.fortify.cli.common.action.helper.ActionHelper; +import com.fortify.cli.common.action.helper.ActionRunner; +import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; +import com.fortify.cli.common.cli.mixin.CommonOptionMixins.OptionParametersMixin; +import com.fortify.cli.common.cli.mixin.CommonOptionMixins.OptionParametersMixin.OptionParameterHelper; +import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin; +import com.fortify.cli.common.progress.helper.IProgressWriterI18n; +import com.fortify.cli.common.util.DisableTest; +import com.fortify.cli.common.util.DisableTest.TestType; + +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +public abstract class AbstractActionRunCommand extends AbstractRunnableCommand implements Runnable { + @Parameters(arity="1", descriptionKey="fcli.action.run.action") private String action; + @Option(names="--action-help", required=false, arity = "0", descriptionKey="fcli.action.run.action-help") private boolean isActionHelpRequested; + @DisableTest({TestType.MULTI_OPT_SPLIT, TestType.MULTI_OPT_PLURAL_NAME, TestType.OPT_LONG_NAME}) + @Option(names="--", paramLabel="", descriptionKey="fcli.action.run.action-parameter") + private List dummyForSynopsis; + @Mixin private OptionParametersMixin actionParameters; + @Mixin private ProgressWriterFactoryMixin progressWriterFactory; + + @Override + public final void run() { + initMixins(); + Runnable delayedConsoleWriter = null; + try ( var progressWriter = progressWriterFactory.create() ) { + progressWriter.writeProgress("Loading action %s", action); + var actionDescriptor = ActionHelper.load(getType(), action); + try ( var actionRunner = ActionRunner.builder() + .action(actionDescriptor) + .inputParameters(actionParameters.getOptions()) + .progressWriter(progressWriter).build() ) + { + delayedConsoleWriter = run(actionDescriptor, actionRunner, progressWriter); + } + } + delayedConsoleWriter.run(); + } + + private Runnable run(ActionDescriptor actionDescriptor, ActionRunner actionRunner, IProgressWriterI18n progressWriter) { + actionRunner.getSpelEvaluator().configure(context->configure(actionRunner, context)); + OptionParameterHelper helper = actionParameters.getHelper(actionRunner::configureOptionParameterHelper); + if ( isActionHelpRequested ) { + String actionHelp = getActionHelp(actionDescriptor, helper); + return ()->{System.out.println(actionHelp);}; + } else { + helper.validate(); + progressWriter.writeProgress("Executing action %s", action); + return actionRunner.execute(); + } + } + + private final String getActionHelp(ActionDescriptor action, OptionParameterHelper helper) { + var usage = action.getUsage(); + return String.format( + "\nAction: %s\n"+ + "\n%s\n"+ + "\n%s\n"+ + "\nAction options:\n"+ + "%s", + action.getName(), usage.getHeader(), usage.getDescription(), helper.getSupportedOptionsTable()); + } + + protected abstract String getType(); + protected abstract void configure(ActionRunner actionRunner, SimpleEvaluationContext context); +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java new file mode 100644 index 0000000000..28a9f751de --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java @@ -0,0 +1,448 @@ +/** + * Copyright 2023 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.common.action.helper; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.springframework.expression.ParseException; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.node.ValueNode; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.json.JsonHelper.AbstractJsonNodeWalker; +import com.fortify.cli.common.spring.expression.SpelHelper; +import com.fortify.cli.common.spring.expression.wrapper.SimpleExpression; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; +import com.fortify.cli.common.util.StringUtils; + +import kong.unirest.HttpMethod; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * This class describes an action deserialized from an action YAML file, + * containing elements describing: + *
    + *
  • Action metadata like name and description
  • + *
  • Action parameters
  • + *
  • Steps to be executed, like executing REST requests and writing output
  • + *
  • Data formatters
  • + *
+ * + * @author Ruud Senden + */ +@Reflectable @NoArgsConstructor +@Data +public class ActionDescriptor { + /** Action name, set in {@link #postLoad(String)} method */ + private String name; + /** Action description */ + private ActionUsageDescriptor usage; + /** Action parameters */ + private List parameters; + /** Additional requests targets */ + private List addRequestTargets; + /** Default values for certain action properties */ + private ActionDefaultValuesDescriptor defaults; + /** Action steps, evaluated in the order as defined in the YAML file */ + private List steps; + /** Partial outputs */ + private List partialOutputs; + /** Full outputs */ + private List outputs; + /** Output writers */ + private List outputWriters; + @JsonIgnore @Getter(lazy=true) private final Map partialOutputsByName = + partialOutputs.stream().collect(Collectors.toMap(ActionOutputDescriptor::getName, Function.identity())); + + /** + * This method is invoked by ActionHelper after deserializing + * an instance of this class from a YAML file. It performs some additional + * initialization and validation. + */ + final void postLoad(String name) { + this.name = name; + checkNotNull("action usage", usage, this); + checkNotNull("action steps", steps, this); + checkNotNull("action outputs", outputs, this); + usage.postLoad(this); + if ( parameters!=null ) { parameters.forEach(d->d.postLoad(this)); } + if ( addRequestTargets!=null ) { addRequestTargets.forEach(d->d.postLoad(this)); } + steps.forEach(d->d.postLoad(this)); + if ( partialOutputs!=null ) { + partialOutputs.forEach(d->d.postLoad(this)); + } + outputs.forEach(d->d.postLoad(this)); + outputWriters.forEach(d->d.postLoad(this)); + } + + private static final void check(boolean isFailure, Object entity, Supplier msgSupplier) { + if ( isFailure ) { + throw new ActionValidationException(msgSupplier.get(), entity); + } + } + + /** + * Utility method for checking whether the given value is not blank, throwing an + * exception otherwise. + * @param property Descriptive name of the YAML property being checked + * @param value Value to be checked for not being blank + * @param entity The object containing the property to be checked + */ + private static final void checkNotBlank(String property, String value, Object entity) { + check(StringUtils.isBlank(value), entity, ()->String.format("Action %s property must be specified", property)); + } + + /** + * Utility method for checking whether the given value is not null, throwing an + * exception otherwise. + * @param property Descriptive name of the YAML property being checked + * @param value Value to be checked for not being null + * @param entity The object containing the property to be checked + */ + private static final void checkNotNull(String property, Object value, Object entity) { + check(value==null, entity, ()->String.format("Action %s property must be specified", property)); + } + + /** + * This class describes action usage header and description. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionUsageDescriptor { + /** Required usage header */ + private String header; + /** Required usage description */ + private String description; + + public void postLoad(ActionDescriptor action) { + checkNotBlank("usage header", header, this); + checkNotBlank("usage description", description, this); + } + } + + /** + * This class describes a request target. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionRequestTargetDescriptor { + /** Required name */ + private String name; + /** Required base URL */ + private TemplateExpression baseUrl; + /** Optional headers */ + private Map headers; + // TODO Add support for next page URL producer + // TODO ? Add proxy support ? + + public final void postLoad(ActionDescriptor action) { + checkNotBlank("request target name", name, this); + checkNotNull("request target base URL", baseUrl, this); + } + } + + /** + * This class describes default values for various action properties. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionDefaultValuesDescriptor { + /** Default value for {@link ActionStepRequestDescriptor#from} */ + private String requestTarget; + } + + /** + * This class describes a action parameter. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionParameterDescriptor { + /** Required parameter name */ + private String name; + /** Required parameter description */ + private String description; + /** Optional parameter type */ + private String type; + /** Optional type parameters*/ + private Map typeParameters; + /** Optional action expression defining the default parameter value if not provided by user */ + private TemplateExpression defaultValue; + /** Boolean indicating whether this parameter is required */ + private boolean required = true; + + public final void postLoad(ActionDescriptor action) { + checkNotBlank("parameter name", name, this); + checkNotNull("parameter description", getDescription(), this); + } + } + + /** + * This class describes a single action step element, which may contain + * requests, progress message, and/or set instructions. This class is + * used for both top-level step elements, and step elements in forEach elements. + * Potentially, later versions may add support for other step types. + * + * @author Ruud Senden + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepDescriptor { + /** Optional if-expression, executing this step only if condition evaluates to true */ + @JsonProperty("if") private SimpleExpression _if; + /** Optional requests for this step element */ + private List requests; + /** Optional progress message action expression for this step element */ + private TemplateExpression progress; + /** Optional set operations */ + private List set; + /** Optional updatePartialOutput operations */ + private List updatePartialOutputs; + + /** + * This method is invoked by the parent element (which may either be another + * step element, or the top-level {@link ActionDescriptor} instance). + * It invokes the postLoad() method on each request descriptor. + */ + public final void postLoad(ActionDescriptor action) { + if ( requests!=null ) { requests.forEach(d->d.postLoad(action)); } + if ( set!=null ) { set.forEach(d->d.postLoad(action)); } + if ( updatePartialOutputs!=null ) { updatePartialOutputs.forEach(d->d.postLoad(action)); } + } + } + + /** + * This class describes a forEach element, allowing iteration over the output of + * the parent element, like the response of a REST request or the contents of a + * action parameter. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepForEachDescriptor { + /** Required name for this step element */ + private String name; + /** Optional requests for which to embed the response in each forEach node */ + private List embed; + /** Optional if-expression, executing steps only if condition evaluates to true */ + @JsonProperty("if") private SimpleExpression _if; + /** Optional break-expression, terminating forEach if condition evaluates to true */ + private SimpleExpression breakIf; + /** Steps to be repeated for each value */ + @JsonProperty("do") private List _do; + + /** + * This method is invoked by the {@link ActionStepDescriptor#postLoad()} + * method. It checks that required properties are set, then calls the postLoad() method for + * each sub-step. + */ + public final void postLoad(ActionDescriptor action) { + checkNotBlank("forEach name", name, this); + checkNotNull("forEach do", _do, this); + if ( embed!=null ) { embed.forEach(d->d.postLoad(action)); } + _do.forEach(d->d.postLoad(action)); + } + } + + /** + * This class describes an operation to explicitly set a data property. + * Note that data properties for request outputs are set automatically. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepSetDescriptor { + /** Required name for this step element */ + private String name; + /** Required value action expression for this step element */ + private TemplateExpression value; + + public void postLoad(ActionDescriptor action) { + checkNotBlank("set name", name, this); + checkNotNull("set value", value, this); + } + } + + /** + * This class describes an operation to explicitly set a data property. + * Note that data properties for request outputs are set automatically. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepUpdatePartialOutputsDescriptor { + /** Required partial output name; must match a partial output definition */ + private String name; + + public void postLoad(ActionDescriptor action) { + checkNotBlank("set name", name, this); + check(!action.getPartialOutputsByName().containsKey(name), this, + ()->String.format("No partial output named %s", name)); + } + } + + /** + * This class describes a REST request. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepRequestDescriptor { + /** Required name for this step element */ + private String name; + /** Optional HTTP method, defaults to 'GET' */ + private HttpMethod method = HttpMethod.GET; + /** Required action expression defining the request URI from which to get the data */ + private TemplateExpression uri; + /** Required target to which to send the request; may be specified here or through defaults.requestTarget */ + private String target; + /** Map defining (non-encoded) request query parameters; parameter values are defined as action expressions */ + private Map query; + /** Optional request body action expression */ + private TemplateExpression body; + /** Type of request; either 'simple' or 'paged' for now */ + private ActionStepRequestType type = ActionStepRequestType.simple; + /** Optional if-expression, executing this request only if condition evaluates to true */ + @JsonProperty("if") private SimpleExpression _if; + /** Optional progress messages for various stages of request processing */ + private ActionStepRequestPagingProgressDescriptor pagingProgress; + /** Optional forEach block to be repeated for every response element */ + private ActionStepForEachDescriptor forEach; + + /** + * This method is invoked by {@link ActionStepDescriptor#postLoad()} + * method. It checks that required properties are set. + */ + protected final void postLoad(ActionDescriptor action) { + checkNotBlank("request name", name, this); + checkNotNull("request uri", uri, this); + if ( StringUtils.isBlank(target) && action.defaults!=null ) { + target = action.defaults.requestTarget; + } + checkNotBlank("request target", target, this); + if ( pagingProgress!=null ) { + type = ActionStepRequestType.paged; + } + if ( forEach!=null ) { + forEach.postLoad(action); + } + } + + public static enum ActionStepRequestType { + simple, paged + } + + @Data + public static final class ActionStepRequestPagingProgressDescriptor { + private TemplateExpression prePageLoad; + private TemplateExpression postPageLoad; + private TemplateExpression postPageProcess; + } + } + + /** + * This class describes an output, which can be either a top-level output + * or partial output. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionOutputDescriptor { + /** Required name for this output */ + private String name; + /** Output contents in JSON format, where each text node is assumed to be a action expression */ + private JsonNode contents; + /** Cached mapping from text node property path to corresponding TemplateExpression instance */ + private final Map valueExpressions = new LinkedHashMap<>(); + + /** + * This method checks whether required name and contents are not blank or null, then + * walks the given contents to parse each text node as a {@link TemplateExpression}, + * caching the resulting {@link TemplateExpression} instance in the {@link #valueExpressions} + * map, throwing an exception if the text node cannot be parsed as a {@link TemplateExpression}. + */ + public final void postLoad(ActionDescriptor action) { + checkNotBlank("(partial) output name", name, this); + checkNotNull("(partial) output contents", contents, this); + new ContentsWalker().walk(contents); + } + + private final class ContentsWalker extends AbstractJsonNodeWalker { + @Override + protected Void getResult() { return null; } + @Override + protected void walkValue(Void state, String path, JsonNode parent, ValueNode node) { + if ( node instanceof TextNode ) { + var expr = node.asText(); + try { + valueExpressions.put(path, SpelHelper.parseTemplateExpression(expr)); + } catch (ParseException e) { + throw new ActionValidationException(String.format("Error parsing action expression '%s'", expr), ActionOutputDescriptor.this, e); + } + } + super.walkValue(state, path, parent, node); + } + } + } + + /** + * This class describes an output writer. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionOutputWriterDescriptor { + /** Required output name for this step element */ + private String outputName; + /** Required action expression defining where to write the data, either stdout, stderr or filename */ + private TemplateExpression to; + + public void postLoad(ActionDescriptor action) { + checkNotBlank("output writer outputName", outputName, this); + action.getOutputs().stream() + .filter(d->outputName.equals(d.getName())) + .findFirst() + .orElseThrow(()->new ActionValidationException(String.format("No output with name %s available", outputName), this)); + } + } + + /** + * Exception class used for action validation errors. + */ + public static final class ActionValidationException extends IllegalStateException { + private static final long serialVersionUID = 1L; + + public ActionValidationException(String message, Throwable cause) { + super(message, cause); + } + + public ActionValidationException(String message, Object actionElement, Throwable cause) { + this(getMessageWithEntity(message, actionElement), cause); + } + + public ActionValidationException(String message) { + super(message); + } + + public ActionValidationException(String message, Object actionElement) { + this(getMessageWithEntity(message, actionElement)); + } + + private static final String getMessageWithEntity(String message, Object actionElement) { + return String.format("%s (entity: %s)", message, actionElement.toString()); + } + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionHelper.java new file mode 100644 index 0000000000..65b596fb03 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionHelper.java @@ -0,0 +1,132 @@ +/** + * Copyright 2023 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.common.action.helper; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fortify.cli.common.util.FileUtils; + +// TODO Clean up this class +// TODO Allow for loading actions from external file/URL +public class ActionHelper { + private static final ObjectMapper yamlObjectMapper = new ObjectMapper(new YAMLFactory()); + private ActionHelper() {} + + public static final ActionDescriptor load(String type, String name) { + AtomicReference result = new AtomicReference<>(); + processZipEntries(type, (zis, ze)->{ + var fileName = name+".yaml"; + if (ze.getName().equals(fileName)) { + result.set(load(fileName, zis)); + return false; + } else { + return true; + } + }); + if ( result.get()==null ) { + throw new IllegalArgumentException("No action found with name "+name); + } + return result.get(); + } + + public static final String loadContents(String type, String name) { + AtomicReference result = new AtomicReference<>(); + processZipEntries(type, (zis, ze)->{ + var fileName = name+".yaml"; + if (ze.getName().equals(fileName)) { + result.set(FileUtils.readInputStreamAsString(zis, StandardCharsets.US_ASCII)); + return false; + } else { + return true; + } + }); + if ( result.get()==null ) { + throw new IllegalArgumentException("No action found with name "+name); + } + return result.get(); + } + + public static final Stream list(String type) { + List result = new ArrayList<>(); + processZipEntries(type, (zis, ze)->{ + result.add(loadAsJson(ze.getName(), zis)); + return true; + }); + return result.stream() + .sorted((a,b)->a.get("name").asText().compareTo(b.get("name").asText())); + } + + private static final ActionDescriptor load(String fileName, InputStream is) { + if ( is==null ) { + // TODO Use more descriptive exception message + throw new IllegalStateException("Can't read "+fileName); + } + try { + var result = yamlObjectMapper.readValue(is, ActionDescriptor.class); + result.postLoad(getActionName(fileName)); + return result; + } catch (IOException e) { + throw new RuntimeException("Error loading action "+fileName, e); + } + } + + private static final ObjectNode loadAsJson(String fileName, InputStream is) { + if ( is==null ) { + // TODO Use more descriptive exception message + throw new IllegalStateException("Can't read "+fileName); + } + try { + var result = yamlObjectMapper.createObjectNode().put("name", getActionName(fileName)); + result.setAll(yamlObjectMapper.readValue(is.readAllBytes(), ObjectNode.class)); + return result; + } catch (IOException e) { + throw new RuntimeException("Error loading action "+fileName, e); + } + } + + private static final String getActionName(String fileName) { + return Path.of(fileName).getFileName().toString().replace(".yaml", ""); + } + + private static final void processZipEntries(String type, BiFunction processor) { + try ( InputStream is = getActionsZipInputStream(type); ZipInputStream zis = new ZipInputStream(is) ) { + ZipEntry entry; + while ( (entry = zis.getNextEntry())!=null ) { + if ( !processor.apply(zis, entry) ) { break; } + } + } catch (IOException e) { + throw new RuntimeException("Error loading actions", e); + } + } + + private static final InputStream getActionsZipInputStream(String type) throws IOException { + return FileUtils.getResourceInputStream(getActionsResourceZip(type)); + } + + private static final String getActionsResourceZip(String type) { + return String.format("com/fortify/cli/%s/actions.zip", type.toLowerCase().replace('-', '_')); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java new file mode 100644 index 0000000000..621b36a6dd --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java @@ -0,0 +1,646 @@ +/** + * Copyright 2023 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.common.action.helper; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.FloatNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.node.ValueNode; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionOutputDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionOutputWriterDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionParameterDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionRequestTargetDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepForEachDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepRequestDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepRequestDescriptor.ActionStepRequestPagingProgressDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepRequestDescriptor.ActionStepRequestType; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepSetDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepUpdatePartialOutputsDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionValidationException; +import com.fortify.cli.common.action.helper.ActionRunner.IActionRequestHelper.BasicActionRequestHelper; +import com.fortify.cli.common.action.helper.ActionRunner.IActionRequestHelper.ActionRequestDescriptor; +import com.fortify.cli.common.cli.mixin.CommonOptionMixins.OptionParametersMixin.OptionParameterHelper; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.json.JsonHelper.JsonNodeDeepCopyWalker; +import com.fortify.cli.common.output.product.IProductHelper; +import com.fortify.cli.common.output.transform.IInputTransformer; +import com.fortify.cli.common.progress.helper.IProgressWriterI18n; +import com.fortify.cli.common.rest.paging.INextPageUrlProducer; +import com.fortify.cli.common.rest.paging.INextPageUrlProducerSupplier; +import com.fortify.cli.common.rest.paging.PagingHelper; +import com.fortify.cli.common.rest.unirest.GenericUnirestFactory; +import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; +import com.fortify.cli.common.rest.unirest.config.UnirestJsonHeaderConfigurer; +import com.fortify.cli.common.rest.unirest.config.UnirestUnexpectedHttpResponseConfigurer; +import com.fortify.cli.common.spring.expression.IConfigurableSpelEvaluator; +import com.fortify.cli.common.spring.expression.ISpelEvaluator; +import com.fortify.cli.common.spring.expression.SpelEvaluator; +import com.fortify.cli.common.spring.expression.SpelHelper; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; +import com.fortify.cli.common.util.JavaHelper; +import com.fortify.cli.common.util.StringUtils; + +import kong.unirest.HttpRequest; +import kong.unirest.UnirestInstance; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Builder +public class ActionRunner implements AutoCloseable { + /** Jackson {@link ObjectMapper} used for various JSON-related operations */ + private static final ObjectMapper objectMapper = JsonHelper.getObjectMapper(); + /** Progress writer, provided through builder method */ + private final IProgressWriterI18n progressWriter; + /** Data extract action, provided through builder method */ + private final ActionDescriptor action; + /** Action input parameters, provided through builder method */ + private final Map inputParameters; + /** ObjectNode holding global data values as produced by the various steps */ + @Getter private final ObjectNode globalData = objectMapper.createObjectNode(); + /** ObjectNode holding parameter values as generated by ActionParameterProcessor */ + @Getter private final ObjectNode parameters = objectMapper.createObjectNode(); + /** ObjectNode holding partial outputs as generated by ActionStepsProcessor */ + private final ObjectNode partialOutputs = objectMapper.createObjectNode(); + /** ObjectNode holding final outputs as generated by ActionOutputProcessor */ + private final ObjectNode outputs = objectMapper.createObjectNode(); + /** SpEL evaluator configured with {@link ActionSpelFunctions} and variables for + * parameters, partialOutputs and outputs as defined above */ + @Getter private final IConfigurableSpelEvaluator spelEvaluator = SpelEvaluator.JSON_GENERIC.copy().configure(this::configureSpelEvaluator); + /** Parameter converters as generated by {@link #createDefaultParameterConverters()} amended by + * custom converters as added through the {@link #addParameterConverter(String, BiFunction)} and + * {@link #addParameterConverter(String, Function)} methods. */ + private final Map> parameterConverters = createDefaultParameterConverters(); + /** Request helpers as configured through the {@link #addRequestHelper(String, IActionRequestHelper)} method */ + private final Map requestHelpers = new HashMap<>(); + + public final Runnable execute() { + globalData.set("parameters", parameters); + globalData.set("partialOutputs", partialOutputs); + globalData.set("outputs", outputs); + progressWriter.writeProgress("Processing action parameters"); + new ActionParameterProcessor().processParameters(); + new ActionAddRequestTargetsProcessor().addRequestTargets(); + progressWriter.writeProgress("Processing action steps"); + new ActionStepsProcessor(globalData).processSteps(); + progressWriter.writeProgress("Producing action outputs"); + new ActionOutputProcessor(globalData).processOutputs(); + var delayedConsoleWriterRunnables = new ActionOutputWriterProcessor(globalData).processOutputWriters(); + progressWriter.writeProgress("Action processing finished"); + return delayedConsoleWriterRunnables; + } + + public final void close() { + requestHelpers.values().forEach(IActionRequestHelper::close); + } + + public final void configureOptionParameterHelper(OptionParameterHelper validator) { + validator.setUnknownOptionFormat("Unknown action parameter: %s"); + validator.setValidationExceptionFormat("Invalid action parameter:\n %s\nSupported action parameters:\n%s\n"); + action.getParameters().forEach(p->configureOptionParameterHelper(validator, p)); + } + + private final void configureOptionParameterHelper(OptionParameterHelper validator, ActionParameterDescriptor parameter) { + validator.addSupportedOption(parameter.getName(), parameter.getDescription()); + if ( parameter.isRequired() ) { + validator.requiredOption(parameter.getName(), "Missing action parameter: %s"); + } + } + + private final void configureSpelEvaluator(SimpleEvaluationContext context) { + SpelHelper.registerFunctions(context, ActionSpelFunctions.class); + } + + public final ActionRunner addParameterConverter(String type, BiFunction converter) { + parameterConverters.put(type, converter); + return this; + } + public final ActionRunner addParameterConverter(String type, Function converter) { + parameterConverters.put(type, (v,a)->converter.apply(v)); + return this; + } + public final ActionRunner addRequestHelper(String name, IActionRequestHelper requestHelper) { + requestHelpers.put(name, requestHelper); + return this; + } + + private IActionRequestHelper getRequestHelper(String name) { + if ( StringUtils.isBlank(name) ) { + if ( requestHelpers.size()==1 ) { + return requestHelpers.values().iterator().next(); + } else { + throw new IllegalStateException(String.format("Required 'from:' property (allowed values: %s) missing", requestHelpers.keySet())); + } + } + var result = requestHelpers.get(name); + if ( result==null ) { + throw new IllegalStateException(String.format("Invalid 'from: %s', allowed values: %s", name, requestHelpers.keySet())); + } + return result; + } + + private static final JsonNode getOutput(ISpelEvaluator spelEvaluator, ActionOutputDescriptor outputDescriptor, ObjectNode data) { + var outputRawContents = outputDescriptor.getContents(); + var output = new JsonNodeOutputWalker(spelEvaluator, outputDescriptor, data).walk(outputRawContents); + return output; + } + + private final Map evaluateTemplateExpressionMap(Map queryExpressions, ObjectNode data, Class targetClass) { + Map result = new LinkedHashMap<>(); + if ( queryExpressions!=null ) { + queryExpressions.entrySet().forEach(e->result.put(e.getKey(), spelEvaluator.evaluate(e.getValue(), data, targetClass))); + } + return result; + } + + private final class ActionParameterProcessor { + private final void processParameters() { + action.getParameters().forEach(this::processParameter); + } + + private final void processParameter(ActionParameterDescriptor parameter) { + var name = parameter.getName(); + var value = inputParameters.get(name); + if ( value==null ) { + var defaultValueExpression = parameter.getDefaultValue(); + value = defaultValueExpression==null + ? null + : spelEvaluator.evaluate(defaultValueExpression, parameters, String.class); + } + parameters.set(name, convertParameterValue(value, parameter)); + } + + private JsonNode convertParameterValue(String value, ActionParameterDescriptor parameter) { + var name = parameter.getName(); + var type = StringUtils.isBlank(parameter.getType()) ? "string" : parameter.getType(); + var paramConverter = parameterConverters.get(type); + if ( paramConverter==null ) { + throw new ActionValidationException(String.format("Unknown parameter type %s for parameter %s", type, name)); + } else { + var args = ParameterTypeConverterArgs.builder() + .progressWriter(progressWriter) + .spelEvaluator(spelEvaluator) + .action(action) + .parameter(parameter) + .parameters(parameters) + .build(); + var result = paramConverter.apply(value, args); + return result==null ? NullNode.instance : result; + } + } + } + + private final class ActionAddRequestTargetsProcessor { + private final void addRequestTargets() { + var requestTargets = action.getAddRequestTargets(); + if ( requestTargets!=null ) { + requestTargets.forEach(this::addRequestTarget); + } + } + private void addRequestTarget(ActionRequestTargetDescriptor descriptor) { + requestHelpers.put(descriptor.getName(), createBasicRequestHelper(descriptor)); + } + + private IActionRequestHelper createBasicRequestHelper(ActionRequestTargetDescriptor descriptor) { + var name = descriptor.getName(); + var baseUrl = spelEvaluator.evaluate(descriptor.getBaseUrl(), globalData, String.class); + var headers = evaluateTemplateExpressionMap(descriptor.getHeaders(), globalData, String.class); + IUnirestInstanceSupplier unirestInstanceSupplier = () -> GenericUnirestFactory.getUnirestInstance(name, u->{ + u.config().defaultBaseUrl(baseUrl).getDefaultHeaders().add(headers); + UnirestUnexpectedHttpResponseConfigurer.configure(u); + UnirestJsonHeaderConfigurer.configure(u); + }); + return new BasicActionRequestHelper(unirestInstanceSupplier, null); + } + } + + @RequiredArgsConstructor + private final class ActionStepsProcessor { + private final ObjectNode data; + + private final ObjectNode processSteps() { + processSteps(action.getSteps()); + return data; + } + + private final void processSteps(List steps) { + if ( steps!=null ) { steps.forEach(this::processStep); } + } + + private final void processStep(ActionStepDescriptor step) { + if ( step.get_if()==null || spelEvaluator.evaluate(step.get_if(), data, Boolean.class) ) { + process(step::getProgress, this::processProgress); + process(step::getRequests, this::processRequests); + processAll(step::getSet, this::processSet); + processAll(step::getUpdatePartialOutputs, this::processUpdatePartialOutput); + } + } + + private void processAll(Supplier> supplier, Consumer consumer) { + var list = supplier.get(); + if ( list!=null ) { + list.forEach(consumer); + } + } + + private void process(Supplier supplier, Consumer consumer) { + var value = supplier.get(); + if ( value!=null ) { + consumer.accept(value); + } + } + + private void processSet(ActionStepSetDescriptor set) { + var value = spelEvaluator.evaluate(set.getValue(), data, Object.class); + globalData.set(set.getName(), objectMapper.valueToTree(value)); + } + + private void processUpdatePartialOutput(ActionStepUpdatePartialOutputsDescriptor updatePartialOutput) { + var outputDescriptor = action.getPartialOutputsByName().get(updatePartialOutput.getName()); + var partialOutputName = outputDescriptor.getName(); + var partialOutput = getOutput(spelEvaluator, outputDescriptor, data); + var partialOutputArray = (ArrayNode)partialOutputs.get(partialOutputName); + if ( partialOutputArray==null ) { + partialOutputArray = objectMapper.createArrayNode(); + partialOutputs.set(partialOutputName, partialOutputArray); + } + partialOutputArray.add(partialOutput); + } + + private void processProgress(TemplateExpression progress) { + progressWriter.writeProgress(spelEvaluator.evaluate(progress, data, String.class)); + } + + private void processRequests(List requests) { + if ( requests!=null ) { + var requestsProcessor = new ActionStepRequestsProcessor(); + requestsProcessor.addRequests(requests, this::processResponse, data); + requestsProcessor.executeRequests(); + } + } + + private final void processResponse(ActionStepRequestDescriptor requestDescriptor, JsonNode rawBody) { + var name = requestDescriptor.getName(); + var body = getRequestHelper(requestDescriptor.getTarget()).transformInput(rawBody); + data.set(name+"_raw", rawBody); + data.set(name, body); + processForEach(requestDescriptor); + } + + private final void processForEach(ActionStepRequestDescriptor requestDescriptor) { + var forEach = requestDescriptor.getForEach(); + if ( forEach!=null ) { + var input = data.get(requestDescriptor.getName()); + if ( input!=null ) { + if ( input instanceof ArrayNode ) { + updateForEachTotalCount(forEach, (ArrayNode)input); + processForEachEmbed(forEach, (ArrayNode)input); + processForEach(forEach, (ArrayNode)input, this::processForEachEntryDo); + } else { + throw new ActionValidationException("forEach not supported on node type "+input.getNodeType()); + } + } + } + } + + private final void processForEachEmbed(ActionStepForEachDescriptor forEach, ArrayNode source) { + var requestExecutor = new ActionStepRequestsProcessor(); + processForEach(forEach, source, getForEachEntryEmbedProcessor(requestExecutor)); + requestExecutor.executeRequests(); + } + + @FunctionalInterface + private interface IForEachEntryProcessor { + void process(ActionStepForEachDescriptor forEach, JsonNode currentNode, ObjectNode newData); + } + + private final void processForEach(ActionStepForEachDescriptor forEach, ArrayNode source, IForEachEntryProcessor entryProcessor) { + for ( int i = 0 ; i < source.size(); i++ ) { + var currentNode = source.get(i); + var newData = JsonHelper.shallowCopy(data); + newData.set(forEach.getName(), currentNode); + var breakIf = forEach.getBreakIf(); + if ( breakIf!=null && spelEvaluator.evaluate(breakIf, newData, Boolean.class) ) { + break; + } + var _if = forEach.get_if(); + if ( _if==null || spelEvaluator.evaluate(_if, newData, Boolean.class) ) { + entryProcessor.process(forEach, currentNode, newData); + } + } + } + + private void updateForEachTotalCount(ActionStepForEachDescriptor forEach, ArrayNode array) { + var totalCountName = String.format("total%sCount", StringUtils.capitalize(forEach.getName())); + var totalCount = data.get(totalCountName); + if ( totalCount==null ) { totalCount = new IntNode(0); } + data.put(totalCountName, totalCount.asInt()+array.size()); + } + + private void processForEachEntryDo(ActionStepForEachDescriptor forEach, JsonNode currentNode, ObjectNode newData) { + var processor = new ActionStepsProcessor(newData); + processor.processSteps(forEach.get_do()); + } + + private IForEachEntryProcessor getForEachEntryEmbedProcessor(ActionStepRequestsProcessor requestExecutor) { + return (forEach, currentNode, newData) -> { + if ( !currentNode.isObject() ) { + // TODO Improve exception message? + throw new IllegalStateException("Cannot embed data on non-object nodes: "+forEach.getName()); + } + requestExecutor.addRequests(forEach.getEmbed(), (rd,r)-> + ((ObjectNode)currentNode).set(rd.getName(), getRequestHelper(rd.getTarget()).transformInput(r)), newData); + }; + } + } + + private final class ActionStepRequestsProcessor { + private final Map> simpleRequests = new LinkedHashMap<>(); + private final Map> pagedRequests = new LinkedHashMap<>(); + + private final void addRequests(List requestDescriptors, BiConsumer responseConsumer, ObjectNode data) { + if ( requestDescriptors!=null ) { + requestDescriptors.forEach(r->addRequest(r, responseConsumer, data)); + } + } + + private final void addRequest(ActionStepRequestDescriptor requestDescriptor, BiConsumer responseConsumer, ObjectNode data) { + var _if = requestDescriptor.get_if(); + if ( _if==null || spelEvaluator.evaluate(_if, data, Boolean.class) ) { + var uri = spelEvaluator.evaluate(requestDescriptor.getUri(), data, String.class); + var query = evaluateTemplateExpressionMap(requestDescriptor.getQuery(), data, Object.class); + var body = requestDescriptor.getBody()==null ? null : spelEvaluator.evaluate(requestDescriptor.getBody(), data, Object.class); + var requestData = new IActionRequestHelper.ActionRequestDescriptor(requestDescriptor.getMethod().toString(), uri, query, body, r->responseConsumer.accept(requestDescriptor, r)); + addPagingProgress(requestData, requestDescriptor.getPagingProgress(), data); + if ( requestDescriptor.getType()==ActionStepRequestType.paged ) { + pagedRequests.computeIfAbsent(requestDescriptor.getTarget(), s->new ArrayList()).add(requestData); + } else { + simpleRequests.computeIfAbsent(requestDescriptor.getTarget(), s->new ArrayList()).add(requestData); + } + } + } + + private void addPagingProgress(ActionRequestDescriptor requestData, ActionStepRequestPagingProgressDescriptor pagingProgress, ObjectNode data) { + if ( pagingProgress!=null ) { + addPagingProgress(pagingProgress.getPrePageLoad(), requestData::setPrePageLoad, data); + addPagingProgress(pagingProgress.getPostPageLoad(), requestData::setPostPageLoad, data); + addPagingProgress(pagingProgress.getPostPageProcess(), requestData::setPostPageProcess, data); + } + } + + private void addPagingProgress(TemplateExpression expr, Consumer consumer, ObjectNode data) { + if ( expr!=null ) { + consumer.accept(()->progressWriter.writeProgress(spelEvaluator.evaluate(expr, data, String.class))); + } + } + + private final void executeRequests() { + simpleRequests.entrySet().forEach(e->executeRequest(e.getKey(), e.getValue(), false)); + pagedRequests.entrySet().forEach(e->executeRequest(e.getKey(), e.getValue(), true)); + } + + private void executeRequest(String target, List requests, boolean isPaged) { + var requestHelper = getRequestHelper(target); + if ( isPaged ) { + requests.forEach(r->requestHelper.executePagedRequest(r)); + } else { + requestHelper.executeSimpleRequests(requests); + } + } + } + + @RequiredArgsConstructor + private final class ActionOutputProcessor { + private final ObjectNode data; + + private final void processOutputs() { + action.getOutputs().forEach(this::processOutput); + } + + private final void processOutput(ActionOutputDescriptor outputDescriptor) { + outputs.set(outputDescriptor.getName(), getOutput(spelEvaluator, outputDescriptor, data)); + } + } + + @RequiredArgsConstructor + private final class ActionOutputWriterProcessor { + private final ObjectNode data; + // We need to delay writing output to console as to not interfere with progress writer + private final List delayedConsoleWriterRunnables = new ArrayList<>(); + + private final Runnable processOutputWriters() { + action.getOutputWriters().forEach(this::processOutputWriter); + return ()->delayedConsoleWriterRunnables.forEach(Runnable::run); + } + + private final void processOutputWriter(ActionOutputWriterDescriptor outputWriterDescriptor) { + var outputName = outputWriterDescriptor.getOutputName(); + var to = spelEvaluator.evaluate(outputWriterDescriptor.getTo(), data, String.class); + var output = outputs.get(outputName); + try { + switch (to.toLowerCase()) { + case "stdout": delayedConsoleWriterRunnables.add(createRunner(System.out, output)); break; + case "stderr": delayedConsoleWriterRunnables.add(createRunner(System.err, output)); break; + default: write(new File(to), output); + } + } catch (IOException e) { + throw new RuntimeException("Error writing action output to "+to); + } + } + + private Runnable createRunner(PrintStream out, JsonNode output) { + String outputString = asString(output); + return ()->out.print(outputString); + } + + private void write(File file, JsonNode output) throws IOException { + try ( var out = new PrintStream(file) ) { + out.println(asString(output)); + } + } + + private final String asString(JsonNode output) { + if ( output instanceof TextNode ) { + return output.asText(); + } else { + return output.toPrettyString(); + } + } + } + + public static interface IActionRequestHelper extends AutoCloseable { + public JsonNode transformInput(JsonNode input); + public void executePagedRequest(ActionRequestDescriptor requestDescriptor); + public void executeSimpleRequests(List requestDescriptor); + public void close(); + + @Data + public static final class ActionRequestDescriptor { + private final String method; + private final String uri; + private final Map queryParams; + private final Object body; + private final Consumer responseConsumer; + private Runnable prePageLoad; + private Runnable postPageLoad; + private Runnable postPageProcess; + + public void prePageLoad() { + run(prePageLoad); + } + public void postPageLoad() { + run(postPageLoad); + } + public void postPageProcess() { + run(postPageProcess); + } + private void run(Runnable runnable) { + if ( runnable!=null ) { runnable.run(); } + } + } + + @RequiredArgsConstructor + public static class BasicActionRequestHelper implements IActionRequestHelper { + private final IUnirestInstanceSupplier unirestInstanceSupplier; + private final IProductHelper productHelper; + private UnirestInstance unirestInstance; + public final UnirestInstance getUnirestInstance() { + if ( unirestInstance==null ) { + unirestInstance = unirestInstanceSupplier.getUnirestInstance(); + } + return unirestInstance; + } + + @Override + public JsonNode transformInput(JsonNode input) { + return JavaHelper.as(productHelper, IInputTransformer.class).orElse(i->i).transformInput(input); + } + @Override + public void executePagedRequest(ActionRequestDescriptor requestDescriptor) { + var unirest = getUnirestInstance(); + INextPageUrlProducer nextPageUrlProducer = (req, resp)->{ + var nextPageUrl = JavaHelper.as(productHelper, INextPageUrlProducerSupplier.class).get() + .getNextPageUrlProducer().getNextPageUrl(req, resp); + if ( nextPageUrl!=null ) { + requestDescriptor.prePageLoad(); + } + return nextPageUrl; + }; + HttpRequest request = createRequest(unirest, requestDescriptor); + requestDescriptor.prePageLoad(); + PagingHelper.processPages(unirest, request, nextPageUrlProducer, r->{ + requestDescriptor.postPageLoad(); + requestDescriptor.getResponseConsumer().accept(r.getBody()); + requestDescriptor.postPageProcess(); + }); + } + @Override + public void executeSimpleRequests(List requestDescriptors) { + var unirest = getUnirestInstance(); + requestDescriptors.forEach(r->executeSimpleRequest(unirest, r)); + } + private void executeSimpleRequest(UnirestInstance unirest, ActionRequestDescriptor requestDescriptor) { + createRequest(unirest, requestDescriptor) + .asObject(JsonNode.class) + .ifSuccess(r->requestDescriptor.getResponseConsumer().accept(r.getBody())); + } + + private HttpRequest createRequest(UnirestInstance unirest, ActionRequestDescriptor r) { + var result = unirest.request(r.getMethod(), r.getUri()) + .queryString(r.getQueryParams()); + return r.getBody()==null ? result : result.body(r.getBody()); + } + + @Override + public void close() { + if ( unirestInstance!=null ) { + unirestInstance.close(); + } + } + } + } + + @Builder @Data + public static final class ParameterTypeConverterArgs { + private final IProgressWriterI18n progressWriter; + private final ISpelEvaluator spelEvaluator; + private final ActionDescriptor action; + private final ActionParameterDescriptor parameter; + private final ObjectNode parameters; + } + + private static final Map> createDefaultParameterConverters() { + Map> result = new HashMap<>(); + result.put("string", (v,a)->new TextNode(v)); + result.put("boolean", (v,a)->BooleanNode.valueOf(Boolean.parseBoolean(v))); + result.put("int", (v,a)->IntNode.valueOf(Integer.parseInt(v))); + result.put("long", (v,a)->LongNode.valueOf(Long.parseLong(v))); + result.put("double", (v,a)->DoubleNode.valueOf(Double.parseDouble(v))); + result.put("float", (v,a)->FloatNode.valueOf(Float.parseFloat(v))); + // TODO Add BigIntegerNode/DecimalNode/ShortNode support? + // TODO Add array support? + return result; + } + + @RequiredArgsConstructor + private static final class JsonNodeOutputWalker extends JsonNodeDeepCopyWalker { + private final ISpelEvaluator spelEvaluator; + private final ActionOutputDescriptor outputDescriptor; + private final ObjectNode data; + @Override + protected JsonNode copyValue(JsonNode state, String path, JsonNode parent, ValueNode node) { + if ( !(node instanceof TextNode) ) { + return super.copyValue(state, path, parent, node); + } else { + TemplateExpression expression = outputDescriptor.getValueExpressions().get(path); + if ( expression==null ) { throw new RuntimeException("No expression for "+path); } + try { + var rawResult = spelEvaluator.evaluate(expression, data, Object.class); + if ( rawResult instanceof CharSequence ) { + rawResult = new TextNode(((String)rawResult).replace("\\n", "\n")); + } + return objectMapper.valueToTree(rawResult); + } catch ( SpelEvaluationException e ) { + throw new RuntimeException("Error evaluating action expression "+expression.getExpressionString(), e); + } + } + } + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSpelFunctions.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSpelFunctions.java new file mode 100644 index 0000000000..7a35a93106 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSpelFunctions.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.common.action.helper; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Safelist; + +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.json.JSONDateTimeConverter; +import com.fortify.cli.common.util.StringUtils; + +import lombok.NoArgsConstructor; + +@Reflectable @NoArgsConstructor +public class ActionSpelFunctions { + private static final Pattern uriPartsPattern = Pattern.compile("^(?(?:(?[A-Za-z]+):)?(\\/{0,3})(?[0-9.\\-A-Za-z]+)(?::(?\\d+))?)(?\\/(?[^?#]*))?(?:\\?(?[^#]*))?(?:#(?.*))?$"); + + public static final String join(String separator, List elts) { + switch (separator) { + case "\\n": separator="\n"; break; + case "\\t": separator="\t"; break; + } + return elts==null ? "" : String.join(separator, elts.stream().map(Object::toString).toList()); + } + + /** + * Convenience method to throw an exception if an expression evaluates to false + * @param throwError true if error should be thrown, false otherwise + * @param msg Message for exception to be thrown + * @return true if throwError is false + * @throws IllegalStateException with the given message if throwError is true + */ + public static final boolean check(boolean throwError, String msg) { + if ( throwError ) { + throw new IllegalStateException(msg); + } else { + return true; + } + } + + /** + * Abbreviate the given text to the given maximum width + * @param text to abbreviate + * @param maxWidth Maximum width + * @return Abbreviated text + */ + public static final String abbreviate(String text, int maxWidth) { + return StringUtils.abbreviate(text, maxWidth); + } + + /** + * @param html to be converted to plain text + * @return Formatted plain-text string for the given HTML contents + */ + public static final String htmlToText(String html) { + if( html==null ) { return null; } + Document document = Jsoup.parse(html); + document.outputSettings(new Document.OutputSettings().prettyPrint(false));//makes html() preserve linebreaks and spacing + document.select("br").append("\\n"); + document.select("p").prepend("\\n\\n"); + String s = document.html().replaceAll("\\\\n", "\n"); + return Jsoup.clean(s, "", Safelist.none(), new Document.OutputSettings().prettyPrint(false)); + } + + /** + * @param html to be converted to plain text + * @return Single line of plain text for the given HTML contents + */ + public static final String htmlToSingleLineText(String html) { + if( html==null ) { return null; } + return Jsoup.clean(html, "", Safelist.none()); + } + + /** + * Parse the given uriString using the regular expression {@value #uriPartsPattern} and return + * the value of the named capture group specified by the part parameter. + * @param uriString to be parsed + * @param part to be returned + * @return Specified part of the given uriString + */ + public static final String uriPart(String uriString, String part) { + if ( StringUtils.isBlank(uriString) ) {return null;} + // We use a regex as WebInspect results may contain URL's that contain invalid characters according to URI class + Matcher matcher = uriPartsPattern.matcher(uriString); + return matcher.matches() ? matcher.group(part) : null; + } + + /** + * Parse the given dateString as a JSON date (see {@link JSONDateTimeConverter}, then format it using the given + * {@link DateTimeFormatter} pattern. + * @param pattern used to format the specified date + * @param dateString JSON string representation of date to be formatted + * @return Formatted date + */ + public static final String formatDateTime(String pattern, String dateString) { + return formatDateTimeWithZoneId(pattern, dateString, ZoneId.systemDefault()); + } + + /** + * Parse the given dateString in the given time zone id as a JSON date (see {@link JSONDateTimeConverter}, + * then format it using the given {@link DateTimeFormatter} pattern. + * @param pattern used to format the specified date + * @param dateString JSON string representation of date to be formatted + * @param defaultZoneId Default time zone id to be used if dateString doesn't provide time zone + * @return Formatted date + */ + public static final String formatDateTimeWithZoneId(String pattern, String dateString, ZoneId defaultZoneId) { + ZonedDateTime zonedDateTime = new JSONDateTimeConverter(defaultZoneId).parseZonedDateTime(dateString); + return DateTimeFormatter.ofPattern(pattern).format(zonedDateTime); + } + + /** + * Parse the given dateString as a JSON date (see {@link JSONDateTimeConverter}, convert it to UTC time, + * then format it using the given {@link DateTimeFormatter} pattern. + * @param pattern used to format the specified date + * @param dateString JSON string representation of date to be formatted + * @return Formatted date + */ + public static final String formatDateTimeAsUTC(String pattern, String dateString) { + return formatDateTimewithZoneIdAsUTC(pattern, dateString, ZoneId.systemDefault()); + } + + /** + * Parse the given dateString as a JSON date (see {@link JSONDateTimeConverter}, convert it to UTC time, + * then format it using the given {@link DateTimeFormatter} pattern. + * @param pattern used to format the specified date + * @param dateString JSON string representation of date to be formatted + * @param defaultZoneId Default time zone id to be used if dateString doesn't provide time zone + * @return Formatted date + */ + public static final String formatDateTimewithZoneIdAsUTC(String pattern, String dateString, ZoneId defaultZoneId) { + ZonedDateTime zonedDateTime = new JSONDateTimeConverter(defaultZoneId).parseZonedDateTime(dateString); + LocalDateTime utcDateTime = LocalDateTime.ofInstant(zonedDateTime.toInstant(), ZoneOffset.UTC); + return DateTimeFormatter.ofPattern(pattern).format(utcDateTime); + } + +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommonOptionMixins.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommonOptionMixins.java index 2488882254..e7083a07f2 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommonOptionMixins.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommonOptionMixins.java @@ -13,13 +13,33 @@ package com.fortify.cli.common.cli.mixin; import java.io.File; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.function.Consumer; +import java.util.function.Function; +import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.common.util.PicocliSpecHelper; import com.fortify.cli.common.util.StringUtils; +import com.github.freva.asciitable.AsciiTable; +import com.github.freva.asciitable.Column; +import com.github.freva.asciitable.HorizontalAlign; +import lombok.Data; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import picocli.CommandLine.Command; +import picocli.CommandLine.IParameterPreprocessor; import picocli.CommandLine.Mixin; +import picocli.CommandLine.Model.ArgSpec; import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.UnmatchedArgsBinding; import picocli.CommandLine.Option; import picocli.CommandLine.ParameterException; @@ -78,4 +98,153 @@ private String getPlainPrompt(CommandSpec spec, Object... promptArgs) { return prompt; } } + + /** + * This mixin allows for collecting any unmatched command line options + * and parsing them into an [option name]=[value] map. + */ + @Command(preprocessor = OptionParametersMixinPreprocessor.class) + public static final class OptionParametersMixin { + @Getter private Map options = new LinkedHashMap<>(); + @Getter private Map optionNameToCurrentArgMap = new LinkedHashMap<>(); + @Mixin private CommandHelperMixin commandHelper; + + public final T setUnmatchedArgs(T arg) { + parse((String[])arg); + return arg; + } + + public final OptionParameterHelper getHelper(Consumer helperConfigurer) { + var helper = new OptionParameterHelper(); + helperConfigurer.accept(helper); + return helper; + } + + public final class OptionParameterHelper { + private final Map optionsWithDescriptions = new LinkedHashMap<>(); + private final Map> optionValidators = new LinkedHashMap<>(); + private final List validationErrors = new ArrayList<>(); + @Setter private String unknownOptionFormat = "Unknown option: %s"; + @Setter private String validationExceptionFormat = "Invalid options:\n %s\nSupported options:\n%s\n"; + + public final void addSupportedOption(String name, String description) { + optionsWithDescriptions.put(name, description); + } + + public final void addOptionValidator(String name, Function validator) { + optionValidators.put(name, validator); + } + + public final void requiredOption(String name, String validationErrorFormat) { + addOptionValidator(name, args->StringUtils.isBlank(args.getValue()) + ? String.format(validationErrorFormat, args.getArg()) + : null); + } + + public final void validate() { + for ( var e : optionNameToCurrentArgMap.entrySet() ) { + if ( !optionsWithDescriptions.containsKey(e.getKey()) ) { + validationErrors.add(String.format(unknownOptionFormat, e.getValue())); + } + } + optionValidators.entrySet().forEach(this::validate); + if ( validationErrors.size()>0 ) { + var errorsString = String.join("\n ", validationErrors); + var supportedOptionsString = getSupportedOptionsTable(); + var msg = String.format(validationExceptionFormat, errorsString, supportedOptionsString); + throw new ParameterException(commandHelper.getCommandSpec().commandLine(), msg); + } + } + + public final String getSupportedOptionsTable() { + return AsciiTable.builder() + .border(AsciiTable.NO_BORDERS) + .data(new Column[] { + new Column().dataAlign(HorizontalAlign.LEFT), + new Column().dataAlign(HorizontalAlign.LEFT), + }, + optionsWithDescriptions.entrySet().stream() + .map(e->new String[] {asArg(e.getKey()), e.getValue()}) + .toList().toArray(String[][]::new)) + .asString(); + } + + private static final String asArg(String name) { + return name.length()==1 ? "-"+name : "--"+name; + } + + private final void validate(Map.Entry> entry) { + var name = entry.getKey(); + var arg = asArg(name); + var value = options.get(name); + var validationError = entry.getValue().apply(new OptionParameterValidatorArgs(name, arg, value)); + if ( StringUtils.isNotBlank(validationError) ) { + validationErrors.add(validationError); + } + } + } + + @Data + public static final class OptionParameterValidatorArgs { + private final String name; + private final String arg; + private final String value; + } + + private final void parse(String[] argsArray) { + var args = asDeque(argsArray); + while ( !args.isEmpty() ) { + var arg = args.pop(); + if ( !arg.startsWith("-") ) { + throw new IllegalArgumentException("Unknown command line option: "+arg); + } else { + var optName = arg.replaceFirst("-+", ""); + getOptionNameToCurrentArgMap().put(optName, arg); + var nextArg = args.peek(); + if ( nextArg==null || nextArg.startsWith("-") ) { + options.put(optName, ""); + } else { + options.put(optName, args.pop()); + } + } + } + } + + private final Deque asDeque(String[] args) { + Deque result = new ArrayDeque<>(); + // Split --opt=value into separate args on the Deque + for ( var arg: args ) { + if ( arg.startsWith("--") ) { + // Allow for --opt=val, adding option and value as separate args + var elts = arg.split("=", 2); + result.add(elts[0]); + if ( elts.length==2 ) { result.add(elts[1]); } + } else if ( arg.startsWith("-") ) { + result.add(arg.substring(0,2)); + if ( arg.length()>2 ) { + var val = arg.substring(2); + if ( val.startsWith("=") ) { val = val.substring(1); } + result.add(val); + } + } else { + result.add(arg); + } + } + return result; + } + } + + @Reflectable @NoArgsConstructor + static final class OptionParametersMixinPreprocessor implements IParameterPreprocessor { + @Override + public final boolean preprocess(Stack args, CommandSpec commandSpec, ArgSpec argSpec, Map info) { + var mixin = commandSpec.mixins().values().stream() + .map(CommandSpec::userObject) + .filter(o->o instanceof OptionParametersMixin) + .map(OptionParametersMixin.class::cast) + .findFirst().get(); + commandSpec.addUnmatchedArgsBinding(UnmatchedArgsBinding.forStringArrayConsumer(mixin::setUnmatchedArgs)); + return false; + } + } } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/AbstractJsonWalker.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/AbstractJsonWalker.java new file mode 100644 index 0000000000..cdc7dbd0b6 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/AbstractJsonWalker.java @@ -0,0 +1,75 @@ +/** + * Copyright 2023 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.common.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.ValueNode; + +public abstract class AbstractJsonWalker { + public R walk(JsonNode node) { + walk("", null, node); + return getResult(); + } + protected abstract R getResult(); + + protected void walk(String path, JsonNode parent, JsonNode node) { + if ( !skipNode(path, parent, node) ) { + if ( node instanceof ContainerNode ) { + walkContainer(path, parent, (ContainerNode)node); + } else if ( node instanceof ValueNode ) { + walkValue(path, parent, (ValueNode)node); + } + } + } + + protected boolean skipNode(String path, JsonNode parent, JsonNode node) { + return false; + } + + protected void walkContainer(String path, JsonNode parent, ContainerNode node) { + if ( node instanceof ArrayNode ) { + walkArray(path, parent, (ArrayNode)node); + } else if ( node instanceof ObjectNode ) { + walkObject(path, parent, (ObjectNode)node); + } + } + + protected void walkObject(String path, JsonNode parent, ObjectNode node) { + node.fields().forEachRemaining(e->walkObjectProperty(appendPath(path, e.getKey()), node, e.getKey(), e.getValue())); + } + + protected void walkObjectProperty(String path, ObjectNode parent, String property, JsonNode value) { + walk(path, parent, value); + } + + protected void walkArray(String path, JsonNode parent, ArrayNode node) { + for ( int i = 0 ; i < node.size() ; i++ ) { + walkArrayElement(appendPath(path, i+""), node, i, node.get(i)); + } + } + + protected void walkArrayElement(String path, ArrayNode parent, int index, JsonNode value) { + walk(path, parent, value); + } + + protected void walkValue(String path, JsonNode parent, ValueNode node) { + + } + + protected final String appendPath(String parent, String entry) { + return String.format("%s[%s]", parent, entry); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JSONDateTimeConverter.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JSONDateTimeConverter.java new file mode 100644 index 0000000000..6842b36431 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JSONDateTimeConverter.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * (c) Copyright 2020 Micro Focus or one of its affiliates + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + * KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + ******************************************************************************/ +package com.fortify.cli.common.json; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Date; + +import org.springframework.core.convert.converter.Converter; + +public final class JSONDateTimeConverter implements Converter { + private final DateTimeFormatter fmtDateTime; + private final ZoneId defaultZoneId; + + public JSONDateTimeConverter() { + this(null, null); + } + + public JSONDateTimeConverter(DateTimeFormatter fmtDateTime) { + this(fmtDateTime, null); + } + + public JSONDateTimeConverter(ZoneId defaultZoneId) { + this(null, defaultZoneId); + } + + public JSONDateTimeConverter(DateTimeFormatter fmtDateTime, ZoneId defaultZoneId) { + this.fmtDateTime = fmtDateTime!=null ? fmtDateTime : createDefaultDateTimeFormatter(); + this.defaultZoneId = defaultZoneId!=null ? defaultZoneId : ZoneId.systemDefault(); + } + + private static final DateTimeFormatter createDefaultDateTimeFormatter() { + return DateTimeFormatter.ofPattern("yyyy-MM-dd[['T'][' ']HH:mm:ss[.SSS][.SS]][ZZZZ][Z][XXX][XX][X]"); + } + + @Override + public Date convert(String source) { + return parseDate(source); + } + + public Date parseDate(String source) { + return Date.from(parseZonedDateTime(source).toInstant()); + } + + public ZonedDateTime parseZonedDateTime(String source) { + TemporalAccessor temporalAccessor = parseTemporalAccessor(source); + if (temporalAccessor instanceof ZonedDateTime) { + return ((ZonedDateTime) temporalAccessor); + } + if (temporalAccessor instanceof LocalDateTime) { + return ((LocalDateTime) temporalAccessor).atZone(defaultZoneId); + } + return ((LocalDate) temporalAccessor).atStartOfDay(defaultZoneId); + } + + public TemporalAccessor parseTemporalAccessor(String source) { + return fmtDateTime.parseBest(source, ZonedDateTime::from, LocalDateTime::from, LocalDate::from); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JsonHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JsonHelper.java index 5495f20bc4..b4a588e0c2 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JsonHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JsonHelper.java @@ -30,8 +30,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ContainerNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.node.ValueNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fortify.cli.common.spring.expression.SpelEvaluator; import com.fortify.cli.common.util.StringUtils; @@ -87,6 +90,12 @@ public static final Stream stream(ArrayNode arrayNode) { return StreamSupport.stream(iterable(arrayNode).spliterator(), false); } + public static final ObjectNode shallowCopy(ObjectNode node) { + var newData = objectMapper.createObjectNode(); + newData.setAll(node); + return newData; + } + public static final ArrayNodeCollector arrayNodeCollector() { return new ArrayNodeCollector(); } @@ -149,5 +158,110 @@ public Function finisher() { public Set characteristics() { return EnumSet.of(Characteristics.UNORDERED); } - } + } + + public static abstract class AbstractJsonNodeWalker { + public final R walk(JsonNode node) { + if ( node!=null ) { + walk(null, "", null, node); + } + return getResult(); + } + protected abstract R getResult(); + + protected void walk(S state, String path, JsonNode parent, JsonNode node) { + if ( !skipNode(state, path, parent, node) ) { + if ( node instanceof ContainerNode ) { + walkContainer(state, path, parent, (ContainerNode)node); + } else if ( node instanceof ValueNode ) { + walkValue(state, path, parent, (ValueNode)node); + } + } + } + + protected boolean skipNode(S state, String path, JsonNode parent, JsonNode node) { + return false; + } + + protected void walkContainer(S state, String path, JsonNode parent, ContainerNode node) { + if ( node instanceof ArrayNode ) { + walkArray(state, path, parent, (ArrayNode)node); + } else if ( node instanceof ObjectNode ) { + walkObject(state, path, parent, (ObjectNode)node); + } + } + + protected void walkObject(S state, String path, JsonNode parent, ObjectNode node) { + node.fields().forEachRemaining(e->walkObjectProperty(state, appendPath(path, e.getKey()), node, e.getKey(), e.getValue())); + } + + protected void walkObjectProperty(S state, String path, ObjectNode parent, String property, JsonNode value) { + walk(state, path, parent, value); + } + + protected void walkArray(S state, String path, JsonNode parent, ArrayNode node) { + for ( int i = 0 ; i < node.size() ; i++ ) { + walkArrayElement(state, appendPath(path, i+""), node, i, node.get(i)); + } + } + + protected void walkArrayElement(S state, String path, ArrayNode parent, int index, JsonNode value) { + walk(state, path, parent, value); + } + + protected void walkValue(S state, String path, JsonNode parent, ValueNode node) {} + + protected final String appendPath(String parent, String entry) { + return String.format("%s[%s]", parent, entry); + } + } + + public static class JsonNodeDeepCopyWalker extends AbstractJsonNodeWalker { + @Getter JsonNode result; + @Override + protected void walkObject(JsonNode state, String path, JsonNode parent, ObjectNode node) { + if ( state==null ) { state = objectMapper.createObjectNode(); } + if ( result==null ) { result = state; } + super.walkObject(state, path, parent, node); + } + @Override + protected void walkObjectProperty(JsonNode state, String path, ObjectNode parent, String property, JsonNode value) { + if ( value instanceof ContainerNode ) { + var newState = createContainerNode(value.getNodeType()); + ((ObjectNode)state).set(property, newState); + super.walkObjectProperty(newState, path, parent, property, value); + } else { + ((ObjectNode)state).set(property, copyValue(state, path, parent, (ValueNode)value)); + } + } + @Override + protected void walkArray(JsonNode state, String path, JsonNode parent, ArrayNode node) { + if ( state==null ) { state = objectMapper.createArrayNode(); } + if ( result==null ) { result = state; } + super.walkArray(state, path, parent, node); + } + @Override + protected void walkArrayElement(JsonNode state, String path, ArrayNode parent, int index, JsonNode value) { + if ( value instanceof ContainerNode ) { + var newState = createContainerNode(value.getNodeType()); + ((ArrayNode)state).insert(index, newState); + super.walkArrayElement(newState, path, parent, index, value); + } else { + ((ArrayNode)state).insert(index, copyValue(state, path, parent, (ValueNode)value)); + } + } + @Override + protected void walkValue(JsonNode state, String path, JsonNode parent, ValueNode node) { + if ( result == null ) { result = copyValue(state, path, parent, node); } + } + protected final JsonNode createContainerNode(JsonNodeType jsonNodeType) { + return jsonNodeType==JsonNodeType.ARRAY + ? objectMapper.createArrayNode() + : objectMapper.createObjectNode(); + } + // We return JsonNode to allow subclasses to return other node types + protected JsonNode copyValue(JsonNode state, String path, JsonNode parent, ValueNode node) { + return node.deepCopy(); + } + } } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/output/standard/StandardOutputWriter.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/output/standard/StandardOutputWriter.java index 247a4564c2..a1a8f7cde9 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/output/standard/StandardOutputWriter.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/output/standard/StandardOutputWriter.java @@ -168,13 +168,8 @@ private final void writeRecords(IRecordWriter recordWriter, HttpRequest httpR * @param httpRequest * @param nextPageRequestProducer */ - private final void writeRecords(IRecordWriter recordWriter, HttpRequest originalRequest, INextPageRequestProducer nextPageRequestProducer) { - var currentRequest = originalRequest; - while ( currentRequest!=null ) { - HttpResponse response = currentRequest.asObject(JsonNode.class); - writeRecords(recordWriter, response); - currentRequest = nextPageRequestProducer.getNextPageRequest(originalRequest, response); - } + private final void writeRecords(IRecordWriter recordWriter, HttpRequest initialRequest, INextPageRequestProducer nextPageRequestProducer) { + PagingHelper.processPages(initialRequest, nextPageRequestProducer, r->writeRecords(recordWriter, r)); } /** diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/helper/ProgressWriterType.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/helper/ProgressWriterType.java index ddf41ca54a..a33dee701a 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/helper/ProgressWriterType.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/helper/ProgressWriterType.java @@ -55,6 +55,14 @@ public void close() { clearProgress(); warnings.forEach(System.err::println); } + + protected final String format(String message, Object... args) { + if ( args==null || args.length==0 ) { + return message; + } else { + return String.format(message, args); + } + } } private static final class NoProgressWriter extends AbstractProgressWriter { @@ -78,7 +86,7 @@ public boolean isMultiLineSupported() { @Override public void writeProgress(String message, Object... args) { - String formattedMessage = String.format(message, args); + String formattedMessage = format(message, args); if ( formattedMessage.indexOf('\n') > 0 ) { // Add extra newline to separate multi-line blocks formattedMessage += "\n"; @@ -98,7 +106,7 @@ public boolean isMultiLineSupported() { @Override public void writeProgress(String message, Object... args) { - String formattedMessage = String.format(message, args); + String formattedMessage = format(message, args); if ( formattedMessage.indexOf('\n') > 0 ) { // Add extra newline to separate multi-line blocks formattedMessage += "\n"; @@ -121,9 +129,9 @@ public boolean isMultiLineSupported() { @Override public void writeProgress(String message, Object... args) { - if ( message.contains("\n") ) { throw new RuntimeException("Multiline status updates are not supported; please file a bug"); } + String formattedMessage = format(message, args); + if ( formattedMessage.contains("\n") ) { throw new RuntimeException("Multiline status updates are not supported; please file a bug"); } clearProgress(); - String formattedMessage = String.format(message, args); System.out.print(formattedMessage); this.lastNumberOfChars = formattedMessage.length(); } @@ -148,8 +156,8 @@ public boolean isMultiLineSupported() { @Override public void writeProgress(String message, Object... args) { + String formattedMessage = format(message, args); clearProgress(); - String formattedMessage = String.format(message, args); System.out.print(formattedMessage); this.lastNumberOfLines = (int)formattedMessage.chars().filter(ch -> ch == '\n').count(); } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/paging/PagingHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/paging/PagingHelper.java index 4b47dda9ab..eb388f7a0b 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/paging/PagingHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/paging/PagingHelper.java @@ -12,6 +12,8 @@ *******************************************************************************/ package com.fortify.cli.common.rest.paging; +import java.util.function.Consumer; + import com.fasterxml.jackson.databind.JsonNode; import kong.unirest.Header; @@ -61,6 +63,23 @@ public static final INextPageRequestProducer asNextPageRequestProducer(UnirestIn return unirest==null || nextPageUrlProducer==null ? null : new NextPageRequestProducer(unirest, nextPageUrlProducer); } + public static final void processPages(UnirestInstance unirest, HttpRequest initialRequest, INextPageUrlProducer nextPageUrlProducer, Consumer> consumer) { + var nextPageRequestProducer = asNextPageRequestProducer(unirest, nextPageUrlProducer); + if ( nextPageRequestProducer==null ) { + throw new IllegalStateException("Cannot process pages without a valid NextPageRequestProducer"); + } + processPages(initialRequest, nextPageRequestProducer, consumer); + } + + public static final void processPages(HttpRequest initialRequest, INextPageRequestProducer nextPageRequestProducer, Consumer> consumer) { + var currentRequest = initialRequest; + while ( currentRequest!=null ) { + HttpResponse response = currentRequest.asObject(JsonNode.class); + consumer.accept(response); + currentRequest = nextPageRequestProducer.getNextPageRequest(initialRequest, response); + } + } + @RequiredArgsConstructor private static final class NextPageRequestProducer implements INextPageRequestProducer { private final UnirestInstance unirest; diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/IConfigurableSpelEvaluator.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/IConfigurableSpelEvaluator.java new file mode 100644 index 0000000000..edf2a0075d --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/IConfigurableSpelEvaluator.java @@ -0,0 +1,21 @@ +/** + * Copyright 2023 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.common.spring.expression; + +import java.util.function.Consumer; + +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +public interface IConfigurableSpelEvaluator extends ISpelEvaluator { + IConfigurableSpelEvaluator configure(Consumer contextConfigurer); +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/ISpelEvaluator.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/ISpelEvaluator.java new file mode 100644 index 0000000000..41281ad1ff --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/ISpelEvaluator.java @@ -0,0 +1,20 @@ +/** + * Copyright 2023 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.common.spring.expression; + +import org.springframework.expression.Expression; + +public interface ISpelEvaluator { + R evaluate(Expression expression, Object input, Class returnClass); + R evaluate(String expression, Object input, Class returnClass); +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelEvaluator.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelEvaluator.java index ddbb1cbead..c8be72c2ce 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelEvaluator.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelEvaluator.java @@ -14,6 +14,8 @@ import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; import org.springframework.core.convert.converter.Converter; import org.springframework.expression.AccessException; @@ -25,48 +27,88 @@ import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.integration.json.JsonNodeWrapperToJsonNodeConverter; import org.springframework.integration.json.JsonPropertyAccessor; +import org.springframework.integration.json.JsonPropertyAccessor.JsonNodeWrapper; 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.json.JsonHelper; -import lombok.Getter; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor -public enum SpelEvaluator { - JSON_GENERIC(createJsonGenericContext()), - JSON_QUERY(createJsonQueryContext()); +public enum SpelEvaluator implements ISpelEvaluator { + JSON_GENERIC(SpelEvaluator::createJsonGenericContext), + JSON_QUERY(SpelEvaluator::createJsonQueryContext); private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser(); - @Getter private final EvaluationContext context; + private EvaluationContext context; + private final Supplier contextSupplier; + + private SpelEvaluator(Supplier contextSupplier) { + this.context = contextSupplier.get(); + this.contextSupplier = contextSupplier; + } public final R evaluate(Expression expression, Object input, Class returnClass) { - return expression.getValue(context, input, returnClass); + return evaluate(context, expression, input, returnClass); } public final R evaluate(String expression, Object input, Class returnClass) { return evaluate(SPEL_PARSER.parseExpression(expression), input, returnClass); } - private static final EvaluationContext createJsonGenericContext() { + public final IConfigurableSpelEvaluator copy() { + return new ConfigurableSpelEvaluator(contextSupplier.get()); + } + + @RequiredArgsConstructor + private static final class ConfigurableSpelEvaluator implements IConfigurableSpelEvaluator { + private final SimpleEvaluationContext context; + + public final R evaluate(Expression expression, Object input, Class returnClass) { + return SpelEvaluator.evaluate(context, expression, input, returnClass); + } + + public final R evaluate(String expression, Object input, Class returnClass) { + return evaluate(SPEL_PARSER.parseExpression(expression), input, returnClass); + } + + @Override + public IConfigurableSpelEvaluator configure(Consumer contextConfigurer) { + contextConfigurer.accept(context); + return this; + } + } + + private static final R evaluate(EvaluationContext context, Expression expression, Object input, Class returnClass) { + return unwrapSpelExpressionResult(expression.getValue(context, input, returnClass), returnClass); + } + + @SuppressWarnings("unchecked") + private static final R unwrapSpelExpressionResult(R result, Class returnClass) { + if ( result instanceof JsonNodeWrapper && returnClass.isAssignableFrom(JsonNode.class) ) { + result = (R)((JsonNodeWrapper)result).getRealNode(); + } + return result; + } + + private static final SimpleEvaluationContext createJsonGenericContext() { SimpleEvaluationContext context = SimpleEvaluationContext .forPropertyAccessors(new JsonPropertyAccessor()) .withConversionService(createJsonConversionService()) .withInstanceMethods() .build(); - SpelHelper.registerFunctions(context, StandardSpelFunctions.class); + SpelHelper.registerFunctions(context, SpelFunctionsStandard.class); return context; } - private static final EvaluationContext createJsonQueryContext() { + private static final SimpleEvaluationContext createJsonQueryContext() { SimpleEvaluationContext context = SimpleEvaluationContext .forPropertyAccessors(new ExistingJsonPropertyAccessor()) .withConversionService(createJsonConversionService()) .withInstanceMethods() .build(); - SpelHelper.registerFunctions(context, StandardSpelFunctions.class); + SpelHelper.registerFunctions(context, SpelFunctionsStandard.class); return context; } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/StandardSpelFunctions.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelFunctionsStandard.java similarity index 98% rename from fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/StandardSpelFunctions.java rename to fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelFunctionsStandard.java index 8952489a88..afe231b553 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/StandardSpelFunctions.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelFunctionsStandard.java @@ -28,7 +28,7 @@ import lombok.NoArgsConstructor; @Reflectable @NoArgsConstructor -public class StandardSpelFunctions { +public class SpelFunctionsStandard { private static final DateTimePeriodHelper PeriodHelper = DateTimePeriodHelper.all(); public static final OffsetDateTime date(String s) { diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelHelper.java index eb41c96e63..69ebbcaa05 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/SpelHelper.java @@ -14,9 +14,23 @@ import java.lang.reflect.Method; +import org.springframework.expression.common.TemplateParserContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; +import com.fortify.cli.common.spring.expression.wrapper.SimpleExpression; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + public final class SpelHelper { + private static final SpelExpressionParser parser = new SpelExpressionParser(); + private static final TemplateParserContext templateContext = new TemplateParserContext("${","}"); + + public static final SimpleExpression parseSimpleExpression(String s) { + return new SimpleExpression(parser.parseExpression(s)); + } + public static final TemplateExpression parseTemplateExpression(String s) { + return new TemplateExpression(parser.parseExpression(s, templateContext)); + } public static final void registerFunctions(SimpleEvaluationContext context, Class clazz) { for ( Method m : clazz.getDeclaredMethods() ) { context.setVariable(m.getName(), m); diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpression.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpression.java new file mode 100644 index 0000000000..23625e90f1 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpression.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * (c) Copyright 2020 Micro Focus or one of its affiliates, a Micro Focus company + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + * KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + ******************************************************************************/ +package com.fortify.cli.common.spring.expression.wrapper; + +import org.springframework.expression.Expression; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + *

This is a simple wrapper class for a Spring {@link Expression} + * instance. It's main use is in combination with + * {@link SimpleExpressionDeserializer} to allow automatic + * conversion from String values to simple {@link Expression} + * instances in JSON/YAML documents.

+ * + *

The reason for needing this wrapper class is to differentiate + * with templated {@link Expression} instances that are handled + * by {@link TemplateExpressionDeserializer}.

+ */ +@JsonDeserialize(using = SimpleExpressionDeserializer.class) +public class SimpleExpression extends WrappedExpression { + public SimpleExpression(Expression target) { + super(target); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpressionDeserializer.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpressionDeserializer.java new file mode 100644 index 0000000000..84f7d59594 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpressionDeserializer.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * (c) Copyright 2020 Micro Focus or one of its affiliates, a Micro Focus company + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + * KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + ******************************************************************************/ +package com.fortify.cli.common.spring.expression.wrapper; + +import java.io.IOException; + +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.formkiq.graalvm.annotations.Reflectable; + +/** + * This Jackson deserializer allows parsing String values into an + * SpEL Expression object. + */ +@Reflectable +public final class SimpleExpressionDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; + private static final SpelExpressionParser parser = new SpelExpressionParser(); + public SimpleExpressionDeserializer() { this(null); } + public SimpleExpressionDeserializer(Class vc) { super(vc); } + + @Override + public SimpleExpression deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonNode node = jp.getCodec().readTree(jp); + return node==null || node.isNull() ? null : new SimpleExpression(parser.parseExpression(node.asText())); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpression.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpression.java new file mode 100644 index 0000000000..ec37450f8d --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpression.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * (c) Copyright 2020 Micro Focus or one of its affiliates, a Micro Focus company + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + * KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + ******************************************************************************/ +package com.fortify.cli.common.spring.expression.wrapper; + +import org.springframework.expression.Expression; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + *

This is a simple wrapper class for a Spring {@link Expression} + * instance. It's main use is in combination with + * {@link TemplateExpressionDeserializer} to allow automatic + * conversion from String values to templated {@link Expression} + * instances.

+ * + *

The reason for needing this wrapper class is to differentiate + * with non-templated {@link Expression} instances that are + * handled by {@link SimpleExpressionDeserializer}.

+ */ +@JsonDeserialize(using = TemplateExpressionDeserializer.class) +public class TemplateExpression extends WrappedExpression { + public TemplateExpression(Expression target) { + super(target); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpressionDeserializer.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpressionDeserializer.java new file mode 100644 index 0000000000..8831a324a7 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpressionDeserializer.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * (c) Copyright 2020 Micro Focus or one of its affiliates, a Micro Focus company + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + * KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + ******************************************************************************/ +package com.fortify.cli.common.spring.expression.wrapper; + +import java.beans.PropertyEditor; +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.SpelHelper; + +/** + * This {@link PropertyEditor} allows parsing String values into a + * TemplateExpression object. + */ +@Reflectable +public final class TemplateExpressionDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; + + public TemplateExpressionDeserializer() { this(null); } + public TemplateExpressionDeserializer(Class vc) { super(vc); } + + @Override + public TemplateExpression deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonNode node = jp.getCodec().readTree(jp); + return node==null || node.isNull() ? null : SpelHelper.parseTemplateExpression(node.asText()); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/WrappedExpression.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/WrappedExpression.java new file mode 100644 index 0000000000..c06bbe1132 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/WrappedExpression.java @@ -0,0 +1,292 @@ +/******************************************************************************* + * (c) Copyright 2020 Micro Focus or one of its affiliates, a Micro Focus company + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + * KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + ******************************************************************************/ +package com.fortify.cli.common.spring.expression.wrapper; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; + +/** + *

This is a simple wrapper class for a Spring {@link Expression} + * instance. This class is used as a based class for both + * {@link SimpleExpression} and {@link TemplateExpression}.

+ */ +public class WrappedExpression implements Expression { + private final Expression target; + + /** + * Constructor for configuring the expression to be wrapped + * @param target {@link Expression} to be wrapped + */ + public WrappedExpression(Expression target) { + this.target = target; + } + + /** + * @see org.springframework.expression.Expression#getValue() + * @return The evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public Object getValue() throws EvaluationException { + return target.getValue(); + } + + /** + * @see org.springframework.expression.Expression#getValue(Object) + * @param rootObject the root object against which to evaluate the expression + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public Object getValue(Object rootObject) throws EvaluationException { + return target.getValue(rootObject); + } + + /** + * @see org.springframework.expression.Expression#getValue(java.lang.Class) + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public T getValue(Class desiredResultType) throws EvaluationException { + return target.getValue(desiredResultType); + } + + /** + * @see org.springframework.expression.Expression#getValue(Object, java.lang.Class) + * @param rootObject the root object against which to evaluate the expression + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public T getValue(Object rootObject,Class desiredResultType) throws EvaluationException { + return target.getValue(rootObject, desiredResultType); + } + + /** + * @see org.springframework.expression.Expression#getValue(EvaluationContext) + * @param context the context in which to evaluate the expression + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + * + */ + public Object getValue(EvaluationContext context) throws EvaluationException { + return target.getValue(context); + } + + /** + * @see org.springframework.expression.Expression#getValue(EvaluationContext, Object) + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + * + */ + public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + return target.getValue(context, rootObject); + } + + /** + * @see org.springframework.expression.Expression#getValue(EvaluationContext, java.lang.Class) + * @param context the context in which to evaluate the expression + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public T getValue(EvaluationContext context, Class desiredResultType) throws EvaluationException { + return target.getValue(context, desiredResultType); + } + + /** + * @see org.springframework.expression.Expression#getValue(EvaluationContext, Object, java.lang.Class) + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) throws EvaluationException { + return target.getValue(context, rootObject, desiredResultType); + } + + /** + * @see org.springframework.expression.Expression#getValueType() + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public Class getValueType() throws EvaluationException { + return target.getValueType(); + } + + /** + * @see org.springframework.expression.Expression#getValueType(Object) + * @param rootObject the root object against which to evaluate the expression + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public Class getValueType(Object rootObject) throws EvaluationException { + return target.getValueType(rootObject); + } + + /** + * @see org.springframework.expression.Expression#getValueType(EvaluationContext) + * @param context the context in which to evaluate the expression + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public Class getValueType(EvaluationContext context) throws EvaluationException { + return target.getValueType(context); + } + + /** + * @see org.springframework.expression.Expression#getValueType(EvaluationContext, Object) + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { + return target.getValueType(context, rootObject); + } + + /** + * @see org.springframework.expression.Expression#getValueTypeDescriptor() + * @return a type descriptor for values that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { + return target.getValueTypeDescriptor(); + } + + /** + * @see org.springframework.expression.Expression#getValueTypeDescriptor(Object) + * @param rootObject the root object against which to evaluate the expression + * @return a type descriptor for values that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { + return target.getValueTypeDescriptor(rootObject); + } + + /** + * @see org.springframework.expression.Expression#getValueTypeDescriptor(EvaluationContext) + * @param context the context in which to evaluate the expression + * @return a type descriptor for values that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { + return target.getValueTypeDescriptor(context); + } + + /** + * @see org.springframework.expression.Expression#getValueTypeDescriptor(EvaluationContext, Object) + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @return a type descriptor for values that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) throws EvaluationException { + return target.getValueTypeDescriptor(context, rootObject); + } + + /** + * @see org.springframework.expression.Expression#isWritable(EvaluationContext) + * @param context the context in which the expression should be checked + * @return {@code true} if the expression is writable; {@code false} otherwise + * @throws EvaluationException if there is a problem determining if it is writable + */ + public boolean isWritable(EvaluationContext context) throws EvaluationException { + return target.isWritable(context); + } + + /** + * @see org.springframework.expression.Expression#isWritable(EvaluationContext, Object) + * @param context the context in which the expression should be checked + * @param rootObject the root object against which to evaluate the expression + * @return {@code true} if the expression is writable; {@code false} otherwise + * @throws EvaluationException if there is a problem determining if it is writable + */ + public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { + return target.isWritable(context, rootObject); + } + + /** + * @see org.springframework.expression.Expression#isWritable(Object) + * @param rootObject the root object against which to evaluate the expression + * @return {@code true} if the expression is writable; {@code false} otherwise + * @throws EvaluationException if there is a problem determining if it is writable + */ + public boolean isWritable(Object rootObject) throws EvaluationException { + return target.isWritable(rootObject); + } + + /** + * @see org.springframework.expression.Expression#setValue(EvaluationContext, Object) + * @param context the context in which to set the value of the expression + * @param value the new value + * @throws EvaluationException if there is a problem during evaluation + */ + public void setValue(EvaluationContext context, Object value) throws EvaluationException { + target.setValue(context, value); + } + + /** + * @see org.springframework.expression.Expression#setValue(Object, Object) + * @param rootObject the root object against which to evaluate the expression + * @param value the new value + * @throws EvaluationException if there is a problem during evaluation + */ + public void setValue(Object rootObject, Object value) throws EvaluationException { + target.setValue(rootObject, value); + } + + /** + * @see org.springframework.expression.Expression#setValue(EvaluationContext, Object, Object) + * @param context the context in which to set the value of the expression + * @param rootObject the root object against which to evaluate the expression + * @param value the new value + * @throws EvaluationException if there is a problem during evaluation + */ + public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { + target.setValue(context, rootObject, value); + } + + /** + * @see org.springframework.expression.Expression#getExpressionString() + * @return the original expression string + */ + public String getExpressionString() { + return target.getExpressionString(); + } + + /** + * @return String representation for this {@link WrappedExpression} + */ + @Override + public String toString() { + return this.getClass().getSimpleName()+"("+getExpressionString()+")"; + } + +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FileUtils.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FileUtils.java index 9fcb863905..5513059e42 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FileUtils.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FileUtils.java @@ -50,6 +50,11 @@ public static final String readResourceAsString(String resourcePath, Charset cha return new String(readResourceAsBytes(resourcePath), charset); } + @SneakyThrows + public static String readInputStreamAsString(InputStream is, Charset charset) { + return new String(is.readAllBytes(), charset); + } + @SneakyThrows public static final byte[] readResourceAsBytes(String resourcePath) { try ( InputStream in = getResourceInputStream(resourcePath) ) { diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/StringUtils.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/StringUtils.java index 717cd47deb..92013b36c5 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/StringUtils.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/StringUtils.java @@ -52,7 +52,7 @@ public static String abbreviate(String input, int maxLength) { if (input.length() <= maxLength) { return input; } else { - return input.substring(0, maxLength); + return input.substring(0, maxLength-3) + "..."; } } } diff --git a/fcli-core/fcli-common/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java b/fcli-core/fcli-common/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java index 966e3cc719..1c925b71ae 100644 --- a/fcli-core/fcli-common/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java +++ b/fcli-core/fcli-common/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java @@ -20,11 +20,6 @@ import java.util.AbstractList; import java.util.Iterator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; - import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.PropertyAccessor; @@ -33,6 +28,12 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.POJONode; + /** * A SpEL {@link PropertyAccessor} that knows how to read properties from JSON objects. * Uses Jackson {@link JsonNode} API for nested properties access. @@ -192,6 +193,10 @@ else if (json.isBinary()) { "Can not get content of binary value: " + json, e); } } + // CHANGED: Compared to original version, this code was added to allow access to POJO node values. + else if (json.isPojo() ) { + return ((POJONode)json).getPojo(); + } throw new IllegalArgumentException("Json is not ValueNode."); } @@ -210,7 +215,10 @@ else if (json.isValueNode()) { } } - interface JsonNodeWrapper extends Comparable { + // CHANGED: Compared to original version, this interface was changed to public + // to allow access by JsonHelper::evaluateSpelExpression (through + // JsonHelper::unwrapSpelExpressionResult). + public interface JsonNodeWrapper extends Comparable { String toString(); diff --git a/fcli-core/fcli-common/src/main/java/org/springframework/integration/json/package-info.java b/fcli-core/fcli-common/src/main/java/org/springframework/integration/json/package-info.java index 6bf933274f..136f18dda5 100644 --- a/fcli-core/fcli-common/src/main/java/org/springframework/integration/json/package-info.java +++ b/fcli-core/fcli-common/src/main/java/org/springframework/integration/json/package-info.java @@ -1,7 +1,11 @@ /** * This package contains a copy of two classes from spring-integration that * fcli depends on, to avoid having to pull in the full spring-integration - * dependency and transitive dependencies. + * dependency and transitive dependencies. Compared to the original version, + * there's one minor change; the JsonNodeWrapper interface has been changed + * to public to allow access to the original JsonNode instance if an SpEL + * expression returns a JsonNode, as used by JsonHelper::evaluateSpelExpression + * (through JsonHelper::unwrapSpelExpressionResult). */ package org.springframework.integration.json; diff --git a/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/i18n/FortifyCLIMessages.properties b/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/i18n/FortifyCLIMessages.properties index 9bf8c02f6a..28d5e84bf5 100644 --- a/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/i18n/FortifyCLIMessages.properties +++ b/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/i18n/FortifyCLIMessages.properties @@ -27,6 +27,10 @@ log-level = Set logging level. Note that DEBUG and TRACE levels may result in se being written to the log file. Allowed values: ${COMPLETION-CANDIDATES}. log-file = File where logging data will be written. Defaults to fcli.log in current directory \ if --log-level is specified. +fcli.action.run.action = Action to run. +fcli.action.run.action-help = Show usage information for the given action. +fcli.action.run.action-parameter = Action parameter(s); see --action-help option to \ + list supported parameters. # Generic, non command-specific output and query options arggroup.output.heading = Output options:%n diff --git a/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/json/JsonNodeDeepCopyWalkerTest.java b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/json/JsonNodeDeepCopyWalkerTest.java new file mode 100644 index 0000000000..5c5a070b2e --- /dev/null +++ b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/json/JsonNodeDeepCopyWalkerTest.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.common.json; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.UUID; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.node.ValueNode; +import com.fortify.cli.common.json.JsonHelper.JsonNodeDeepCopyWalker; + +public class JsonNodeDeepCopyWalkerTest { + private static final ObjectMapper objectMapper = JsonHelper.getObjectMapper(); + + @ParameterizedTest + @MethodSource("getSampleNodesStream") + public void testDeepCopy(JsonNode node) throws Exception { + var copy = new JsonNodeDeepCopyWalker().walk(node); + assertEquals(node, copy); + if ( node!=null && !(node instanceof ValueNode) ) { + System.out.println(copy.toPrettyString()); + assertFalse(node==copy); + } + } + + private static Stream getSampleNodesStream() { + return Stream.of( + Arguments.of(new Object[] {null}), + Arguments.of(createEmptyObjectNode()), + Arguments.of(createSampleValueObjectNode()), + Arguments.of(createSampleMultiLevelObjectNode()), + Arguments.of(createEmptyArrayNode()), + Arguments.of(createSampleValueArrayNode()), + Arguments.of(createSampleMultiLevelArrayNode()), + Arguments.of(createSampleValueNode()) + ); + } + + private static ObjectNode createEmptyObjectNode() { + return objectMapper.createObjectNode(); + } + + private static ArrayNode createEmptyArrayNode() { + return objectMapper.createArrayNode(); + } + + private static ObjectNode createSampleMultiLevelObjectNode() { + var result = createSampleValueObjectNode(); + result.set("array", createSampleValueArrayNode()); + result.set("obj", createSampleValueObjectNode()); + return result; + } + + private static ObjectNode createSampleValueObjectNode() { + var result = objectMapper.createObjectNode(); + result.put("int", 1); + result.put("bool", true); + result.put("float", 2.2f); + result.put("str1", "someString"); + result.set("val", createSampleValueNode()); + return result; + } + + private static ArrayNode createSampleMultiLevelArrayNode() { + var result = objectMapper.createArrayNode(); + for ( int i = 0 ; i < 20 ; i++ ) { + result.add(createSampleMultiLevelObjectNode()); + } + return result; + } + + private static ArrayNode createSampleValueArrayNode() { + var result = objectMapper.createArrayNode(); + for ( int i = 0 ; i < 10 ; i++ ) { + result.add(100+i); + } + return result; + } + + private static ValueNode createSampleValueNode() { + return new TextNode(UUID.randomUUID().toString()); + } + +} diff --git a/fcli-core/fcli-fod/build.gradle b/fcli-core/fcli-fod/build.gradle index 8e063597b9..cf287d3126 100644 --- a/fcli-core/fcli-fod/build.gradle +++ b/fcli-core/fcli-fod/build.gradle @@ -1 +1,7 @@ +task zipResources_templates(type: Zip) { + destinationDirectory = file("${buildDir}/generated-zip-resources/com/fortify/cli/fod") + archiveFileName = "actions.zip" + from("${projectDir}/src/main/resources//com/fortify/cli/fod/actions/zip") +} + apply from: "${sharedGradleScriptsDir}/fcli-module.gradle" \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanFileUploadCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanFileUploadCommand.java index a83c2309ed..e33ccf00b9 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanFileUploadCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanFileUploadCommand.java @@ -20,6 +20,7 @@ import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.helper.FoDFileTransferHelper; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; + import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import picocli.CommandLine.Mixin; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanSetupCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanSetupCommand.java index 6d66a3fac7..efcd37fc8e 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanSetupCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanSetupCommand.java @@ -13,6 +13,9 @@ package com.fortify.cli.fod._common.scan.cli.cmd; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fortify.cli.common.cli.mixin.CommonOptionMixins; @@ -24,12 +27,11 @@ import com.fortify.cli.fod._common.scan.cli.mixin.FoDEntitlementFrequencyTypeMixins; import com.fortify.cli.fod._common.scan.helper.FoDScanType; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; + import kong.unirest.HttpRequest; import kong.unirest.HttpResponse; import kong.unirest.UnirestInstance; import lombok.Getter; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/mixin/FoDDastFileTypeMixins.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/mixin/FoDDastFileTypeMixins.java index d4d027495c..e464aeb0de 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/mixin/FoDDastFileTypeMixins.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/mixin/FoDDastFileTypeMixins.java @@ -14,6 +14,7 @@ package com.fortify.cli.fod._common.scan.cli.mixin; import com.fortify.cli.fod._common.util.FoDEnums; + import lombok.Getter; import picocli.CommandLine.Option; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDFileUploadDescriptor.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDFileUploadDescriptor.java index 9811ac4069..68aebaa0c1 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDFileUploadDescriptor.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDFileUploadDescriptor.java @@ -15,6 +15,7 @@ import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.common.json.JsonNodeHolder; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanAssessmentTypeDescriptor.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanAssessmentTypeDescriptor.java index ae68f721e2..37e4a798a6 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanAssessmentTypeDescriptor.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanAssessmentTypeDescriptor.java @@ -17,7 +17,11 @@ import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastLegacyDescriptor; import com.fortify.cli.fod.sast_scan.helper.FoDScanConfigSastDescriptor; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; @Reflectable @NoArgsConstructor @AllArgsConstructor @Data @ToString diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanHelper.java index 275a7d6fce..22fc8a28eb 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanHelper.java @@ -13,6 +13,18 @@ package com.fortify.cli.fod._common.scan.helper; +import static java.util.function.Predicate.not; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanUtils; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -21,7 +33,12 @@ import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.fod._common.rest.FoDUrls; -import com.fortify.cli.fod._common.scan.helper.dast.*; +import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedHelper; +import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedSetupBaseRequest; +import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedSetupGraphQlRequest; +import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedSetupGrpcRequest; +import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedSetupOpenApiRequest; +import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedSetupPostmanRequest; import com.fortify.cli.fod._common.util.FoDEnums; import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastAutomatedDescriptor; import com.fortify.cli.fod.release.helper.FoDReleaseAssessmentTypeDescriptor; @@ -29,17 +46,11 @@ import com.fortify.cli.fod.rest.lookup.helper.FoDLookupDescriptor; import com.fortify.cli.fod.rest.lookup.helper.FoDLookupHelper; import com.fortify.cli.fod.rest.lookup.helper.FoDLookupType; + import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; import lombok.SneakyThrows; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeanUtils; - -import java.util.*; - -import static java.util.function.Predicate.not; public class FoDScanHelper { @Getter diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanStatus.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanStatus.java index 226a2ec41a..1ae845aa51 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanStatus.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanStatus.java @@ -13,12 +13,12 @@ package com.fortify.cli.fod._common.scan.helper; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - import java.util.ArrayList; import java.util.stream.Stream; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + public enum FoDScanStatus { Not_Started(1), In_Progress(2), Completed(3), Canceled(4), Waiting(5), Scheduled(6), Queued(7); diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedHelper.java index f5a1caf35f..85e9defa53 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedHelper.java @@ -24,7 +24,7 @@ import com.fortify.cli.fod._common.scan.helper.FoDStartScanResponse; import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastAutomatedDescriptor; import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; -import kong.unirest.HttpRequest; + import kong.unirest.UnirestInstance; import lombok.Getter; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupBaseRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupBaseRequest.java index 02345c867e..16189414ab 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupBaseRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupBaseRequest.java @@ -16,7 +16,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.fod._common.util.FoDEnums; -import lombok.*; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; @Reflectable @NoArgsConstructor @AllArgsConstructor diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupGraphQlRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupGraphQlRequest.java index b01549d1c5..88e12faf66 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupGraphQlRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupGraphQlRequest.java @@ -15,6 +15,7 @@ import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.fod._common.util.FoDEnums; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupGrpcRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupGrpcRequest.java index e0f8317fe8..9826a8a8d8 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupGrpcRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupGrpcRequest.java @@ -15,6 +15,7 @@ import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.fod._common.util.FoDEnums; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupOpenApiRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupOpenApiRequest.java index ce71e1386c..283c60e227 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupOpenApiRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupOpenApiRequest.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.formkiq.graalvm.annotations.Reflectable; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupPostmanRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupPostmanRequest.java index 5351522f19..26d749d293 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupPostmanRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupPostmanRequest.java @@ -13,15 +13,16 @@ package com.fortify.cli.fod._common.scan.helper.dast; +import java.util.ArrayList; + import com.formkiq.graalvm.annotations.Reflectable; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import java.util.ArrayList; - @EqualsAndHashCode(callSuper = true) @Reflectable @NoArgsConstructor @AllArgsConstructor @Data @SuperBuilder diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupWebsiteRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupWebsiteRequest.java index d8e868ab04..92612da1c6 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupWebsiteRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupWebsiteRequest.java @@ -13,13 +13,19 @@ package com.fortify.cli.fod._common.scan.helper.dast; +import java.util.ArrayList; + import com.fasterxml.jackson.annotation.JsonInclude; import com.formkiq.graalvm.annotations.Reflectable; -import com.fortify.cli.fod._common.util.FoDEnums; -import lombok.*; -import lombok.experimental.SuperBuilder; -import java.util.ArrayList; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; @EqualsAndHashCode(callSuper = true) @Reflectable @NoArgsConstructor @AllArgsConstructor diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupWorkflowRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupWorkflowRequest.java index bf93eed2b5..5c16c22f46 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupWorkflowRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupWorkflowRequest.java @@ -13,13 +13,19 @@ package com.fortify.cli.fod._common.scan.helper.dast; +import java.util.ArrayList; + import com.fasterxml.jackson.annotation.JsonInclude; import com.formkiq.graalvm.annotations.Reflectable; -import com.fortify.cli.fod._common.util.FoDEnums; -import lombok.*; -import lombok.experimental.SuperBuilder; -import java.util.ArrayList; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; @EqualsAndHashCode(callSuper = true) @Reflectable @NoArgsConstructor @AllArgsConstructor diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_main/cli/cmd/FoDCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_main/cli/cmd/FoDCommands.java index a405465b59..3cbb6ede6c 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_main/cli/cmd/FoDCommands.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_main/cli/cmd/FoDCommands.java @@ -15,6 +15,7 @@ import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; import com.fortify.cli.fod._common.session.cli.cmd.FoDSessionCommands; import com.fortify.cli.fod.access_control.cli.cmd.FoDAccessControlCommands; +import com.fortify.cli.fod.action.cli.cmd.FoDActionCommands; import com.fortify.cli.fod.app.cli.cmd.FoDAppCommands; import com.fortify.cli.fod.dast_scan.cli.cmd.FoDDastScanCommands; import com.fortify.cli.fod.mast_scan.cli.cmd.FoDMastScanCommands; @@ -42,6 +43,7 @@ // - If it makes sense to 'group' related entities, like app, microservice // and release FoDSessionCommands.class, + FoDActionCommands.class, FoDAccessControlCommands.class, FoDAppCommands.class, FoDMicroserviceCommands.class, diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionCommands.java new file mode 100644 index 0000000000..834c82319b --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionCommands.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.action.cli.cmd; + +import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; + +import picocli.CommandLine.Command; + +@Command( + name = "action", + subcommands = { + FoDActionGetCommand.class, + FoDActionListCommand.class, + FoDActionRunCommand.class, + } +) +public class FoDActionCommands extends AbstractContainerCommand { +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionGetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionGetCommand.java new file mode 100644 index 0000000000..3f17f674d3 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionGetCommand.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.action.cli.cmd; + +import com.fortify.cli.common.action.cli.cmd.AbstractActionGetCommand; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; + +import picocli.CommandLine.Command; + +@Command(name = OutputHelperMixins.Get.CMD_NAME) +public class FoDActionGetCommand extends AbstractActionGetCommand { + @Override + protected final String getType() { + return "FoD"; + } +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionListCommand.java new file mode 100644 index 0000000000..9b312544a3 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionListCommand.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.action.cli.cmd; + +import com.fortify.cli.common.action.cli.cmd.AbstractActionListCommand; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = OutputHelperMixins.List.CMD_NAME) +public class FoDActionListCommand extends AbstractActionListCommand { + @Getter @Mixin OutputHelperMixins.List outputHelper; + + @Override + protected final String getType() { + return "FoD"; + } +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionRunCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionRunCommand.java new file mode 100644 index 0000000000..90237bccdb --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionRunCommand.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.action.cli.cmd; + +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.action.cli.cmd.AbstractActionRunCommand; +import com.fortify.cli.common.action.helper.ActionRunner; +import com.fortify.cli.common.action.helper.ActionRunner.IActionRequestHelper.BasicActionRequestHelper; +import com.fortify.cli.common.action.helper.ActionRunner.ParameterTypeConverterArgs; +import com.fortify.cli.common.output.product.IProductHelper; +import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; +import com.fortify.cli.common.spring.expression.SpelHelper; +import com.fortify.cli.fod._common.rest.helper.FoDProductHelper; +import com.fortify.cli.fod._common.session.cli.mixin.FoDUnirestInstanceSupplierMixin; +import com.fortify.cli.fod.release.helper.FoDReleaseHelper; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = "run") +public class FoDActionRunCommand extends AbstractActionRunCommand { + @Getter @Mixin private FoDUnirestInstanceSupplierMixin unirestInstanceSupplier; + + @Override + protected final String getType() { + return "FoD"; + } + + @Override + protected void configure(ActionRunner templateRunner, SimpleEvaluationContext context) { + templateRunner + .addParameterConverter("release_single", this::loadRelease) + .addRequestHelper("fod", new FoDDataExtractRequestHelper(unirestInstanceSupplier::getUnirestInstance, FoDProductHelper.INSTANCE)); + context.setVariable("fod", new FoDSpelFunctions(templateRunner)); + } + + @RequiredArgsConstructor + public final class FoDSpelFunctions { + private final ActionRunner templateRunner; + public String issueBrowserUrl(ObjectNode issue) { + var deepLinkExpression = baseUrl() + +"/redirect/Issues/${vulnId}"; + return templateRunner.getSpelEvaluator().evaluate(SpelHelper.parseTemplateExpression(deepLinkExpression), issue, String.class); + } + public String releaseBrowserUrl(ObjectNode appversion) { + var deepLinkExpression = baseUrl() + +"/redirect/Releases/${releaseId}"; + return templateRunner.getSpelEvaluator().evaluate(SpelHelper.parseTemplateExpression(deepLinkExpression), appversion, String.class); + } + public String appBrowserUrl(ObjectNode appversion) { + var deepLinkExpression = baseUrl() + +"/redirect/Applications/${applicationId}"; + return templateRunner.getSpelEvaluator().evaluate(SpelHelper.parseTemplateExpression(deepLinkExpression), appversion, String.class); + } + private String baseUrl() { + return unirestInstanceSupplier.getSessionDescriptor().getUrlConfig().getUrl() + .replaceAll("/+$", ""); + } + } + + private final JsonNode loadRelease(String nameOrId, ParameterTypeConverterArgs args) { + args.getProgressWriter().writeProgress("Loading release %s", nameOrId); + var result = FoDReleaseHelper.getReleaseDescriptor(unirestInstanceSupplier.getUnirestInstance(), nameOrId, ":", true); + args.getProgressWriter().writeProgress("Loaded release %s", result.getQualifiedName()); + return result.asJsonNode(); + } + + private static final class FoDDataExtractRequestHelper extends BasicActionRequestHelper { + public FoDDataExtractRequestHelper(IUnirestInstanceSupplier unirestInstanceSupplier, IProductHelper productHelper) { + super(unirestInstanceSupplier, productHelper); + } + } +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanGetConfigCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanGetConfigCommand.java index 5be56e060c..ba570ce6d3 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanGetConfigCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanGetConfigCommand.java @@ -16,6 +16,7 @@ import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanConfigGetCommand; import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedHelper; import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastAutomatedDescriptor; + import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Command; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupApiCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupApiCommand.java index 1eca358e00..2e63473ad5 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupApiCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupApiCommand.java @@ -12,6 +12,9 @@ */ package com.fortify.cli.fod.dast_scan.cli.cmd; +import java.util.ArrayList; +import java.util.Collections; + import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanSetupCommand; @@ -20,6 +23,7 @@ import com.fortify.cli.fod._common.scan.helper.FoDScanType; import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedSetupBaseRequest; import com.fortify.cli.fod._common.util.FoDEnums; + import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -27,10 +31,6 @@ import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; - @Command(name = FoDOutputHelperMixins.SetupApi.CMD_NAME) @CommandGroup("*-scan-setup") public class FoDDastAutomatedScanSetupApiCommand extends AbstractFoDScanSetupCommand { @Getter @Mixin private FoDOutputHelperMixins.SetupWorkflow outputHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWebsiteCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWebsiteCommand.java index e00ee85d13..0e1402ceff 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWebsiteCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWebsiteCommand.java @@ -12,6 +12,9 @@ */ package com.fortify.cli.fod.dast_scan.cli.cmd; +import java.util.ArrayList; +import java.util.Set; + import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.rest.FoDUrls; @@ -21,6 +24,7 @@ import com.fortify.cli.fod._common.scan.helper.FoDScanType; import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedSetupWebsiteRequest; import com.fortify.cli.fod._common.util.FoDEnums; + import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -28,9 +32,6 @@ import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; -import java.util.ArrayList; -import java.util.Set; - @Command(name = FoDOutputHelperMixins.SetupWebsite.CMD_NAME) @CommandGroup("*-scan-setup") public class FoDDastAutomatedScanSetupWebsiteCommand extends AbstractFoDScanSetupCommand { @Getter @Mixin private FoDOutputHelperMixins.SetupWebsite outputHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWorkflowCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWorkflowCommand.java index ea5c2d2ba0..55fcb13874 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWorkflowCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWorkflowCommand.java @@ -12,6 +12,8 @@ */ package com.fortify.cli.fod.dast_scan.cli.cmd; +import java.util.ArrayList; + import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.rest.FoDUrls; @@ -21,6 +23,7 @@ import com.fortify.cli.fod._common.scan.helper.FoDScanType; import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedSetupWorkflowRequest; import com.fortify.cli.fod._common.util.FoDEnums; + import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -28,8 +31,6 @@ import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; -import java.util.ArrayList; - @Command(name = FoDOutputHelperMixins.SetupWorkflow.CMD_NAME) @CommandGroup("*-scan-setup") public class FoDDastAutomatedScanSetupWorkflowCommand extends AbstractFoDScanSetupCommand { @Getter @Mixin private FoDOutputHelperMixins.SetupWorkflow outputHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanStartCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanStartCommand.java index 88bb4ecbce..f1fad284df 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanStartCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanStartCommand.java @@ -14,12 +14,12 @@ package com.fortify.cli.fod.dast_scan.cli.cmd; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; -import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanStartCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanDescriptor; import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastAutomatedHelper; import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastAutomatedDescriptor; import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; + import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Command; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastLegacyScanStartCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastLegacyScanStartCommand.java index eaaf56fe5e..010f2dff89 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastLegacyScanStartCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastLegacyScanStartCommand.java @@ -20,7 +20,6 @@ import java.util.Optional; import java.util.Properties; -import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastLegacyHelper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -38,6 +37,7 @@ import com.fortify.cli.fod._common.scan.helper.dast.FoDScanDastLegacyStartRequest; import com.fortify.cli.fod._common.util.FoDEnums; import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastLegacyDescriptor; +import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastLegacyHelper; import com.fortify.cli.fod.release.helper.FoDReleaseAssessmentTypeDescriptor; import com.fortify.cli.fod.release.helper.FoDReleaseAssessmentTypeHelper; import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanFileUploadCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanFileUploadCommand.java index f2a11c9521..d244383d88 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanFileUploadCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanFileUploadCommand.java @@ -16,6 +16,7 @@ import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanFileUploadCommand; import com.fortify.cli.fod._common.scan.cli.mixin.FoDDastFileTypeMixins; + import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/helper/FoDScanConfigDastAutomatedDescriptor.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/helper/FoDScanConfigDastAutomatedDescriptor.java index 2cdc60f022..2395c555b6 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/helper/FoDScanConfigDastAutomatedDescriptor.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/helper/FoDScanConfigDastAutomatedDescriptor.java @@ -15,6 +15,7 @@ import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.common.json.JsonNodeHolder; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportCommands.java index 77992813c4..c019f2e725 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportCommands.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportCommands.java @@ -14,6 +14,7 @@ import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; import com.fortify.cli.common.variable.DefaultVariablePropertyName; + import picocli.CommandLine; @CommandLine.Command(name = "report", diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportCreateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportCreateCommand.java index a7fc3e6224..f7a12048a5 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportCreateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportCreateCommand.java @@ -16,7 +16,6 @@ import com.fortify.cli.common.cli.util.EnvSuffix; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; -import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; @@ -26,6 +25,7 @@ import com.fortify.cli.fod.report.helper.FoDReportCreateRequest; import com.fortify.cli.fod.report.helper.FoDReportFormatType; import com.fortify.cli.fod.report.helper.FoDReportHelper; + import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Command; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportDeleteCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportDeleteCommand.java index 433c4cfdf9..2cd1972a15 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportDeleteCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportDeleteCommand.java @@ -15,12 +15,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; -import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.report.cli.mixin.FoDReportResolverMixin; import com.fortify.cli.fod.report.helper.FoDReportDescriptor; import com.fortify.cli.fod.report.helper.FoDReportHelper; + import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Command; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportDownloadCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportDownloadCommand.java index 7c0034d4cd..fb32fffecb 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportDownloadCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportDownloadCommand.java @@ -12,16 +12,18 @@ *******************************************************************************/ package com.fortify.cli.fod.report.cli.cmd; +import java.nio.file.StandardCopyOption; + import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.cli.mixin.CommonOptionMixins; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; -import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.report.cli.mixin.FoDReportResolverMixin; import com.fortify.cli.fod.report.helper.FoDReportDescriptor; import com.fortify.cli.fod.report.helper.FoDReportHelper; + import kong.unirest.GetRequest; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -29,8 +31,6 @@ import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; -import java.nio.file.StandardCopyOption; - @Command(name = OutputHelperMixins.Download.CMD_NAME) public class FoDReportDownloadCommand extends AbstractFoDJsonNodeOutputCommand implements IActionCommandResultSupplier { @Getter @Mixin private OutputHelperMixins.Download outputHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportGetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportGetCommand.java index 37104bfbee..3a191ab1b1 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportGetCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportGetCommand.java @@ -14,10 +14,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; -import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.report.cli.mixin.FoDReportResolverMixin; -import com.fortify.cli.fod.report.helper.FoDReportHelper; + import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Command; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportListCommand.java index fbd3bafcb8..713f371cfe 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportListCommand.java @@ -12,16 +12,14 @@ *******************************************************************************/ package com.fortify.cli.fod.report.cli.cmd; -import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; -import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.rest.query.IServerSideQueryParamGeneratorSupplier; import com.fortify.cli.common.rest.query.IServerSideQueryParamValueGenerator; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDBaseRequestOutputCommand; 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.report.helper.FoDReportHelper; + import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportTemplateListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportTemplateListCommand.java index ac6b24140e..e4a916915d 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportTemplateListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportTemplateListCommand.java @@ -20,6 +20,7 @@ import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.report.helper.FoDReportTemplateGroupType; import com.fortify.cli.fod.report.helper.FoDReportTemplateHelper; + import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportWaitForCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportWaitForCommand.java index 5be0048ebe..6196696d47 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportWaitForCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/cmd/FoDReportWaitForCommand.java @@ -12,23 +12,23 @@ *******************************************************************************/ package com.fortify.cli.fod.report.cli.cmd; +import java.util.Set; + import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.rest.cli.cmd.AbstractWaitForCommand; import com.fortify.cli.common.rest.wait.WaitHelper.WaitHelperBuilder; -import com.fortify.cli.fod._common.rest.helper.FoDProductHelper; import com.fortify.cli.fod._common.scan.helper.FoDScanStatus; import com.fortify.cli.fod._common.session.cli.mixin.FoDUnirestInstanceSupplierMixin; import com.fortify.cli.fod.report.cli.mixin.FoDReportResolverMixin; import com.fortify.cli.fod.report.helper.FoDReportStatus; import com.fortify.cli.fod.report.helper.FoDReportStatus.FoDReportStatusIterable; + import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; -import java.util.Set; - @Command(name = OutputHelperMixins.WaitFor.CMD_NAME) public class FoDReportWaitForCommand extends AbstractWaitForCommand { @Getter @Mixin private FoDUnirestInstanceSupplierMixin unirestInstanceSupplier; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/mixin/FoDReportResolverMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/mixin/FoDReportResolverMixin.java index b59fe0087e..a34e55aaa2 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/mixin/FoDReportResolverMixin.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/mixin/FoDReportResolverMixin.java @@ -12,19 +12,19 @@ *******************************************************************************/ package com.fortify.cli.fod.report.cli.mixin; +import java.util.Collection; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import com.fasterxml.jackson.databind.JsonNode; -import com.fortify.cli.common.cli.util.EnvSuffix; import com.fortify.cli.fod.report.helper.FoDReportDescriptor; import com.fortify.cli.fod.report.helper.FoDReportHelper; + import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; -import java.util.Collection; -import java.util.stream.Collectors; -import java.util.stream.Stream; - public class FoDReportResolverMixin { public static abstract class AbstractFoDReportResolverMixin { diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/mixin/FoDReportTemplateByNameOrIdResolverMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/mixin/FoDReportTemplateByNameOrIdResolverMixin.java index 9173b68f9f..33458516f1 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/mixin/FoDReportTemplateByNameOrIdResolverMixin.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/cli/mixin/FoDReportTemplateByNameOrIdResolverMixin.java @@ -15,6 +15,7 @@ import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.fod.report.helper.FoDReportTemplateDescriptor; import com.fortify.cli.fod.report.helper.FoDReportTemplateHelper; + import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Option; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportCreateRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportCreateRequest.java index f4453634ed..b719398a54 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportCreateRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportCreateRequest.java @@ -13,7 +13,12 @@ package com.fortify.cli.fod.report.helper; import com.formkiq.graalvm.annotations.Reflectable; -import lombok.*; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; @Reflectable @NoArgsConstructor @AllArgsConstructor @Getter @ToString @Builder diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportDescriptor.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportDescriptor.java index e9483aebd9..3ccb325ca3 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportDescriptor.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportDescriptor.java @@ -14,6 +14,7 @@ import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.common.json.JsonNodeHolder; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportHelper.java index 27185a44b6..040522b349 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportHelper.java @@ -18,6 +18,7 @@ import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.output.transform.fields.RenameFieldsTransformer; import com.fortify.cli.fod._common.rest.FoDUrls; + import kong.unirest.UnirestInstance; import lombok.Getter; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportStatus.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportStatus.java index 9531a844eb..11a25a3edb 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportStatus.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportStatus.java @@ -12,12 +12,12 @@ *******************************************************************************/ package com.fortify.cli.fod.report.helper; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - import java.util.ArrayList; import java.util.stream.Stream; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + public enum FoDReportStatus { Started(1), Completed(2), Failed(3), Queued(4); diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportTemplateDescriptor.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportTemplateDescriptor.java index 087eb713e8..f397d0d8e6 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportTemplateDescriptor.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportTemplateDescriptor.java @@ -14,6 +14,7 @@ import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.common.json.JsonNodeHolder; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportTemplateHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportTemplateHelper.java index 66d71db05f..887b0b5ac1 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportTemplateHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/report/helper/FoDReportTemplateHelper.java @@ -12,6 +12,9 @@ *******************************************************************************/ package com.fortify.cli.fod.report.helper; +import java.util.Optional; +import java.util.stream.Stream; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -20,12 +23,10 @@ import com.fortify.cli.common.output.transform.fields.RenameFieldsTransformer; import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.fod._common.rest.FoDUrls; + import kong.unirest.UnirestInstance; import lombok.Getter; -import java.util.Optional; -import java.util.stream.Stream; - public class FoDReportTemplateHelper { @Getter diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/bitbucket-sast-report.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/bitbucket-sast-report.yaml new file mode 100644 index 0000000000..ae9c68f0a4 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/bitbucket-sast-report.yaml @@ -0,0 +1,108 @@ +usage: + header: Generate a BitBucket Code Insights report listing FoD SAST vulnerabilities. + description: | + For information on how to import this report into BitBucket, see + https://support.atlassian.com/bitbucket-cloud/docs/code-insights/ + +defaults: + requestTarget: fod + +parameters: + - name: report-output + description: "Report output location; either 'stdout', 'stderr' or file name. Default value: bb-fortify-report.json" + required: false + defaultValue: bb-fortify-report.json + - name: annotations-output + description: "Annotations output location; either 'stdout', 'stderr' or file name. Default value: bb-fortify-annotations.json" + required: false + defaultValue: bb-fortify-annotations.json + - name: release + description: "Required release id or :[:]" + type: release_single + +steps: + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities?limit=50 + query: + filters: scantype:Static + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/details + do: + - updatePartialOutputs: + - name: annotations + +outputWriters: + - outputName: annotations + to: ${parameters['annotations-output']} + - outputName: report + to: ${parameters['report-output']} + +outputs: + - name: annotations + contents: ${partialOutputs.annotations?:{}} + - name: report + contents: + # uuid: + title: Fortify Scan Report + details: Fortify on Demand detected ${parameters.release.issueCount} ${parameters.release.issueCount==1 ? 'vulnerability':'vulnerabilities'} + #external_id: + reporter: Fortify on Demand + link: ${#fod.releaseBrowserUrl(parameters.release)} + # remote_link_enabled: + logo_url: https://bitbucket.org/workspaces/fortifysoftware/avatar + report_type: SECURITY + result: ${parameters.release.isPassed ? 'PASSED':'FAILED'} + data: + - type: DATE + title: Last Static Scan # Apparently BB is very strict on how TZ is presented, so we always provide UTC date/time + value: ${#formatDateTimewithZoneIdAsUTC("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'",parameters.release.staticScanDate?:'1970-01-01T00:00:00',parameters.release.serverZoneId)} + - type: NUMBER + title: Rating + value: ${parameters.release.rating} + - type: NUMBER + title: Critical (SAST) + value: ${parameters.release.staticCritical} + - type: NUMBER + title: Critical (Overall) + value: ${parameters.release.critical} + - type: NUMBER + title: High (SAST) + value: ${parameters.release.staticHigh} + - type: NUMBER + title: High (Overall) + value: ${parameters.release.high} + - type: NUMBER + title: Medium (SAST) + value: ${parameters.release.staticMedium} + - type: NUMBER + title: Medium (Overall) + value: ${parameters.release.medium} + - type: NUMBER + title: Low (SAST) + value: ${parameters.release.staticLow} + - type: NUMBER + title: Low (Overall) + value: ${parameters.release.low} + +partialOutputs: + - name: annotations + contents: + external_id: FTFY-${issue.id} + # uuid: + annotation_type: VULNERABILITY + path: ${issue.primaryLocationFull} + line: ${issue.lineNumber==0?1:issue.lineNumber} + summary: ${issue.category} + details: ${#htmlToText(issue_details?.summary)} + # result: PASSED|FAILED|SKIPPED|IGNORED + severity: ${(issue.severityString matches "(Critical|High|Medium|Low)") ? issue.severityString.toUpperCase():"LOW"} + link: ${#fod.issueBrowserUrl(issue)} + # created_on: + # updated_on: diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-sast-report.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-sast-report.yaml new file mode 100644 index 0000000000..53f32442d3 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-sast-report.yaml @@ -0,0 +1,126 @@ +usage: + header: Generate a GitHub Code Scanning report listing FoD DAST vulnerabilities. + description: | + For information on how to import this report into GitHub, see + https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github + +defaults: + requestTarget: fod + +parameters: + - name: output + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gh-fortify-sast.sarif" + required: false + defaultValue: gh-fortify-sast.sarif + - name: release + description: "Required release id or :[:]" + type: release_single + +steps: + - progress: Loading static scan summary + - requests: + - name: staticScanSummary + uri: /api/v3/scans/${parameters.release.currentStaticScanId}/summary + if: parameters.release.currentStaticScanId!=null + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities?limit=50 + query: + filters: scantype:Static + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/details + - name: recommendations + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/recommendations + - name: traces + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/traces + do: + - updatePartialOutputs: + - name: rules + - name: results + +outputWriters: + - outputName: github-sast-report + to: ${parameters.output} + +outputs: + - name: github-sast-report + contents: + "[$schema]": https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json + version: '2.1.0' + runs: + - tool: + driver: + name: 'Fortify on Demand' + version: SCA ${staticScanSummary?.staticScanSummaryDetails?.engineVersion?:'version unknown'}; Rulepack ${staticScanSummary?.staticScanSummaryDetails?.rulePackVersion?:'version unknown'} + rules: ${partialOutputs.rules?:{}} + results: ${#check(partialOutputs.results?.size()>1000 , "GitHub does not support importing more than 1000 vulnerabilities. Please clean the scan results or update vulnerability search criteria.")?partialOutputs.results?:{}:{}} + +partialOutputs: + - name: rules + contents: + id: ${issue.id+''} + shortDescription: + text: ${issue.category} + fullDescription: + text: ${#htmlToText(issue.details?.summary)} + help: + text: | + ${#htmlToText(issue.details?.explanation)} + + ${#htmlToText(issue.recommendations?.recommendations)} + + + For more information, see ${#fod.issueBrowserUrl(issue)} + properties: + tags: ${{issue.severityString}} + precision: ${(issue.severityString matches "(Critical|Medium)") ? "high":"low" } + security-severity: ${{Critical:10.0,High:8.9,Medium:6.9,Low:3.9}.get(issue.severityString)+''} + + - name: results + contents: + ruleId: ${issue.id+''} + message: + text: ${#htmlToText(issue.details?.summary)} + level: ${(issue.severityString matches "(Critical|High)") ? "warning":"note" } + partialFingerprints: + issueInstanceId: ${issue.instanceId} + locations: + - physicalLocation: + artifactLocation: + uri: ${issue.primaryLocationFull} + region: + startLine: ${issue.lineNumber==0?1:issue.lineNumber} + endLine: ${issue.lineNumber==0?1:issue.lineNumber} + startColumn: ${1} # Needs to be specified as an expression in order to end up as integer instead of string in JSON + endColumn: ${80} + codeFlows: |- + ${ + issue.traces==null ? {} + : + {{ + threadFlows: issue.traces.![{ + locations: traceEntries?.![{ + location: { + message: { + text: #htmlToText(displayText).replaceAll(" ", " ") + }, + physicalLocation: { + artifactLocation: { + uri: location + }, + region: { + startLine: lineNumber==0?1:lineNumber + } + } + } + }] + }] + }} + } + diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-dast-report.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-dast-report.yaml new file mode 100644 index 0000000000..9c521496de --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-dast-report.yaml @@ -0,0 +1,150 @@ +usage: + header: Generate a GitLab DAST report listing FoD DAST vulnerabilities. + description: | + For information on how to import this report into GitLab, see + https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdast + +defaults: + requestTarget: fod + +parameters: + - name: output + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-dast.json" + required: false + defaultValue: gl-fortify-dast.json + - name: release + description: "Required release id or :[:]" + type: release_single + +steps: + - progress: Loading dynamic scan summary + - requests: + - name: dynamicScanSummary + uri: /api/v3/scans/${parameters.release.currentDynamicScanId}/summary + if: parameters.release.currentDynamicScanId!=null + - name: siteTree + uri: /api/v3/scans/${parameters.release.currentDynamicScanId}/site-tree + if: parameters.release.currentDynamicScanId!=null + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities?limit=50 + query: + filters: scantype:Dynamic + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/details + - name: recommendations + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/recommendations + - name: request_response + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/request-response + do: + - updatePartialOutputs: + - name: vulnerabilities + +outputWriters: + - outputName: gitlab-dast-report + to: ${parameters.output} + +outputs: + - name: gitlab-dast-report + contents: + schema: https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/v15.0.0/dist/dast-report-format.json + version: 15.0.0 + scan: + start_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", dynamicScanSummary?.startedDateTime?:'1970-01-01T00:00:00')} + end_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", dynamicScanSummary?.completedDateTime?:'1970-01-01T00:00:00')} + status: ${parameters.release.dynamicAnalysisStatusTypeId==2?'success':'failure'} + type: dast + analyzer: + id: FoD-DAST + name: Fortify on Demand + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: WebInspect ${dynamicScanSummary?.scanToolVersion?:'version unknown'} + vendor: + name: Fortify + scanner: + id: FoD-DAST + name: Fortify on Demand + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: WebInspect ${dynamicScanSummary?.scanToolVersion?:'version unknown'} + vendor: + name: Fortify + scanned_resources: |- + ${ + siteTree==null ? {} + : siteTree.![{ + method: method, + url: scheme+'://'+host+':'+port+path, + type: 'url' + }] + } + vulnerabilities: ${partialOutputs.vulnerabilities?:{}} + # remediations: ... + +partialOutputs: + - name: vulnerabilities + contents: + id: ${issue.vulnId} + category: dast + name: ${issue.category} + message: ${issue.category} + description: ${#abbreviate(#htmlToText(issue.details?.summary), 15000)} + cve: 'N/A' + severity: ${{'Critical':'Critical','High':'High','Medium':'Medium','Low':'Low','Best Practice':'Info','Info':'Info'}.get(issue.severityString)?:'Unknown'} + confidence: ${(issue.severityString matches "(Critical|Medium)") ? "High":"Low" } + solution: ${#abbreviate(#htmlToText(issue.details?.explanation)+'\n\n'+#htmlToText(issue.recommendations?.recommendations), 7000)} + scanner: + id: FoD-DAST + name: Fortify on Demand + identifiers: |- + ${{ + { + name: "Instance id: "+issue.instanceId, + url: issue.deepLink, + type: "issueInstanceId", + value: issue.instanceId + } + }} + links: + - name: Additional issue details, including analysis trace, in Fortify on Demand + url: ${issue.deepLink} + # evidence: # TODO + # source: + # id: + # name: + # url: + # summary: + # request: + # headers: + # - name: + # value: + # method: + # url: + # body: + # response: + # headers: + # - name: + # value: + # reason_phrase: OK|Internal Server Error|... + # status_code: 200|500|... + # body: + # supporting_messages: + # - name: + # request: ... + # response: ... + location: + hostname: ${#uriPart(issue.primaryLocationFull, 'serverUrl')?:''} + method: ${#substringBefore(issue.request_response?.requestContent,' ')?:''} + param: ${#uriPart(issue.primaryLocationFull, 'query')?:''} + path: ${#uriPart(issue.primaryLocationFull, 'path')?:''} + # assets: + # - type: http_session|postman + # name: + # url: link to asset in build artifacts + # discovered_at: 2020-01-28T03:26:02.956 + \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-sast-report.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-sast-report.yaml new file mode 100644 index 0000000000..929ff3b19d --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-sast-report.yaml @@ -0,0 +1,104 @@ +usage: + header: Generate a GitLab SAST report listing FoD SAST vulnerabilities. + description: | + For information on how to import this report into GitLab, see + https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportssast + +defaults: + requestTarget: fod + +parameters: + - name: output + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-sast.json" + required: false + defaultValue: gl-fortify-sast.json + - name: release + description: "Required release id or :[:]" + type: release_single + +steps: + - progress: Loading static scan summary + - requests: + - name: staticScanSummary + uri: /api/v3/scans/${parameters.release.currentStaticScanId}/summary + if: parameters.release.currentStaticScanId!=null + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities?limit=50 + query: + filters: scantype:Static + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/details + - name: recommendations + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/recommendations + do: + - updatePartialOutputs: + - name: vulnerabilities + + +outputWriters: + - outputName: gitlab-sast-report + to: ${parameters.output} + +outputs: + - name: gitlab-sast-report + contents: + schema: https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/v15.0.0/dist/sast-report-format.json + version: 15.0.0 + scan: + start_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", staticScanSummary?.startedDateTime?:'1970-01-01T00:00:00')} + end_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", staticScanSummary?.completedDateTime?:'1970-01-01T00:00:00')} + status: ${parameters.release.staticAnalysisStatusTypeId==2?'success':'failure'} + type: sast + analyzer: + id: FoD-SAST + name: Fortify on Demand + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: SCA ${staticScanSummary?.staticScanSummaryDetails?.engineVersion?:'version unknown'}; Rulepack ${staticScanSummary?.staticScanSummaryDetails?.rulePackVersion?:'version unknown'} + vendor: + name: Fortify + scanner: + id: FoD-SAST + name: Fortify on Demand + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: SCA ${staticScanSummary?.staticScanSummaryDetails?.engineVersion?:'version unknown'}; Rulepack ${staticScanSummary?.staticScanSummaryDetails?.rulePackVersion?:'version unknown'} + vendor: + name: Fortify + vulnerabilities: ${partialOutputs.vulnerabilities?:{}} + +partialOutputs: + - name: vulnerabilities + contents: + category: sast + confidence: ${(issue.severityString matches "(Critical|Medium)") ? "High":"Low" } + description: ${#abbreviate(#htmlToText(issue.details?.summary), 15000)} + id: ${issue.vulnId} + cve: 'N/A' + identifiers: |- + ${{ + { + name: "Instance id: "+issue.instanceId, + url: issue.deepLink, + type: "issueInstanceId", + value: issue.instanceId + } + }} + location: + file: ${issue.primaryLocationFull} + start_line: ${issue.lineNumber} + links: + - name: Additional issue details, including analysis trace, in Fortify on Demand + url: ${issue.deepLink} + message: ${issue.category} + name: ${issue.category} + scanner: + id: FoD-SAST + name: Fortify on Demand + severity: ${issue.severityString} + solution: ${#abbreviate(#htmlToText(issue.details?.explanation)+'\n\n'+#htmlToText(issue.recommendations?.recommendations), 7000)} diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sonarqube-sast-report.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sonarqube-sast-report.yaml new file mode 100644 index 0000000000..f3f0c91238 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sonarqube-sast-report.yaml @@ -0,0 +1,61 @@ +usage: + header: Generate a SonarQube External Issues report listing FoD SAST vulnerabilities. + description: | + For information on how to import this report into SonarQube, see + https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/importing-external-issues/external-analyzer-reports/ + +defaults: + requestTarget: fod + +parameters: + - name: output + description: "Optional output location; either 'stdout', 'stderr' or file name. Default value: sq-fortify-sast.json" + required: false + defaultValue: sq-fortify-sast.json + - name: file-path-prefix + description: "Optional prefix for issue file paths" + required: false + defaultValue: "" + - name: release + description: "Required release id or :[:]" + type: release_single + +steps: + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities?limit=50 + query: + filters: scantype:Static + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + forEach: + name: issue + do: + - updatePartialOutputs: + - name: issues + +outputWriters: + - outputName: sq-sast-report + to: ${parameters.output} + +outputs: + - name: sq-sast-report + contents: + issues: ${partialOutputs.issues?:{}} + +partialOutputs: + - name: issues + contents: + engineId: FortifyOnDemand + ruleId: ${issue.category} + severity: ${{'Critical':'CRITICAL','High':'MAJOR','Medium':'MINOR','Low':'INFO'}.get(issue.severityString)} + type: VULNERABILITY + primaryLocation: + message: ${issue.category} - ${#fod.issueBrowserUrl(issue)} + filePath: ${parameters['file-path-prefix']}${issue.primaryLocationFull} + textRange: + startLine: ${issue.lineNumber==0?1:issue.lineNumber} + # effortMinutes: + # secondaryLocations: + \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index 020931067f..4497256ed4 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -151,8 +151,13 @@ fcli.fod.rest.lookup.usage.description = Use this command to retrieve the values fcli.fod.rest.lookup.[0] = The type of lookup items to return. Valid values: ${COMPLETION-CANDIDATES}. \ Leave empty to list all the valid lookup items of the REST API. +# fcli fod action +fcli.fod.action.usage.header = Manage & run FoD-related actions. +fcli.fod.action.run.usage.header = Run an action. +fcli.fod.action.list.usage.header = List built-in actions. +fcli.fod.action.get.usage.header = Get action contents. -### For the "fod access-control" command ### +# fcli fod access-control fcli.fod.access-control.usage.header = Manage FoD users & groups. fcli.fod.access-control.list-roles.usage.header = List user roles. @@ -196,7 +201,7 @@ fcli.fod.access-control.user.output.header.roleName = Role -### For the "fod app" command ### +# fcli fod app fcli.fod.app.usage.header = Manage FoD applications. fcli.fod.app.output.header.applicationId = Id @@ -244,7 +249,7 @@ fcli.fod.app.update.criticality = The business criticality of the application. fcli.fod.app.update.attr = Attribute id or name and its value to set on the application. fcli.fod.app.list-scans.usage.header = List scans for a given application. -### For the "fod microservice" command ### +# fcli fod microservice fcli.fod.microservice.usage.header = Manage FoD application microservices. fcli.fod.microservice.output.header.microserviceId = Id @@ -264,7 +269,7 @@ fcli.fod.microservice.update.usage.header = Update an existing application micro fcli.fod.microservice.update.name = The updated name for the microservice. fcli.fod.microservice.update.invalid-parameter = Unable to resolve application name and microservice name. -### For the "fod release" command ### +# fcli fod release fcli.fod.release.usage.header = Manage FoD application releases. fcli.fod.release.output.header.releaseId = Id @@ -298,14 +303,14 @@ fcli.fod.release.list-assessment-types.usage.header = List assessment types for fcli.fod.release.list-assessment-types.scan-types = Comma-separated list of scan types for which to list assessment types. Default value: ${DEFAULT-VALUE}. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.release.list-scans.usage.header = List scans for a given release. -### For the "fod assessment-type" command ### +# fcli fod assessment-type fcli.fod.assessment-type.usage.header = Manage FoD assessment types. fcli.fod.assessment-type.output.header.assessmentTypeId = Id fcli.fod.assessment-type.output.header.unitInfo = Units fcli.fod.assessment-type.list.usage.header = List assessment types. fcli.fod.assessment-type.list.scan-types = Comma-separated list of scan types for which to list assessment types. Default value: ${DEFAULT-VALUE}. Valid values: ${COMPLETION-CANDIDATES}. -### For the "fod entitlement" command ### +# fcli fod entitlement fcli.fod.entitlement.usage.header = View FoD entitlements. fcli.fod.entitlement.entitlement-id = Entitlement Id fcli.fod.entitlement.output.header.entitlementId = Id @@ -314,7 +319,7 @@ fcli.fod.entitlement.output.header.unitInfo = Units Remaining fcli.fod.entitlement.list.usage.header = List entitlements. fcli.fod.entitlement.get.usage.header = Get entitlement. -### For the "fod scan" command ### +# fcli fod scan fcli.fod.scan.usage.header = Manage FoD scans. fcli.fod.scan.usage.description = The commands listed below allow for generically managing scans on FoD. \ Commands for setting up, starting, downloading and importing existing scan results can be found on the \ @@ -345,7 +350,7 @@ fcli.fod.scan.wait-for.until = Wait until either any or all scans match. If neit fcli.fod.scan.wait-for.while = Wait while either any or all scans match. fcli.fod.scan.wait-for.any-state = One or more scan states against which to match the given scans. -### For the "fod sast-scan" command ### +# fcli fod sast-scan fcli.fod.sast-scan.usage.header = Manage FoD SAST scans. fcli.fod.sast-scan.description = The commands listed below allow for starting and managing SAST scans on FoD. fcli.fod.sast-scan.output.header.scanId = Id @@ -415,7 +420,7 @@ fcli.fod.sast-scan.download.file = File path and name where to save the FPR file fcli.fod.sast-scan.download-latest.usage.header = Download latest scan results from release. fcli.fod.sast-scan.download-latest.file = File path and name where to save the FPR file. -### For the "fod dast-scan" command ### +# fcli fod dast-scan fcli.fod.dast-scan.usage.header = Manage FoD DAST scans. fcli.fod.dast-scan.description = The commands listed below allow for starting and managing DAST scans on FoD. fcli.fod.dast-scan.output.header.scanId = Id @@ -578,7 +583,7 @@ fcli.fod.dast-scan.setup-api.network-username = ${fcli.fod.dast-scan.setup-websi fcli.fod.dast-scan.setup-api.network-password = ${fcli.fod.dast-scan.setup-website.network-password} fcli.fod.dast-scan.setup-api.false-positive-removal = ${fcli.fod.dast-scan.setup-website.false-positive-removal} -### For the "fod mast-scan" command ### +# fcli fod mast-scan fcli.fod.mast-scan.usage.header = Manage FoD MAST scans. fcli.fod.mast-scan.description = The commands listed below allow for starting and managing MAST scans on FoD. fcli.fod.mast-scan.output.header.scanId = Id @@ -636,7 +641,7 @@ fcli.fod.mast-scan.download.file = File path and name where to save the FPR file fcli.fod.mast-scan.download-latest.usage.header = Download latest scan results from release. fcli.fod.mast-scan.download-latest.file = File path and name where to save the FPR file. -### For the "fod oss-scan" command ### +# fcli fod oss-scan fcli.fod.oss-scan.usage.header = Manage FoD OSS scans. fcli.fod.oss-scan.description = The commands listed below allow for starting and managing OSS scans on FoD. fcli.fod.oss-scan.output.header.scanId = Id @@ -671,9 +676,8 @@ fcli.fod.oss-scan.download.file = File path and name where to save the SBOM file fcli.fod.oss-scan.download-latest.usage.header = Download latest scan results from release. fcli.fod.oss-scan.download-latest.file = File path and name where to save the SBOM file. -### For the "fod report" command ### +# fcli fod report fcli.fod.report.usage.header = Manage FoD reports. - fcli.fod.report.output.header.reportId = Id fcli.fod.report.output.header.reportName = Name fcli.fod.report.output.header.reportStatusType = Status @@ -728,6 +732,7 @@ fcli.fod.entitlement-consumed = Warning: all units of the entitlement have been fcli.env.default.prefix=FCLI_DEFAULT # Table output columns configuration +fcli.fod.action.list.output.table.options = name,usage.header fcli.fod.access-control.user.output.table.options = userId,userName,firstName,lastName,email,roleName fcli.fod.access-control.group.output.table.options = id,name,assignedUsersCount,assignedApplicationsCount fcli.fod.access-control.role.output.table.options = id,name diff --git a/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/data_extract/templates/SC-SAST.zip b/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/data_extract/templates/SC-SAST.zip deleted file mode 100644 index 1b82ba32a22f924c0992f82213ceb0041b8bc8f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200 zcmWIWW@h1H00Dj%j|eaWN=P!uFr;UeWRxc9Cg-Q5>J}#_=H+GPrR!BD=H`Tka56CW zHXlg^;nE6j21b?_%nS@*BEXxGNsbwpp%QRAmNbHxc&uQBSb0l6t F0RY0&CsY6c diff --git a/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties b/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties index 9c79d702ee..a08ea38f98 100644 --- a/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties +++ b/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties @@ -100,7 +100,8 @@ fcli.sc-sast.session.list.usage.description.1 = For sessions created using user expired. Similarly, any changes to token validity will not be reflected in the output of this command. %n fcli.sc-sast.session.list.usage.description.2 = For sessions created using a pre-generated token, fcli cannot \ display session expiration date or status, as SSC doesn't allow for obtaining this information. - + + # fcli sc-sast scan fcli.sc-sast.scan.usage.header = Manage ScanCentral SAST scans. fcli.sc-sast.scan.cancel.usage.header = Cancel a previously submitted scan request. diff --git a/fcli-core/fcli-ssc/build.gradle b/fcli-core/fcli-ssc/build.gradle index 8e063597b9..566400f9f2 100644 --- a/fcli-core/fcli-ssc/build.gradle +++ b/fcli-core/fcli-ssc/build.gradle @@ -1 +1,6 @@ +task zipResources_templates(type: Zip) { + destinationDirectory = file("${buildDir}/generated-zip-resources/com/fortify/cli/ssc") + archiveFileName = "actions.zip" + from("${projectDir}/src/main/resources/com/fortify/cli/ssc/actions/zip") +} apply from: "${sharedGradleScriptsDir}/fcli-module.gradle" \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/helper/SSCInputTransformer.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/helper/SSCInputTransformer.java index 6e42eb6ee3..60f60d6835 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/helper/SSCInputTransformer.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/helper/SSCInputTransformer.java @@ -16,6 +16,6 @@ public class SSCInputTransformer { public static final JsonNode getDataOrSelf(JsonNode json) { - return json.has("data") ? json.get("data") : json; + return json!=null && json.has("data") ? json.get("data") : json; } } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_main/cli/cmd/SSCCommands.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_main/cli/cmd/SSCCommands.java index e9c36420a4..b7d23b5025 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_main/cli/cmd/SSCCommands.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_main/cli/cmd/SSCCommands.java @@ -15,6 +15,7 @@ import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; import com.fortify.cli.ssc._common.session.cli.cmd.SSCSessionCommands; import com.fortify.cli.ssc.access_control.cli.cmd.SSCAccessControlCommands; +import com.fortify.cli.ssc.action.cli.cmd.SSCActionCommands; import com.fortify.cli.ssc.alert.cli.cmd.SSCAlertCommands; import com.fortify.cli.ssc.app.cli.cmd.SSCAppCommands; import com.fortify.cli.ssc.appversion.cli.cmd.SSCAppVersionCommands; @@ -43,6 +44,7 @@ // 'rest' has a different header ('Interact with' compared to most // other commands ('Manage'). SSCSessionCommands.class, + SSCActionCommands.class, SSCAccessControlCommands.class, SSCAlertCommands.class, SSCAppCommands.class, diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionCommands.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionCommands.java new file mode 100644 index 0000000000..98b6e5f848 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionCommands.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.action.cli.cmd; + +import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; + +import picocli.CommandLine.Command; + +@Command( + name = "action", + subcommands = { + SSCActionRunCommand.class, + SSCActionGetCommand.class, + SSCActionListCommand.class, + } +) +public class SSCActionCommands extends AbstractContainerCommand { +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionGetCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionGetCommand.java new file mode 100644 index 0000000000..93c7e47c2f --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionGetCommand.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.action.cli.cmd; + +import com.fortify.cli.common.action.cli.cmd.AbstractActionGetCommand; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; + +import picocli.CommandLine.Command; + +@Command(name = OutputHelperMixins.Get.CMD_NAME) +public class SSCActionGetCommand extends AbstractActionGetCommand { + @Override + protected final String getType() { + return "SSC"; + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionListCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionListCommand.java new file mode 100644 index 0000000000..907b2c8a81 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionListCommand.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.action.cli.cmd; + +import com.fortify.cli.common.action.cli.cmd.AbstractActionListCommand; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = OutputHelperMixins.List.CMD_NAME) +public class SSCActionListCommand extends AbstractActionListCommand { + @Getter @Mixin OutputHelperMixins.List outputHelper; + + @Override + protected final String getType() { + return "SSC"; + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionRunCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionRunCommand.java new file mode 100644 index 0000000000..30a20909b8 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionRunCommand.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright 2021, 2023 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.action.cli.cmd; + +import java.util.List; + +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.action.cli.cmd.AbstractActionRunCommand; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionValidationException; +import com.fortify.cli.common.action.helper.ActionRunner; +import com.fortify.cli.common.action.helper.ActionRunner.IActionRequestHelper.BasicActionRequestHelper; +import com.fortify.cli.common.action.helper.ActionRunner.ParameterTypeConverterArgs; +import com.fortify.cli.common.output.product.IProductHelper; +import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; +import com.fortify.cli.common.spring.expression.SpelHelper; +import com.fortify.cli.common.util.StringUtils; +import com.fortify.cli.ssc._common.rest.bulk.SSCBulkRequestBuilder; +import com.fortify.cli.ssc._common.rest.helper.SSCProductHelper; +import com.fortify.cli.ssc._common.session.cli.mixin.SSCUnirestInstanceSupplierMixin; +import com.fortify.cli.ssc.appversion.helper.SSCAppVersionHelper; +import com.fortify.cli.ssc.issue.helper.SSCIssueFilterSetHelper; + +import kong.unirest.HttpRequest; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = "run") +public class SSCActionRunCommand extends AbstractActionRunCommand { + @Getter @Mixin private SSCUnirestInstanceSupplierMixin unirestInstanceSupplier; + + @Override + protected final String getType() { + return "SSC"; + } + + @Override + protected void configure(ActionRunner templateRunner, SimpleEvaluationContext context) { + templateRunner + .addParameterConverter("appversion_single", this::loadAppVersion) + .addParameterConverter("filterset", this::loadFilterSet) + .addRequestHelper("ssc", new SSCDataExtractRequestHelper(unirestInstanceSupplier::getUnirestInstance, SSCProductHelper.INSTANCE)); + context.setVariable("ssc", new SSCSpelFunctions(templateRunner)); + } + + @RequiredArgsConstructor + public final class SSCSpelFunctions { + private final ActionRunner templateRunner; + public String issueBrowserUrl(ObjectNode issue, ObjectNode filterset) { + var deepLinkExpression = baseUrl() + +"/html/ssc/version/${projectVersionId}/fix/${id}/?engineType=${engineType}&issue=${issueInstanceId}"; + if ( filterset!=null ) { + deepLinkExpression+="&filterSet="+filterset.get("guid").asText(); + } + return templateRunner.getSpelEvaluator().evaluate(SpelHelper.parseTemplateExpression(deepLinkExpression), issue, String.class); + } + public String appversionBrowserUrl(ObjectNode appversion) { + var deepLinkExpression = baseUrl() + +"/html/ssc/index.jsp#!/version/${id}/fix"; + return templateRunner.getSpelEvaluator().evaluate(SpelHelper.parseTemplateExpression(deepLinkExpression), appversion, String.class); + } + private String baseUrl() { + return unirestInstanceSupplier.getSessionDescriptor().getUrlConfig().getUrl() + .replaceAll("/+$", ""); + } + + } + + private final JsonNode loadAppVersion(String nameOrId, ParameterTypeConverterArgs args) { + args.getProgressWriter().writeProgress("Loading application version %s", nameOrId); + var result = SSCAppVersionHelper.getRequiredAppVersion(unirestInstanceSupplier.getUnirestInstance(), nameOrId, ":"); + args.getProgressWriter().writeProgress("Loaded application version %s", result.getAppAndVersionName()); + return result.asJsonNode(); + } + + private final JsonNode loadFilterSet(String titleOrId, ParameterTypeConverterArgs args) { + var progressMessage = StringUtils.isBlank(titleOrId) + ? "Loading default filter set" + : String.format("Loading filter set %s", titleOrId); + args.getProgressWriter().writeProgress(progressMessage); + var parameter = args.getParameter(); + var typeParameters = parameter.getTypeParameters(); + var appVersionIdExpression = typeParameters==null ? null : typeParameters.get("appversion.id"); + if ( appVersionIdExpression==null ) { + appVersionIdExpression = SpelHelper.parseTemplateExpression("${appversion?.id}"); + } + var appVersionId = args.getSpelEvaluator().evaluate(appVersionIdExpression, args.getParameters(), String.class); + if ( StringUtils.isBlank(appVersionId) ) { + throw new ActionValidationException(String.format("Template parameter %s requires ${%s} to be available", parameter.getName(), appVersionIdExpression.getExpressionString())); + } + var filterSetDescriptor = new SSCIssueFilterSetHelper(unirestInstanceSupplier.getUnirestInstance(), appVersionId).getDescriptorByTitleOrId(titleOrId, false); + if ( filterSetDescriptor==null ) { + throw new IllegalArgumentException("Unknown filter set: "+titleOrId); + } + return filterSetDescriptor.asJsonNode(); + } + + private static final class SSCDataExtractRequestHelper extends BasicActionRequestHelper { + public SSCDataExtractRequestHelper(IUnirestInstanceSupplier unirestInstanceSupplier, IProductHelper productHelper) { + super(unirestInstanceSupplier, productHelper); + } + + @Override + public void executeSimpleRequests(List requestDescriptors) { + if ( requestDescriptors.size()==1 ) { + var rd = requestDescriptors.get(0); + createRequest(rd).asObject(JsonNode.class).ifSuccess(r->rd.getResponseConsumer().accept(r.getBody())); + } else { + var bulkRequestBuilder = new SSCBulkRequestBuilder(); + requestDescriptors.forEach(r->bulkRequestBuilder.request(createRequest(r), r.getResponseConsumer())); + bulkRequestBuilder.execute(getUnirestInstance()); + } + } + + private HttpRequest createRequest(ActionRequestDescriptor requestDescriptor) { + var request = getUnirestInstance(). request(requestDescriptor.getMethod(), requestDescriptor.getUri()) + .queryString(requestDescriptor.getQueryParams()); + var body = requestDescriptor.getBody(); + return body==null ? request : request.body(body); + } + } +} diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bitbucket-sast-report.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bitbucket-sast-report.yaml new file mode 100644 index 0000000000..35c309cdbf --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bitbucket-sast-report.yaml @@ -0,0 +1,141 @@ +usage: + header: Generate a BitBucket Code Insights report listing SSC SAST vulnerabilities. + description: | + For information on how to import this report into BitBucket, see + https://support.atlassian.com/bitbucket-cloud/docs/code-insights/ + +defaults: + requestTarget: ssc + +parameters: + - name: report-output + description: "Report output location; either 'stdout', 'stderr' or file name. Default value: bb-fortify-report.json" + required: false + defaultValue: bb-fortify-report.json + - name: annotations-output + description: "Annotations output location; either 'stdout', 'stderr' or file name. Default value: bb-fortify-annotations.json" + required: false + defaultValue: bb-fortify-annotations.json + - name: appversion + description: "Required application version id or :" + type: appversion_single + - name: filterset + description: "Filter set name or guid from which to load issue data. Default value: Default filter set for given application version" + required: false + type: filterset + +steps: + - progress: Loading latest static scan + - requests: + - name: artifacts + uri: /api/v1/projectVersions/${parameters.appversion.id}/artifacts + type: paged + query: + embed: scans + forEach: + name: artifact + breakIf: lastStaticScan!=null + do: + - set: + - name: lastStaticScan + value: ${artifact._embed.scans?.^[type=='SCA']} + - progress: Loading issue counts + - requests: + - name: fpo_counts_sca + uri: /api/v1/projectVersions/${parameters.appversion.id}/issueGroups + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + groupingtype: 11111111-1111-1111-1111-111111111150 + filterset: ${parameters.filterset.guid} + - requests: + - name: fpo_counts_total + uri: /api/v1/projectVersions/${parameters.appversion.id}/issueGroups + query: + groupingtype: 11111111-1111-1111-1111-111111111150 + filterset: ${parameters.filterset.guid} + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues?limit=100 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + filterset: ${parameters.filterset.guid} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - updatePartialOutputs: + - name: annotations + +outputWriters: + - outputName: annotations + to: ${parameters['annotations-output']} + - outputName: report + to: ${parameters['report-output']} + +outputs: + - name: annotations + contents: ${partialOutputs.annotations?:{}} + - name: report + contents: + # uuid: + title: Fortify Scan Report + details: Fortify detected ${partialOutputs.annotations?.size()?:0} static ${partialOutputs.annotations?.size()==1 ? 'vulnerability':'vulnerabilities'} + #external_id: + reporter: Fortify Static Code Analyzer ${lastStaticScan?.engineVersion?:''} + link: ${#ssc.appversionBrowserUrl(parameters.appversion)} + # remote_link_enabled: + logo_url: https://bitbucket.org/workspaces/fortifysoftware/avatar + report_type: SECURITY + result: 'PASSED' + data: + - type: TEXT + title: Application Version + value: ${parameters.appversion.project.name} - ${parameters.appversion.name} + - type: DATE + title: Last Static Scan + value: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", lastStaticScan?.uploadDate?:'1970-01-01T00:00:00')} + - type: NUMBER + title: Critical (SAST) + value: ${fpo_counts_sca.^[id=='Critical']?.visibleCount?:0} + - type: NUMBER + title: Critical (Overall) + value: ${fpo_counts_total.^[id=='Critical']?.visibleCount?:0} + - type: NUMBER + title: High (SAST) + value: ${fpo_counts_sca.^[id=='High']?.visibleCount?:0} + - type: NUMBER + title: High (Overall) + value: ${fpo_counts_total.^[id=='High']?.visibleCount?:0} + - type: NUMBER + title: Medium (SAST) + value: ${fpo_counts_sca.^[id=='Medium']?.visibleCount?:0} + - type: NUMBER + title: Medium (Overall) + value: ${fpo_counts_total.^[id=='Medium']?.visibleCount?:0} + - type: NUMBER + title: Low (SAST) + value: ${fpo_counts_sca.^[id=='Low']?.visibleCount?:0} + - type: NUMBER + title: Low (Overall) + value: ${fpo_counts_total.^[id=='Low']?.visibleCount?:0} + +partialOutputs: + - name: annotations + contents: + external_id: FTFY-${issue.id} + # uuid: + annotation_type: VULNERABILITY + path: ${issue.fullFileName} + line: ${issue.lineNumber==0?1:issue.lineNumber} + summary: ${issue.issueName} + details: ${issue.details?.brief} + # result: PASSED|FAILED|SKIPPED|IGNORED + severity: ${issue.friority.toUpperCase()} + link: ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + # created_on: + # updated_on: diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-decoration.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-decoration.yaml new file mode 100644 index 0000000000..31fdfa876f --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-decoration.yaml @@ -0,0 +1,136 @@ +usage: + header: Generate GitHub Pull Request decoration. + description: | + This template generates GitHub Pull Request review comments. + +parameters: + - name: appversion + description: "Required application version id or :" + type: appversion_single + - name: filterset + description: "Filter set name or guid from which to load issue data. Default value: Default filter set for given application version" + required: false + type: filterset + - name: artifact-id + description: "Required artifact id for which to generate PR decorations" + required: true + - name: github-token + description: 'Required GitHub Token' + required: true + - name: github-owner + description: 'Required GitHub repository owner' + required: true + - name: github-repo + description: 'Required GitHub repository' + required: true + - name: pr + description: 'Required PR number' + required: true + - name: commit + description: 'Required commit hash' + required: true + +addRequestTargets: + - name: github + baseUrl: https://api.github.com + headers: + Authorization: Bearer ${parameters['github-token']} + 'X-GitHub-Api-Version': '2022-11-28' + +defaults: + requestTarget: ssc + +steps: + - progress: Loading artifact + - requests: + - if: parameters['artifact-id']!=null + name: artifact + uri: /api/v1/artifacts/${parameters['artifact-id']}?embed=scans + - set: + - name: scanDates + value: ${artifact._embed.scans.![uploadDate]} + - progress: 'ScanDates: ${scanDates.toString()}' + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues?limit=100 + query: + showremoved: true + filterset: ${parameters.filterset.guid} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + if: scanDates.contains(issue.removedDate) || scanDates.contains(issue.foundDate) + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - if: scanDates.contains(issue.removedDate) + updatePartialOutputs: + - name: removedIssues + - if: scanDates.contains(issue.foundDate) && issue.engineType=='SCA' + updatePartialOutputs: + - name: newStaticIssues + - if: scanDates.contains(issue.foundDate) && issue.engineType!='SCA' + updatePartialOutputs: + - name: newOtherIssues + - progress: Sending GitHub request + - updatePartialOutputs: + - name: reviewBody + - name: reviewRequestBody + - requests: + - name: GitHub PR review + method: POST + uri: /repos/${parameters['github-owner']}/${parameters['github-repo']}/pulls/${parameters['pr']}/reviews + target: github + body: ${partialOutputs.reviewRequestBody[0]} + +outputWriters: + - outputName: status + to: stdout + +outputs: + - name: status + contents: | + PR review comments have been added. +partialOutputs: + - name: reviewBody + contents: | + ${partialOutputs.newStaticIssues==null && partialOutputs.newOtherIssues==null + ? "Fortify didn't detect any new potential vulnerabilities" + : "Fortify detected potential vulnerabilities"} + ${partialOutputs.newStaticIssues==null + ? '' + : '### New Issues (SAST)\n\nSee file comments below.'} + ${partialOutputs.newOtherIssues==null + ? '' + : ('### New Issues (non-SAST)\n\n* '+#join('\n* ',partialOutputs.newOtherIssues))} + ${partialOutputs.removedIssues==null + ? '' + : ('### Removed Issues\n\n* '+#join('\n* ',partialOutputs.removedIssues))} + - name: reviewRequestBody + contents: + owner: ${parameters['github-owner']} + repo: ${parameters['github-repo']} + pull_number: ${parameters['pr']} + commit_id: ${parameters['commit']} + body: ${partialOutputs.reviewBody[0]} + event: COMMENT + comments: ${partialOutputs.newStaticIssues} + - name: newStaticIssues + contents: + path: ${issue.fullFileName} + line: ${issue.lineNumber==0?1:issue.lineNumber} + body: | +

Security Scanning / Fortify SAST

+

${issue.details.friority} - ${issue.details.issueName}

+

${issue.details.brief}

+
+

More detailed information

+ - name: newOtherIssues + contents: > + [${issue.fullFileName}${issue.lineNumber==null?'':':'+issue.lineNumber} - ${issue.issueName}](${#ssc.issueBrowserUrl(issue,parameters.filterset)}) + - name: removedIssues + contents: > + [${issue.fullFileName}${issue.lineNumber==null?'':':'+issue.lineNumber} - ${issue.issueName}](${#ssc.issueBrowserUrl(issue,parameters.filterset)}) \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-sast-report.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-sast-report.yaml new file mode 100644 index 0000000000..d899a65d26 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-sast-report.yaml @@ -0,0 +1,134 @@ +usage: + header: Generate a GitHub Code Scanning report listing SSC DAST vulnerabilities. + description: | + For information on how to import this report into GitHub, see + https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github + +defaults: + requestTarget: ssc + +parameters: + - name: output + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gh-fortify-sast.sarif" + required: false + defaultValue: gh-fortify-sast.sarif + - name: appversion + description: "Required application version id or :" + type: appversion_single + - name: filterset + description: "Filter set name or guid from which to load issue data. Default value: Default filter set for given application version" + required: false + type: filterset + +steps: + - progress: Loading latest static scan + - requests: + - name: artifacts + uri: /api/v1/projectVersions/${parameters.appversion.id}/artifacts + type: paged + query: + embed: scans + forEach: + name: artifact + breakIf: lastStaticScan!=null + do: + - set: + - name: lastStaticScan + value: ${artifact._embed.scans?.^[type=='SCA']} + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues?limit=100 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + filterset: ${parameters.filterset.guid} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - updatePartialOutputs: + - name: rules + - name: results + +outputWriters: + - outputName: github-sast-report + to: ${parameters.output} + +outputs: + - name: github-sast-report + contents: + "$schema": https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json + version: '2.1.0' + runs: + - tool: + driver: + name: 'Fortify SCA' + version: ${lastStaticScan?.engineVersion?:'unknown'} + rules: ${partialOutputs.rules?:{}} + results: ${#check(partialOutputs.results?.size()>1000 , "GitHub does not support importing more than 1000 vulnerabilities. Please clean the scan results or update vulnerability search criteria.")?partialOutputs.results?:{}:{}} + +partialOutputs: + - name: rules + contents: + id: ${issue.id+''} + shortDescription: + text: ${issue.issueName} + fullDescription: + text: ${issue.details?.brief} + help: + text: | + ${#htmlToText(issue.details?.detail)} + + ${#htmlToText(issue.details?.recommendation)} + + + For more information, see ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + properties: + tags: ${{issue.friority}} + precision: ${(issue.friority matches "(Critical|Medium)") ? "high":"low" } + security-severity: ${{Critical:10.0,High:8.9,Medium:6.9,Low:3.9}.get(issue.friority)+''} + - name: results + contents: + ruleId: ${issue.id+''} + message: + text: ${issue.details?.brief} + level: ${(issue.friority matches "(Critical|High)") ? "warning":"note" } + partialFingerprints: + issueInstanceId: ${issue.issueInstanceId} + locations: + - physicalLocation: + artifactLocation: + uri: ${issue.fullFileName} + region: + startLine: ${issue.lineNumber==0?1:issue.lineNumber} + endLine: ${issue.lineNumber==0?1:issue.lineNumber} + startColumn: ${1} # Needs to be specified as an expression in order to end up as integer instead of string in JSON + endColumn: ${80} + codeFlows: |- + ${ + issue.details?.traceNodes==null ? {} + : + {{ + threadFlows: issue.details?.traceNodes.![{ + locations: #this.![{ + location: { + message: { + text: text + }, + physicalLocation: { + artifactLocation: { + uri: fullPath + }, + region: { + startLine: line==0?1:line + } + } + } + }] + }] + }} + } \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-dast-report.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-dast-report.yaml new file mode 100644 index 0000000000..15d0e3570f --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-dast-report.yaml @@ -0,0 +1,155 @@ +usage: + header: Generate a GitLab DAST report listing SSC DAST vulnerabilities. + description: | + For information on how to import this report into GitLab, see + https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdast + +defaults: + requestTarget: ssc + +parameters: + - name: output + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-dast.json" + required: false + defaultValue: gl-fortify-dast.json + - name: appversion + description: "Required application version id or :" + type: appversion_single + - name: filterset + description: "Filter set name or guid from which to load issue data. Default value: Default filter set for given application version" + required: false + type: filterset + +steps: + - progress: Loading latest dynamic scan + - requests: + - name: artifacts + uri: /api/v1/projectVersions/${parameters.appversion.id}/artifacts + type: paged + query: + embed: scans + forEach: + name: artifact + breakIf: lastDynamicScan!=null + do: + - set: + - name: lastDynamicScan + value: ${artifact._embed.scans?.^[type=='WEBINSPECT']} + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues?limit=100 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:WEBINSPECT + filterset: ${parameters.filterset.guid} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - updatePartialOutputs: + - name: vulnerabilities + +outputWriters: + - outputName: gitlab-dast-report + to: ${parameters.output} + +outputs: + - name: gitlab-dast-report + contents: + schema: https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/v15.0.0/dist/dast-report-format.json + version: 15.0.0 + scan: + start_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", lastDynamicScan?.uploadDate?:'1970-01-01T00:00:00')} + end_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", lastDynamicScan?.uploadDate?:'1970-01-01T00:00:00')} + status: success + type: dast + analyzer: + id: fortify-webinspect + name: Fortify WebInspect + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: WebInspect ${lastDynamicScan?.engineVersion?:'version unknown'} + vendor: + name: Fortify + scanner: + id: fortify-webinspect + name: Fortify WebInspect + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: WebInspect ${lastDynamicScan?.engineVersion?:'version unknown'} + vendor: + name: Fortify + scanned_resources: ${{}} +# scanned_resources: |- +# ${ +# release.siteTree==null ? {} +# : release.siteTree.![{ +# method: method, +# url: scheme+'://'+host+':'+port+path, +# type: 'url' +# }] +# ] + vulnerabilities: ${partialOutputs.vulnerabilities?:{}} + # remediations: ... + +partialOutputs: + - name: vulnerabilities + contents: + id: ${issue.issueInstanceId} + category: sast + name: ${issue.issueName} + message: ${issue.issueName} + description: ${#abbreviate(#htmlToText(issue.details?.brief), 15000)} + cve: 'N/A' + severity: ${issue.friority} + confidence: ${(issue.friority matches "(Critical|Medium)") ? "High":"Low" } + solution: ${#abbreviate(#htmlToText(issue.details?.brief)+'\n\n'+#htmlToText(issue.details?.recommendation), 7000)} + scanner: + id: fortify-webinspect + name: Fortify WebInspect + identifiers: + - name: "Instance id: ${issue.issueInstanceId}" + type: issueInstanceId + value: ${issue.issueInstanceId} + url: ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + links: + - name: Additional issue.details, including analysis trace, in Software Security Center + url: ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + - name: SecureCodeWarrior Training + url: ${issue.details?.appSecTrainingUrl} + # evidence: # TODO + # source: + # id: + # name: + # url: + # summary: + # request: + # headers: + # - name: + # value: + # method: + # url: + # body: + # response: + # headers: + # - name: + # value: + # reason_phrase: OK|Internal Server Error|... + # status_code: 200|500|... + # body: + # supporting_messages: + # - name: + # request: ... + # response: ... + location: + hostname: ${#uriPart(issue.details.url, 'serverUrl')?:''} + method: ${issue.details.method?:''} + param: ${issue.details.attackPayload?:''} + path: ${#uriPart(issue.details.url, 'path')?:''} + # assets: + # - type: http_session|postman + # name: + # url: link to asset in build artifacts + # discovered_at: 2020-01-28T03:26:02.956 \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-debricked-report.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-debricked-report.yaml new file mode 100644 index 0000000000..a40427102d --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-debricked-report.yaml @@ -0,0 +1,115 @@ +usage: + header: Generate a GitLab Dependency Scanning report listing SSC Debricked vulnerabilities. + description: | + For information on how to import this report into GitLab, see + https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdependency_scanning + +defaults: + requestTarget: ssc + +parameters: + - name: output + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-debricked-depscan.json" + required: false + defaultValue: gl-fortify-debricked-depscan.json + - name: appversion + description: "Required application version id or :" + type: appversion_single + - name: filterset + description: "Filter set name or guid from which to load issue data. Default value: Default filter set for given application version" + required: false + type: filterset + +steps: + - progress: Loading latest Debricked scan + - requests: + - name: artifacts + uri: /api/v1/projectVersions/${parameters.appversion.id}/artifacts + type: paged + query: + embed: scans + forEach: + name: artifact + breakIf: lastDebrickedScan!=null + do: + - set: + - name: lastDebrickedScan + value: ${artifact._embed.scans?.^[type=='DEBRICKED']} + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues?limit=100 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:DEBRICKED + filterset: ${parameters.filterset.guid} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - updatePartialOutputs: + - name: vulnerabilities + +outputWriters: + - outputName: gitlab-debricked-report + to: ${parameters.output} + +outputs: + - name: gitlab-debricked-report + contents: + schema: https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/v15.0.0/dist/dependency-scanning-report-format.json + version: 15.0.0 + scan: + start_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", lastDebrickedScan?.uploadDate?:'1970-01-01T00:00:00')} + end_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", lastDebrickedScan?.uploadDate?:'1970-01-01T00:00:00')} + status: success + type: dependency_scanning + analyzer: + id: fortify-debricked + name: Fortify/Debricked + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: Debricked Fortify Parser Plugin ${lastDebrickedScan?.engineVersion?:'version unknown'} + vendor: + name: Fortify+Debricked + scanner: + id: fortify-debricked + name: Fortify/Debricked + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: Debricked Fortify Parser Plugin ${lastDebrickedScan?.engineVersion?:'version unknown'} + vendor: + name: Fortify+Debricked + dependency_files: ${{}} + vulnerabilities: ${partialOutputs.vulnerabilities?:{}} + +partialOutputs: + - name: vulnerabilities + contents: + id: ${issue.issueInstanceId} + category: dependency_scanning + name: ${issue.issueName} + message: ${issue.issueName} + description: ${#abbreviate(#htmlToText(issue.details?.brief), 15000)} + cve: ${issue.details?.customAttributes?.externalId} + severity: ${issue.friority} + confidence: ${(issue.friority matches "(Critical|Medium)") ? "High":"Low" } + scanner: + id: fortify-debricked + name: Fortify/Sonaytype + identifiers: + - name: "Instance id: ${issue.issueInstanceId}" + type: issueInstanceId + value: ${issue.issueInstanceId} + url: ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + links: + - name: Additional issue.details, including analysis trace, in Software Security Center + url: ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + - name: CWE URL + url: ${issue.details?.customAttributes?.externalUrl} + location: + file: ${issue.fullFileName} + dependency: + package.name: ${issue.details?.customAttributes?.componentName > '' ? issue.details?.customAttributes?.componentName :'Not Set'} + version: ${issue.details?.customAttributes?.componentVersion > '' ? issue.details?.customAttributes?.componentVersion :'Not Set'} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sast-report.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sast-report.yaml new file mode 100644 index 0000000000..3f38fcf7b5 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sast-report.yaml @@ -0,0 +1,113 @@ +usage: + header: Generate a GitLab SAST report listing SSC SAST vulnerabilities. + description: | + For information on how to import this report into GitLab, see + https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportssast + +defaults: + requestTarget: ssc + +parameters: + - name: output + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-sast.json" + required: false + defaultValue: gl-fortify-sast.json + - name: appversion + description: "Required application version id or :" + type: appversion_single + - name: filterset + description: "Filter set name or guid from which to load issue data. Default value: Default filter set for given application version" + required: false + type: filterset + +steps: + - progress: Loading latest static scan + - requests: + - name: artifacts + uri: /api/v1/projectVersions/${parameters.appversion.id}/artifacts + type: paged + query: + embed: scans + forEach: + name: artifact + breakIf: lastStaticScan!=null + do: + - set: + - name: lastStaticScan + value: ${artifact._embed.scans?.^[type=='SCA']} + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues?limit=100 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + filterset: ${parameters.filterset.guid} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - updatePartialOutputs: + - name: vulnerabilities + +outputWriters: + - outputName: gitlab-sast-report + to: ${parameters.output} + +outputs: + - name: gitlab-sast-report + contents: + schema: https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/v15.0.0/dist/sast-report-format.json + version: 15.0.0 + scan: + start_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", lastStaticScan?.uploadDate?:'1970-01-01T00:00:00')} + end_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", lastStaticScan?.uploadDate?:'1970-01-01T00:00:00')} + status: success + type: sast + analyzer: + id: fortify-sca + name: Fortify SCA + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: SCA ${lastStaticScan?.engineVersion?:'version unknown'} + vendor: + name: Fortify + scanner: + id: fortify-sca + name: Fortify SCA + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: SCA ${lastStaticScan?.engineVersion?:'version unknown'} + vendor: + name: Fortify + vulnerabilities: ${partialOutputs.vulnerabilities?:{}} + +partialOutputs: + - name: vulnerabilities + contents: + id: ${issue.issueInstanceId} + category: sast + name: ${issue.issueName} + message: ${issue.issueName} + description: ${#abbreviate(#htmlToText(issue.details?.brief), 15000)} + cve: 'N/A' + severity: ${issue.friority} + confidence: ${(issue.friority matches "(Critical|Medium)") ? "High":"Low"} + solution: ${#abbreviate(#htmlToText(issue.details?.detail)+'\n\n'+#htmlToText(issue.details?.recommendation), 7000)} + scanner: + id: fortify-sca + name: Fortify SCA + identifiers: + - name: "Instance id: ${issue.issueInstanceId}" + type: issueInstanceId + value: ${issue.issueInstanceId} + url: ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + links: + - name: Additional issue details, including analysis trace, in Software Security Center + url: ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + - name: SecureCodeWarrior Training + url: ${issue.details?.appSecTrainingUrl} + location: + file: ${issue.fullFileName} + start_line: ${issue.lineNumber} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sonatype-report.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sonatype-report.yaml new file mode 100644 index 0000000000..b3efa28077 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sonatype-report.yaml @@ -0,0 +1,115 @@ +usage: + header: Generate a GitLab Dependency Scanning report listing SSC Sonatype vulnerabilities. + description: | + For information on how to import this report into GitLab, see + https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdependency_scanning + +defaults: + requestTarget: ssc + +parameters: + - name: output + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-sonatype-depscan.json" + required: false + defaultValue: gl-fortify-sonatype-depscan.json + - name: appversion + description: "Required application version id or :" + type: appversion_single + - name: filterset + description: "Filter set name or guid from which to load issue data. Default value: Default filter set for given application version" + required: false + type: filterset + +steps: + - progress: Loading latest Sonatype scan + - requests: + - name: artifacts + uri: /api/v1/projectVersions/${parameters.appversion.id}/artifacts + type: paged + query: + embed: scans + forEach: + name: artifact + breakIf: lastSonatypeScan!=null + do: + - set: + - name: lastSonatypeScan + value: ${artifact._embed.scans?.^[type=='SONATYPE']} + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues?limit=100 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SONATYPE + filterset: ${parameters.filterset.guid} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - updatePartialOutputs: + - name: vulnerabilities + +outputWriters: + - outputName: gitlab-sonatype-report + to: ${parameters.output} + +outputs: + - name: gitlab-sonatype-report + contents: + schema: https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/v15.0.0/dist/dependency-scanning-report-format.json + version: 15.0.0 + scan: + start_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", lastSonatypeScan?.uploadDate?:'1970-01-01T00:00:00')} + end_time: ${#formatDateTime("yyyy-MM-dd'T'HH:mm:ss", lastSonatypeScan?.uploadDate?:'1970-01-01T00:00:00')} + status: success + type: dependency_scanning + analyzer: + id: fortify-sonatype + name: Fortify/Sonatype + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: Sonatype Fortify Parser Plugin ${lastSonatypeScan?.engineVersion?:'version unknown'} + vendor: + name: Fortify+Sonatype + scanner: + id: fortify-sonatype + name: Fortify/Sonatype + url: https://www.microfocus.com/en-us/products/application-security-testing/overview + version: Sonatype Fortify Parser Plugin ${lastSonatypeScan?.engineVersion?:'version unknown'} + vendor: + name: Fortify+Sonatype + dependency_files: ${{}} + vulnerabilities: ${partialOutputs.vulnerabilities?:{}} + +partialOutputs: + - name: vulnerabilities + contents: + id: ${issue.issueInstanceId} + category: dependency_scanning + name: ${issue.issueName} + message: ${issue.issueName} + description: ${#abbreviate(#htmlToText(issue.details?.brief), 15000)} + cve: 'N/A' + severity: ${issue.friority} + confidence: ${(issue.friority matches "(Critical|Medium)") ? "High":"Low" } + scanner: + id: fortify-sonatype + name: Fortify/Sonaytype + identifiers: + - name: "Instance id: ${issue.issueInstanceId}" + type: issueInstanceId + value: ${issue.issueInstanceId} + url: ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + links: + - name: Additional issue.details, including analysis trace, in Software Security Center + url: ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + - name: CWE URL + url: ${issue.details?.customAttributes?.cweurl} + location: + file: ${issue.fullFileName} + dependency: + package.name: ${issue.details?.customAttributes?.artifact > '' ? issue.details?.customAttributes?.artifact :'Not Set'} + version: ${issue.details?.customAttributes?.version > '' ? issue.details?.customAttributes?.version :'Not Set'} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sonarqube-sast-report.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sonarqube-sast-report.yaml new file mode 100644 index 0000000000..169d33789c --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sonarqube-sast-report.yaml @@ -0,0 +1,65 @@ +usage: + header: Generate a SonarQube External Issues report listing SSC SAST vulnerabilities. + description: | + For information on how to import this report into SonarQube, see + https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/importing-external-issues/external-analyzer-reports/ + +defaults: + requestTarget: ssc + +parameters: + - name: output + description: "Optional output location; either 'stdout', 'stderr' or file name. Default value: sq-fortify-sast.json" + required: false + defaultValue: sq-fortify-sast.json + - name: file-path-prefix + description: "Optional prefix for issue file paths" + required: false + defaultValue: "" + - name: appversion + description: "Required application version id or :" + type: appversion_single + - name: filterset + description: "Optional filter set name or guid from which to load issue data. Default value: Default filter set for given application version" + required: false + type: filterset + +steps: + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues?limit=100 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + filterset: ${parameters.filterset.guid} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + do: + - updatePartialOutputs: + - name: issues + +outputWriters: + - outputName: sq-sast-report + to: ${parameters.output} + +outputs: + - name: sq-sast-report + contents: + issues: ${partialOutputs.issues?:{}} + +partialOutputs: + - name: issues + contents: + engineId: FortifySCA + ruleId: ${issue.issueName} + severity: ${{'Critical':'CRITICAL','High':'MAJOR','Medium':'MINOR','Low':'INFO'}.get(issue.friority)} + type: VULNERABILITY + primaryLocation: + message: ${issue.issueName} - ${#ssc.issueBrowserUrl(issue,parameters.filterset)} + filePath: ${parameters['file-path-prefix']}${issue.fullFileName} + textRange: + startLine: ${issue.lineNumber==0?1:issue.lineNumber} + # effortMinutes: + # secondaryLocations: \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties index 247df255de..4e1651c1fb 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/i18n/SSCMessages.properties @@ -127,7 +127,13 @@ fcli.ssc.rest.call.transform = This option allows for performing custom transfor data based on a Spring Expression Language (SpEL) expression. For example, this allows for retrieving \ data from sub-properties, or using project selection/projection. Note that the expression operates on \ the raw response, as if --no-transform was specified before evaluating the expression. - + +# fcli ssc action +fcli.ssc.action.usage.header = Manage & run SSC-related actions. +fcli.ssc.action.run.usage.header = Run an action. +fcli.ssc.action.list.usage.header = List built-in actions. +fcli.ssc.action.get.usage.header = Get action contents. + # fcli ssc access-control fcli.ssc.access-control.usage.header = Manage SSC users, roles & tokens. fcli.ssc.access-control.create-role.usage.header = Create a role. @@ -466,6 +472,7 @@ fcli.ssc.system-state.wait-for-job.any-state=One or more processing states again fcli.env.default.prefix=FCLI_DEFAULT # Table output columns configuration +fcli.ssc.action.list.output.table.options = name,usage.header fcli.ssc.access-control.role.output.table.options = id,name,builtIn,allApplicationRole fcli.ssc.access-control.permission.output.table.options = id,name fcli.ssc.access-control.token.output.table.options = id,username,type,creationDate,terminalDate,timeRemaining diff --git a/fcli-other/fcli-bom/build.gradle b/fcli-other/fcli-bom/build.gradle index 236a6cc0ed..b6dcdb95e7 100644 --- a/fcli-other/fcli-bom/build.gradle +++ b/fcli-other/fcli-bom/build.gradle @@ -53,7 +53,11 @@ dependencies { api 'org.junit.jupiter:junit-jupiter-params:5.9.3' api 'org.junit.jupiter:junit-jupiter-engine:5.9.3' - //required for unpacking tar.gz (debricked cli) + // Required for unpacking tar.gz (debricked cli) api('org.apache.commons:commons-compress:1.25.0') + + // Used for processing HTML text returned by SSC/FoD endpoints like issue summaries/details/... + api('org.jsoup:jsoup:1.17.2') + } } \ No newline at end of file diff --git a/fcli-other/fcli-gradle/fcli-java.gradle b/fcli-other/fcli-gradle/fcli-java.gradle index ee1eb57ca8..69a9e90ee0 100644 --- a/fcli-other/fcli-gradle/fcli-java.gradle +++ b/fcli-other/fcli-gradle/fcli-java.gradle @@ -7,6 +7,24 @@ java { targetCompatibility = JavaVersion.toVersion("17") } +/* +eclipse { + classpath { + file.whenMerged { cp -> + cp.entries.add( new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/main/zipped-resources', null) ) + } + } +} +*/ +sourceSets { + main { + resources { + exclude '**/zip/*' + exclude '**/zip' + } + } +} + dependencies { implementation platform(project("${fcliBomRef}")) annotationProcessor platform(project("${fcliBomRef}")) @@ -60,8 +78,11 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - //required for unpacking tar.gz (debricked cli) - implementation('org.apache.commons:commons-compress') + // Required for unpacking tar.gz (debricked cli) + implementation('org.apache.commons:commons-compress') + + // Used for processing HTML text returned by SSC/FoD endpoints like issue summaries/details/... + implementation('org.jsoup:jsoup') } compileJava { @@ -81,16 +102,42 @@ test { showStandardStreams = true } } + +ext.generatedZipResourcesDir = "${buildDir}/generated-zip-resources" +/* For some reason the below doesn't work, probably related to https://github.com/gradle/gradle/issues/24368 + * For now, we just have dedicated zip tasks in SSC/FoD build.gradle files +fileTree(dir: 'src/main/zipped-resources') + .visit { e -> if ( e.isDirectory() && e.name=='zip' ) { + def zipTaskName = "zipResources_${e.path.replaceAll('/', '_')}" + def destDir = "$generatedZipResourcesDir/${e.relativePath}" + def zipName = "${e.relativePath.parent.lastName}.zip" + def from = "${e.file.absolutePath}" + println "zipTaskName: $zipTaskName" + println "destDir: $destDir" + println "zipName: $zipName" + println "from: $from" + tasks.create(name: "$zipTaskName", type: Zip) { + destinationDirectory = file("$destDir") + archiveFileName = "$zipName" + from = file("$from") + } + }} +*/ +task generateZipResources(dependsOn: tasks.matching { Task task -> task.name.startsWith("zipResources_")}) +sourceSets.main.output.dir generatedZipResourcesDir, builtBy: generateZipResources // Generate resource-config.json ext.generatedResourceConfigDir = "${buildDir}/generated-resource-config" tasks.register('generateResourceConfig') { + dependsOn = [generateZipResources] doLast { def outputDir = "${generatedResourceConfigDir}/META-INF/native-image/fcli-generated/${project.name}"; mkdir "${outputDir}" def entries = []; fileTree(dir: 'src/main/resources', excludes: ['**/i18n/**', 'META-INF/**']) .visit {e -> if ( !e.isDirectory() ) {entries << '\n {"pattern":"'+e.relativePath+'"}'}}; + fileTree(dir: generatedZipResourcesDir) + .visit {e -> if ( !e.isDirectory() ) {entries << '\n {"pattern":"'+e.relativePath+'"}'}}; if ( entries.size>0 ) { def contents = '{"resources":[' + entries.join(",") + '\n]}'; file("${outputDir}/resource-config.json").text = contents;