diff --git a/fcli-core/fcli-common/build.gradle b/fcli-core/fcli-common/build.gradle index 4f8368616f..6f63caf885 100644 --- a/fcli-core/fcli-common/build.gradle +++ b/fcli-core/fcli-common/build.gradle @@ -1 +1,7 @@ +task zipResources_templates(type: Zip) { + destinationDirectory = file("${buildDir}/generated-zip-resources/com/fortify/cli/common") + archiveFileName = "actions.zip" + from("${projectDir}/src/main/resources//com/fortify/cli/common/actions/zip") +} + apply from: "${sharedGradleScriptsDir}/fcli-java.gradle" \ No newline at end of file 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/AbstractActionHelpCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionHelpCommand.java new file mode 100644 index 0000000000..153cc2e1d2 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionHelpCommand.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * 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.ActionDescriptor; +import com.fortify.cli.common.action.helper.ActionHelper; +import com.fortify.cli.common.action.helper.ActionParameterHelper; +import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; + +import picocli.CommandLine.Parameters; +import picocli.CommandLine.Unmatched; + +public abstract class AbstractActionHelpCommand extends AbstractRunnableCommand implements Runnable { + @Parameters(arity="1", descriptionKey="fcli.action.run.action") private String action; + @Unmatched private String[] actionArgs; // We explicitly ignore any unknown CLI args, to allow for + // users to simply switch between run and help commands. + + @Override + public final void run() { + initMixins(); + var actionDescriptor = ActionHelper.load(getType(), action); + System.out.println(getActionHelp(actionDescriptor)); + } + + private final String getActionHelp(ActionDescriptor action) { + 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(), ActionParameterHelper.getSupportedOptionsTable(action)); + } + + protected abstract String getType(); +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionImportCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionImportCommand.java new file mode 100644 index 0000000000..dda5bd1157 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionImportCommand.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * 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.fortify.cli.common.action.helper.ActionHelper; +import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; +import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier; +import com.fortify.cli.common.util.StringUtils; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; + +public abstract class AbstractActionImportCommand extends AbstractOutputCommand implements IJsonNodeSupplier { + @ArgGroup(exclusive = true, multiplicity = "1") private ImportArgGroup argGroup = new ImportArgGroup(); + private static final class ImportArgGroup { + @ArgGroup(exclusive=false) private ZipArgGroup zipArgGroup = new ZipArgGroup(); + @ArgGroup(exclusive=false) private FileArgGroup fileArgGroup = new FileArgGroup(); + } + private static final class ZipArgGroup { + @Option(names = {"--zip", "-z"}, required = true, descriptionKey="cli.action.import.zip") private String zip; + } + private static final class FileArgGroup { + @Option(names = {"--file", "-f"}, required = true, descriptionKey="cli.action.import.file") private String file; + @Option(names = {"--name", "-n"}, required = false, descriptionKey="cli.action.import.name") private String name; + } + + @Override + public final JsonNode getJsonNode() { + var zip = argGroup.zipArgGroup.zip; + if ( StringUtils.isNotBlank(zip) ) { + return ActionHelper.importZip(getType(), zip); + } else { + return ActionHelper.importSingle(getType(), argGroup.fileArgGroup.name, argGroup.fileArgGroup.file); + } + } + @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/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/AbstractActionResetCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionResetCommand.java new file mode 100644 index 0000000000..cb0f2bcd29 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionResetCommand.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.fortify.cli.common.action.helper.ActionHelper; +import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; +import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier; +import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; + +public abstract class AbstractActionResetCommand extends AbstractOutputCommand implements IJsonNodeSupplier, IActionCommandResultSupplier { + @Override + public final JsonNode getJsonNode() { + return ActionHelper.reset(getType()); + } + @Override + public String getActionCommandResult() { + return "DELETED"; + } + @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..0852f1ef6f --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * 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.ActionHelper; +import com.fortify.cli.common.action.helper.ActionParameterHelper; +import com.fortify.cli.common.action.helper.ActionRunner; +import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; +import com.fortify.cli.common.cli.mixin.CommandHelperMixin; +import com.fortify.cli.common.cli.util.SimpleOptionsParser.OptionsParseResult; +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.ParameterException; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.Unmatched; + +public abstract class AbstractActionRunCommand extends AbstractRunnableCommand implements Runnable { + @Parameters(arity="1", descriptionKey="fcli.action.run.action") private String action; + @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 ProgressWriterFactoryMixin progressWriterFactory; + @Mixin CommandHelperMixin commandHelper; + @Unmatched private String[] actionArgs; + + @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() + .onValidationErrors(this::onValidationErrors) + .action(actionDescriptor) + .progressWriter(progressWriter).build() ) + { + delayedConsoleWriter = run(actionRunner, progressWriter); + } + } + delayedConsoleWriter.run(); + } + + private Runnable run(ActionRunner actionRunner, IProgressWriterI18n progressWriter) { + actionRunner.getSpelEvaluator().configure(context->configure(actionRunner, context)); + progressWriter.writeProgress("Executing action %s", actionRunner.getAction().getName()); + return actionRunner.run(actionArgs); + } + + private ParameterException onValidationErrors(OptionsParseResult optionsParseResult) { + var errorsString = String.join("\n ", optionsParseResult.getValidationErrors()); + var supportedOptionsString = ActionParameterHelper.getSupportedOptionsTable(optionsParseResult.getOptions()); + var msg = String.format("Option errors:\n %s\nSupported options:\n%s\n", errorsString, supportedOptionsString); + return new ParameterException(commandHelper.getCommandSpec().commandLine(), msg); + } + + 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..3b2cb1bef9 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java @@ -0,0 +1,471 @@ +/** + * 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 java.util.stream.Stream; + +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; + /** Whether this is a custom action, set in {@link #postLoad(String)} method */ + private boolean custom; + /** 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; + /** Value templates */ + private List valueTemplates; + @JsonIgnore @Getter(lazy=true) private final Map valueTemplatesByName = + valueTemplates.stream().collect(Collectors.toMap(ActionValueTemplateDescriptor::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, boolean isCustom) { + this.name = name; + this.custom = isCustom; + checkNotNull("action usage", usage, this); + checkNotNull("action steps", steps, 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 ( valueTemplates!=null ) { + valueTemplates.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)); + } + + public static interface IActionIfSupplier { + SimpleExpression get_if(); + } + + /** + * 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; + /** Optional comma-separated CLI aliases */ + private String cliAliases; + /** Required parameter description */ + private String description; + /** Optional parameter type */ + private String type; + /** Optional type parameters*/ + private Map typeParameters; + /** Optional template expression defining the default parameter value if not provided by user */ + private TemplateExpression defaultValue; + /** Boolean indicating whether this parameter is required, default is true */ + private boolean required = true; + + public final void postLoad(ActionDescriptor action) { + checkNotBlank("parameter name", name, this); + checkNotNull("parameter description", getDescription(), this); + // TODO Check no duplicate names; ideally ActionRunner should also verify + // that option names/aliases don't conflict with command options + // like --help/-h, --log-file, ... + } + + public final String[] getCliAliasesArray() { + if ( cliAliases==null ) { return new String[] {}; } + return Stream.of(cliAliases).map(String::trim).toArray(String[]::new); + } + } + + /** + * 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. + * TODO Potentially, later versions may add support for other step types. Some ideas + * for potentially useful steps: + *
    + *
  • if: Execute sub-steps only if condition evaluates to true
  • + *
  • forEach: Execute sub-steps for every value in input array
  • + *
  • throw: Throw an exception (based on if condition), for example on validation errors
  • + *
  • fcli: Run other fcli commands to allow for workflow-oriented templates. + * Primary question is what to do with output, i.e., store JSON output + * in 'data', ability to output regular command output to console (but + * how to avoid interference with ProgressWriter?), ...
  • + *
+ * + * @author Ruud Senden + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepDescriptor implements IActionIfSupplier { + /** 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 template expression for this step element */ + private TemplateExpression progress; + /** Optional set operations */ + private List set; + /** Optional write operations */ + private List write; + + /** + * 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 ( write!=null ) { write.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 implements IActionIfSupplier { + /** 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; + /** Required name for this step element */ + private String name; + /** Optional requests for which to embed the response in each forEach node */ + private List embed; + /** 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 implements IActionIfSupplier { + /** Optional if-expression, executing this step only if condition evaluates to true */ + @JsonProperty("if") private SimpleExpression _if; + /** Required name for this step element */ + private String name; + /** Value template expression for this step element */ + private TemplateExpression value; + /** Value template used to generate the value to be set */ + private String valueTemplate; + /** Operation to be performed when setting the value */ + private ActionStepSetOperation operation; + + public void postLoad(ActionDescriptor action) { + checkNotBlank("set name", name, this); + if ( value==null && valueTemplate==null ) { + valueTemplate = name; + } + if ( valueTemplate!=null ) { + check(!action.getValueTemplates().stream().anyMatch(d->d.getName().equals(valueTemplate)), this, + ()->"No value template found with name "+valueTemplate); + } + } + + // TODO: Add other operations like 'merge' for merging two ObjectNodes or ArrayNodes? + public static enum ActionStepSetOperation { + replace, append; + } + } + + /** + * This class describes a 'write' step. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepWriteDescriptor implements IActionIfSupplier { + /** Optional if-expression, executing this step only if condition evaluates to true */ + @JsonProperty("if") private SimpleExpression _if; + /** Required template expression defining where to write the data, either stdout, stderr or filename */ + private TemplateExpression to; + /** Value template expression that generated the contents to be written */ + private TemplateExpression value; + /** Value template used to generate the value to be set */ + private String valueTemplate; + + public void postLoad(ActionDescriptor action) { + checkNotNull("write to", to, this); + check(value==null && StringUtils.isBlank(valueTemplate), this, ()-> + "Either value or valueTemplate must be specified on write step"); + } + } + + /** + * This class describes a REST request. + */ + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepRequestDescriptor implements IActionIfSupplier { + /** Optional if-expression, executing this request only if condition evaluates to true */ + @JsonProperty("if") private SimpleExpression _if; + /** Required name for this step element */ + private String name; + /** Optional HTTP method, defaults to 'GET' */ + private HttpMethod method = HttpMethod.GET; + /** Required template 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 template expressions */ + private Map query; + /** Optional request body template expression */ + private TemplateExpression body; + /** Type of request; either 'simple' or 'paged' for now */ + private ActionStepRequestType type = ActionStepRequestType.simple; + /** 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 ActionValueTemplateDescriptor { + /** Required name for this output */ + private String name; + /** Output contents in JSON format, where each text node is assumed to be a template 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 template expression '%s'", expr), ActionValueTemplateDescriptor.this, e); + } + } + super.walkValue(state, path, parent, node); + } + } + } + + /** + * 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..efa948d3e6 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionHelper.java @@ -0,0 +1,276 @@ +/** + * 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.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fortify.cli.common.http.proxy.helper.ProxyHelper; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.rest.unirest.GenericUnirestFactory; +import com.fortify.cli.common.util.FcliDataHelper; +import com.fortify.cli.common.util.FileUtils; +import com.fortify.cli.common.util.StringUtils; + +import kong.unirest.UnirestInstance; +import lombok.SneakyThrows; + +public class ActionHelper { + private static final Logger LOG = LoggerFactory.getLogger(ActionHelper.class); + private static final ObjectMapper yamlObjectMapper = new ObjectMapper(new YAMLFactory()); + private ActionHelper() {} + + public static final ActionDescriptor load(String type, String name) { + return loadZipEntry(type, name, ActionHelper::loadDescriptor); + } + + public static final String loadContents(String type, String name) { + return loadZipEntry(type, name, (is, ze, isCustom)->FileUtils.readInputStreamAsString(is, StandardCharsets.US_ASCII)); + } + + public static final Stream list(String type) { + Map result = new HashMap<>(); + processBuiltinAndCustomZipEntries(type, (zis, ze, isCustom)->{ + result.putIfAbsent(getActionName(ze.getName()), loadAsJson(ze.getName(), zis, isCustom)); + return true; + }); + return result.values().stream() + .sorted((a,b)->a.get("name").asText().compareTo(b.get("name").asText())); + } + + @SneakyThrows + public static final ArrayNode importZip(String type, String source) { + var result = JsonHelper.getObjectMapper().createArrayNode(); + IZipEntryProcessor processor = (zis, ze, isCustom)->importEntry(result, zis, getActionName(ze.getName()), type); + try { + var url = new URL(source); + try ( var unirest = createUnirestInstance(type, url) ) { + unirest.get(url.toString()).asObject(r->processZipEntries(r.getContent(), processor, true)).getBody(); + } + } catch (MalformedURLException e ) { + try ( var is = Files.newInputStream(Path.of(source)) ) { + processZipEntries(is, processor, true); + } + } + return result; + } + + @SneakyThrows + public static final ArrayNode importSingle(String type, String name, String source) { + var result = JsonHelper.getObjectMapper().createArrayNode(); + var finalName = StringUtils.isBlank(name) ? getActionName(source) : name; + try { + var url = new URL(source); + try ( var unirest = createUnirestInstance(type, url) ) { + unirest.get(url.toString()).asObject(r->importEntry(result, r.getContent(), finalName, type)).getBody(); + } + } catch (MalformedURLException e ) { + try ( var is = Files.newInputStream(Path.of(source)) ) { + importEntry(result, is, finalName, type); + } + } + return result; + } + + @SneakyThrows + public static final ArrayNode reset(String type) { + var result = JsonHelper.getObjectMapper().createArrayNode(); + var zipPath = getCustomActionsZipPath(type); + if ( Files.exists(zipPath) ) { + try ( var is = Files.newInputStream(zipPath) ) { + processZipEntries(is, (zis, ze, isCustom) -> { + result.add(loadAsJson(ze.getName(), zis, isCustom)); + return true; + }, true); + } + Files.delete(zipPath); + } + return result; + } + + + private static final boolean importEntry(ArrayNode importedEntries, InputStream is, String name, String type) { + try { + // Read input stream as string for further processing + var contents = FileUtils.readInputStreamAsString(is, StandardCharsets.US_ASCII); + // Read contents as JsonNode to update importedEntries array after import + var json = yamlObjectMapper.readValue(contents, ObjectNode.class); + // Verify that the template can be successfully parsed and post-processed + yamlObjectMapper.treeToValue(json, ActionDescriptor.class).postLoad(name, true); + // Import entry to zip-file + importEntry(name, type, contents); + // Add JSON to the imported entries array. + importedEntries.add(updateJson(name, true, json)); + } catch ( Exception e ) { + LOG.warn("WARN: Skipping "+name+" due to errors, see debug log for details"); + LOG.debug("WARN: Skipping "+name+" due to errors", e); + } + return true; + } + + @SneakyThrows + private static void importEntry(String name, String type, String contents) { + Map env = Collections.singletonMap("create", "true"); + + try (FileSystem zipfs = FileSystems.newFileSystem(getCustomActionsZipPath(type), env)) { + Path filePath = zipfs.getPath(getActionName(name)+".yaml"); + Files.write(filePath, contents.getBytes(StandardCharsets.US_ASCII), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + } + + private static final UnirestInstance createUnirestInstance(String type, URL url) { + var result = GenericUnirestFactory.createUnirestInstance(); + ProxyHelper.configureProxy(result, type.toLowerCase()+"-action", url.toString()); + return result; + } + + private static final ActionDescriptor loadDescriptor(InputStream is, ZipEntry ze, boolean isCustom) { + return loadDescriptor(is, ze.getName(), isCustom); + } + + private static final ActionDescriptor loadDescriptor(InputStream is, String actionName, boolean isCustom) { + try { + var result = yamlObjectMapper.readValue(is, ActionDescriptor.class); + result.postLoad(getActionName(actionName), isCustom); + return result; + } catch (IOException e) { + throw new RuntimeException("Error loading action "+actionName, e); + } + } + + private static final ObjectNode loadAsJson(String fileName, InputStream is, boolean isCustom) { + if ( is==null ) { + // TODO Use more descriptive exception message + throw new IllegalStateException("Can't read "+fileName); + } + try { + var result = yamlObjectMapper.readValue(is.readAllBytes(), ObjectNode.class); + return updateJson(fileName, isCustom, result); + } catch (IOException e) { + throw new RuntimeException("Error loading action "+fileName, e); + } + } + + private static ObjectNode updateJson(String fileName, boolean isCustom, ObjectNode result) { + return result + .put("name", getActionName(fileName)) + .put("custom", isCustom) + .put("isCustomString", isCustom?"Yes":"No"); + } + + private static final String getActionName(String fileName) { + return Path.of(fileName).getFileName().toString().replace(".yaml", ""); + } + + private static final T loadZipEntry(String type, String name, IZipEntryProcessor processor) { + AtomicReference result = new AtomicReference<>(); + processBuiltinAndCustomZipEntries(type, (zis, ze, isCustom)->{ + var fileName = name+".yaml"; + if (ze.getName().equals(fileName)) { + result.set(processor.process(zis, ze, isCustom)); + return false; + } else { + return true; + } + }); + if ( result.get()==null ) { + throw new IllegalArgumentException("No action found with name "+name); + } + return result.get(); + } + + @SneakyThrows + private static final void processBuiltinAndCustomZipEntries(String type, IZipEntryProcessor processor) { + boolean _continue; + try ( var customActionsZipFileInputStream = getCustomActionsZipFileInputStream(type) ) { + _continue = processZipEntries(customActionsZipFileInputStream, processor, true); + } + if ( _continue ) { + try ( var builtinActionsZipFileInputStream = getBuiltinActionsZipFileInputStream(type) ) { + _continue = processZipEntries(builtinActionsZipFileInputStream, processor, false); + } + } + if ( _continue ) { + try ( var commonActionsZipFileInputStream = getCommonActionsZipFileInputStream() ) { + _continue = processZipEntries(commonActionsZipFileInputStream, processor, false); + } + } + } + + private static final boolean processZipEntries(InputStream zipFileInputStream, IZipEntryProcessor processor, boolean isCustom) { + if ( zipFileInputStream!=null ) { + try ( ZipInputStream zis = new ZipInputStream(zipFileInputStream) ) { + ZipEntry entry; + while ( (entry = zis.getNextEntry())!=null ) { + if ( !processor.process(zis, entry, isCustom) ) { return false; } + } + } catch (IOException e) { + throw new RuntimeException("Error loading actions", e); + } + } + return true; + } + + @SneakyThrows + private static final InputStream getCustomActionsZipFileInputStream(String type) { + var zipPath = getCustomActionsZipPath(type); + return !Files.exists(zipPath) ? null : Files.newInputStream(zipPath); + } + + private static final InputStream getBuiltinActionsZipFileInputStream(String type) { + return FileUtils.getResourceInputStream(getBuiltinActionsResourceZip(type)); + } + + private static final InputStream getCommonActionsZipFileInputStream() { + return FileUtils.getResourceInputStream(getCommonActionsResourceZip()); + } + + private static Path getCustomActionsZipPath(String type) { + return FcliDataHelper.getFcliConfigPath().resolve("action").resolve(type.toLowerCase()+".zip"); + } + + private static final String getBuiltinActionsResourceZip(String type) { + return String.format("com/fortify/cli/%s/actions.zip", type.toLowerCase().replace('-', '_')); + } + + private static final String getCommonActionsResourceZip() { + return "com/fortify/cli/common/actions.zip"; + } + + @FunctionalInterface + private static interface IZipEntryProcessor { + T process(ZipInputStream is, ZipEntry entry, boolean isCustom); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionParameterHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionParameterHelper.java new file mode 100644 index 0000000000..0250178a37 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionParameterHelper.java @@ -0,0 +1,70 @@ +/** + * 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.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionParameterDescriptor; +import com.fortify.cli.common.cli.util.SimpleOptionsParser.IOptionDescriptor; +import com.fortify.cli.common.cli.util.SimpleOptionsParser.OptionDescriptor; +import com.github.freva.asciitable.AsciiTable; +import com.github.freva.asciitable.Column; +import com.github.freva.asciitable.HorizontalAlign; + +public final class ActionParameterHelper { + private ActionParameterHelper() {} + + public static final List getOptionDescriptors(ActionDescriptor action) { + var parameters = action.getParameters(); + List result = new ArrayList<>(parameters.size()); + parameters.forEach(p->addOptionDescriptor(result, p)); + return result; + } + + private static final void addOptionDescriptor(List result, ActionParameterDescriptor parameter) { + result.add(OptionDescriptor.builder() + .name(getOptionName(parameter.getName())) + .aliases(getOptionAliases(parameter.getCliAliasesArray())) + .description(parameter.getDescription()) + .build()); + } + + static final String getOptionName(String parameterNameOrAlias) { + var prefix = parameterNameOrAlias.length()==1 ? "-" : "--"; + return prefix+parameterNameOrAlias; + } + + private static final List getOptionAliases(String[] aliases) { + return aliases==null ? null : Stream.of(aliases).map(ActionParameterHelper::getOptionName).toList(); + } + + public static final String getSupportedOptionsTable(ActionDescriptor action) { + return getSupportedOptionsTable(getOptionDescriptors(action)); + } + + public static final String getSupportedOptionsTable(List options) { + return AsciiTable.builder() + .border(AsciiTable.NO_BORDERS) + .data(new Column[] { + new Column().dataAlign(HorizontalAlign.LEFT), + new Column().dataAlign(HorizontalAlign.LEFT), + }, + options.stream() + .map(option->new String[] {option.getOptionNamesAndAliasesString(" | "), option.getDescription()}) + .toList().toArray(String[][]::new)) + .asString(); + } + +} 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..e3fc3a8ab6 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java @@ -0,0 +1,696 @@ +/** + * 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.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.ActionStepSetDescriptor.ActionStepSetOperation; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionStepWriteDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionValidationException; +import com.fortify.cli.common.action.helper.ActionDescriptor.ActionValueTemplateDescriptor; +import com.fortify.cli.common.action.helper.ActionDescriptor.IActionIfSupplier; +import com.fortify.cli.common.action.helper.ActionRunner.IActionRequestHelper.ActionRequestDescriptor; +import com.fortify.cli.common.action.helper.ActionRunner.IActionRequestHelper.BasicActionRequestHelper; +import com.fortify.cli.common.cli.util.SimpleOptionsParser; +import com.fortify.cli.common.cli.util.SimpleOptionsParser.IOptionDescriptor; +import com.fortify.cli.common.cli.util.SimpleOptionsParser.OptionsParseResult; +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 */ + @Getter private final ActionDescriptor action; + /** Callback to handle validation errors */ + private final Function onValidationErrors; + /** 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(); + /** 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<>(); + // We need to delay writing output to console as to not interfere with progress writer + private final List delayedConsoleWriterRunnables = new ArrayList<>(); + + public final Runnable run(String[] args) { + globalData.set("parameters", parameters); + progressWriter.writeProgress("Processing action parameters"); + var optionsParseResult = new ActionParameterProcessor(args).processParameters(); + if ( optionsParseResult.hasValidationErrors() ) { + throw onValidationErrors.apply(optionsParseResult); + } else { + new ActionAddRequestTargetsProcessor().addRequestTargets(); + progressWriter.writeProgress("Processing action steps"); + new ActionStepsProcessor(globalData, null).processSteps(); + progressWriter.writeProgress("Producing action outputs"); + progressWriter.writeProgress("Action processing finished"); + } + return ()->delayedConsoleWriterRunnables.forEach(Runnable::run); + } + + public final void close() { + requestHelpers.values().forEach(IActionRequestHelper::close); + } + + 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 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 OptionsParseResult optionsParseResult; + + public ActionParameterProcessor(String[] args) { + this.optionsParseResult = parseParameterValues(args); + } + private final OptionsParseResult processParameters() { + var validationErrors = optionsParseResult.getValidationErrors(); + if ( validationErrors.size()==0 ) { + action.getParameters().forEach(this::addParameterData); + } + return optionsParseResult; + } + + private final OptionsParseResult parseParameterValues(String[] args) { + List optionDescriptors = ActionParameterHelper.getOptionDescriptors(action); + var parseResult = new SimpleOptionsParser(optionDescriptors).parse(args); + addDefaultValues(parseResult); + addValidationMessages(parseResult); + return parseResult; + } + + private final void addDefaultValues(OptionsParseResult parseResult) { + action.getParameters().forEach(p->addDefaultValue(parseResult, p)); + } + + private final void addValidationMessages(OptionsParseResult parseResult) { + action.getParameters().forEach(p->addValidationMessages(parseResult, p)); + } + + private final void addDefaultValue(OptionsParseResult parseResult, ActionParameterDescriptor parameter) { + var name = parameter.getName(); + var value = getOptionValue(parseResult, parameter); + if ( value==null ) { + var defaultValueExpression = parameter.getDefaultValue(); + value = defaultValueExpression==null + ? null + : spelEvaluator.evaluate(defaultValueExpression, globalData, String.class); + } + parseResult.getOptionValuesByName().put(ActionParameterHelper.getOptionName(name), value); + } + + private final void addValidationMessages(OptionsParseResult parseResult, ActionParameterDescriptor parameter) { + if ( parameter.isRequired() && StringUtils.isBlank(getOptionValue(parseResult, parameter)) ) { + parseResult.getValidationErrors().add("No value provided for required option "+ + ActionParameterHelper.getOptionName(parameter.getName())); + } + } + + private final void addParameterData(ActionParameterDescriptor parameter) { + var name = parameter.getName(); + var value = getOptionValue(optionsParseResult, parameter); + if ( value==null ) { + var defaultValueExpression = parameter.getDefaultValue(); + value = defaultValueExpression==null + ? null + : spelEvaluator.evaluate(defaultValueExpression, globalData, String.class); + } + parameters.set(name, convertParameterValue(value, parameter)); + } + private String getOptionValue(OptionsParseResult parseResult, ActionParameterDescriptor parameter) { + var optionName = ActionParameterHelper.getOptionName(parameter.getName()); + return parseResult.getOptionValuesByName().get(optionName); + } + + 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 localData; + private final ActionStepsProcessor parent; + + private final void processSteps() { + processSteps(action.getSteps()); + } + + private final void processSteps(List steps) { + if ( steps!=null ) { steps.forEach(this::processStep); } + } + + private final void processStep(ActionStepDescriptor step) { + if ( _if(step) ) { + processSupplier(step::getProgress, this::processProgressStep); + processSupplier(step::getRequests, this::processRequestsStep); + processAll(step::getSet, this::processSetStep); + processAll(step::getWrite, this::processWriteStep); + } + } + + private void processAll(Supplier> supplier, Consumer consumer) { + var list = supplier.get(); + if ( list!=null ) { list.forEach(value->processValue(value, consumer)); } + } + + private void processSupplier(Supplier supplier, Consumer consumer) { + processValue(supplier.get(), consumer); + } + + private void processValue(T value, Consumer consumer) { + if ( _if(value) ) { consumer.accept(value); } + } + + private final boolean _if(Object o) { + if (o==null) { return false; } + if (o instanceof IActionIfSupplier ) { + var _if = ((IActionIfSupplier) o).get_if(); + if ( _if!=null ) { + return spelEvaluator.evaluate(_if, localData, Boolean.class); + } + } + return true; + } + + private void processSetStep(ActionStepSetDescriptor set) { + var name = set.getName(); + var value = getValueForSetStep(set); + setDataValue(name, value); + } + + private void setDataValue(String name, JsonNode value) { + localData.set(name, value); + if ( parent!=null ) { parent.setDataValue(name, value); } + } + + private JsonNode getValueForSetStep(ActionStepSetDescriptor set) { + var valueExpression = set.getValue(); + var valueTemplate = set.getValueTemplate(); + var value = valueExpression!=null + ? getValue(valueExpression) + : getFormattedValue(valueTemplate); + return getTargetValueForSetStep(set, value); + } + + private JsonNode getTargetValueForSetStep(ActionStepSetDescriptor set, JsonNode value) { + var result = value; + var name = set.getName(); + var op = set.getOperation(); + if ( op==ActionStepSetOperation.append ) { + result = localData.get(name); + if ( result==null ) { + result = objectMapper.createArrayNode(); + } + if ( !result.isArray() ) { + throw new IllegalStateException("Cannot append value to existing value of type "+result.getNodeType()); + } + ((ArrayNode)result).add(value); + } + return result; + } + + private JsonNode getValue(TemplateExpression valueExpression) { + var value = spelEvaluator.evaluate(valueExpression, localData, Object.class); + return objectMapper.valueToTree(value); + } + + private JsonNode getFormattedValue(String valueTemplate) { + var valueTemplateDescriptor = action.getValueTemplatesByName().get(valueTemplate); + var outputRawContents = valueTemplateDescriptor.getContents(); + return new JsonNodeOutputWalker(spelEvaluator, valueTemplateDescriptor, localData).walk(outputRawContents); + } + + private void processWriteStep(ActionStepWriteDescriptor write) { + var to = spelEvaluator.evaluate(write.getTo(), localData, String.class); + var valueExpression = write.getValue(); + var valueTemplate = write.getValueTemplate(); + var value = asString(valueExpression!=null + ? getValue(valueExpression) + : getFormattedValue(valueTemplate)); + try { + switch (to.toLowerCase()) { + case "stdout": delayedConsoleWriterRunnables.add(createRunner(System.out, value)); break; + case "stderr": delayedConsoleWriterRunnables.add(createRunner(System.err, value)); break; + default: write(new File(to), value); + } + } catch (IOException e) { + throw new RuntimeException("Error writing action output to "+to); + } + } + + private Runnable createRunner(PrintStream out, String output) { + return ()->out.print(output); + } + + private void write(File file, String output) throws IOException { + try ( var out = new PrintStream(file) ) { + out.println(output); + } + } + + private final String asString(Object output) { + if ( output instanceof TextNode ) { + return ((TextNode)output).asText(); + } else if ( output instanceof JsonNode ) { + return ((JsonNode)output).toPrettyString(); + } else { + return output.toString(); + } + } + + private void processProgressStep(TemplateExpression progress) { + progressWriter.writeProgress(spelEvaluator.evaluate(progress, localData, String.class)); + } + + private void processRequestsStep(List requests) { + if ( requests!=null ) { + var requestsProcessor = new ActionStepRequestsProcessor(); + requestsProcessor.addRequests(requests, this::processResponse, localData); + requestsProcessor.executeRequests(); + } + } + + private final void processResponse(ActionStepRequestDescriptor requestDescriptor, JsonNode rawBody) { + var name = requestDescriptor.getName(); + var body = getRequestHelper(requestDescriptor.getTarget()).transformInput(rawBody); + localData.set(name+"_raw", rawBody); + localData.set(name, body); + processForEach(requestDescriptor); + } + + private final void processForEach(ActionStepRequestDescriptor requestDescriptor) { + var forEach = requestDescriptor.getForEach(); + if ( forEach!=null ) { + var input = localData.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(localData); + 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 = localData.get(totalCountName); + if ( totalCount==null ) { totalCount = new IntNode(0); } + localData.put(totalCountName, totalCount.asInt()+array.size()); + } + + private void processForEachEntryDo(ActionStepForEachDescriptor forEach, JsonNode currentNode, ObjectNode newData) { + var processor = new ActionStepsProcessor(newData, this); + 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); + } + } + } + + 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 ActionValueTemplateDescriptor 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/util/SimpleOptionsParser.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/SimpleOptionsParser.java new file mode 100644 index 0000000000..bfbcd5c8be --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/SimpleOptionsParser.java @@ -0,0 +1,139 @@ +/** + * 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.cli.util; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import picocli.CommandLine.Option; +import picocli.CommandLine.Unmatched; + +/** + * This class allows for parsing unmatched options; if a command requires + * support for dynamic options (that cannot be declared through Picocli + * {@link Option} annotation), it can declare a field with Picocli + * {@link Unmatched} annotation and use this class to parse & process + * these unmatched options. + */ +@RequiredArgsConstructor +public final class SimpleOptionsParser { + private final List options; + @Getter(lazy = true) private final Map optionsDescriptorsByNameAndAliases = _getOptionsDescriptorsByNameAndAliases(); + + public static interface IOptionDescriptor { + String getName(); + List getAliases(); + String getDescription(); + + default String getOptionNamesAndAliasesString(String delimiter) { + var name = getName(); + List aliases = getAliases()==null ? Collections.emptyList() : getAliases(); + return Stream.concat(Stream.of(name), aliases.stream()) + .collect(Collectors.joining(delimiter)); + } + } + + @Builder @Data + public static class OptionDescriptor implements IOptionDescriptor { + private final String name; + private final List aliases; + private final String description; + } + + @Data + public final class OptionsParseResult { + private final List options; + private final Map optionValuesByName = new LinkedHashMap<>(); + private final Map cliArgsByOptionNames = new LinkedHashMap<>(); + private final List validationErrors = new ArrayList<>(); + + public final boolean hasValidationErrors() { + return validationErrors.size()>0; + } + } + + public final OptionsParseResult parse(String[] args) { + var result = new OptionsParseResult(options); + var validationErrors = result.getValidationErrors(); + var rawArgValues = parseArgs(validationErrors, args); + updateResult(result, rawArgValues); + return result; + } + + private final void updateResult(OptionsParseResult result, Map rawArgValues) { + rawArgValues.entrySet().forEach(e->updateResult(result, e.getKey(), e.getValue())); + } + + private final void updateResult(OptionsParseResult result, String arg, String value) { + var option = getOption(arg); + var name = option.getName(); + result.getCliArgsByOptionNames().put(name, arg); + result.getOptionValuesByName().put(name, value); // Store option names, not aliases + } + + /** + * This method returns a map of CLI argument names with corresponding values. Note that + * we return the argument names as specified on the command line, not the corresponding + * option name. This allows for later validation messages to display the original + * argument name (which may be an alias), not the option name. + */ + private Map parseArgs(List validationErrors, String[] args) { + Map result = new LinkedHashMap<>(); + if ( args!=null && args.length>0 ) { + var optionsByNameAndAliases = getOptionsDescriptorsByNameAndAliases(); + var argsDeque = new ArrayDeque<>(Arrays.asList(args)); + while ( !argsDeque.isEmpty() ) { + var argWithPossibleValue = argsDeque.pop(); + var argElts = argWithPossibleValue.split("=", 2); + var arg = argElts[0]; + if ( !optionsByNameAndAliases.containsKey(arg) ) { + validationErrors.add("Unknown command line option: "+arg); + } else if ( argElts.length==2 ) { + result.put(arg, argElts[1]); + } else { + var nextArg = argsDeque.peek(); + var value = optionsByNameAndAliases.containsKey(nextArg) ? null : argsDeque.pop(); + result.put(arg, value); + } + } + } + return result; + } + + private final Map _getOptionsDescriptorsByNameAndAliases() { + final Map result = new LinkedHashMap<>(); + options.forEach(option->{ + result.put(option.getName(), option); + var aliases = option.getAliases(); + if ( aliases!=null ) { + aliases.forEach(alias->result.put(alias, option)); + } + }); + return result; + } + + private final IOptionDescriptor getOption(String nameOrAlias) { + return getOptionsDescriptorsByNameAndAliases().get(nameOrAlias); + } +} \ No newline at end of file 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/cli/cmd/AbstractRestCallCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/cli/cmd/AbstractRestCallCommand.java index 594171457e..5ab65f8242 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/cli/cmd/AbstractRestCallCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/cli/cmd/AbstractRestCallCommand.java @@ -28,8 +28,8 @@ import com.fortify.cli.common.rest.paging.INextPageUrlProducerSupplier; import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; import com.fortify.cli.common.util.DisableTest; -import com.fortify.cli.common.util.JavaHelper; import com.fortify.cli.common.util.DisableTest.TestType; +import com.fortify.cli.common.util.JavaHelper; import com.fortify.cli.common.util.StringUtils; import kong.unirest.HttpRequest; 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..d6f786df3f 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,91 @@ 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 com.fortify.cli.common.util.StringUtils; -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, StringUtils.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, StringUtils.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..f3cb1e175a 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 @@ -28,16 +28,19 @@ public static String ifBlank(String s, String defaultValue) { } public static final String substringBefore(String str, String separator) { + if ( str==null ) { return null; } final int pos = str.indexOf(separator); return pos==-1 ? str : str.substring(0, pos); } public static final String substringAfter(String str, String separator) { + if ( str==null ) { return null; } final int pos = str.indexOf(separator); return pos==-1 ? "" : str.substring(pos + separator.length()); } public static final String substringAfterLast(String str, String separator) { + if ( str==null ) { return null; } final int pos = str.lastIndexOf(separator); return pos==-1 ? "" : str.substring(pos + separator.length()); } @@ -48,11 +51,12 @@ public static final String capitalize(String str) { : str.substring(0,1).toUpperCase() + str.substring(1).toLowerCase(); } - public static String abbreviate(String input, int maxLength) { - if (input.length() <= maxLength) { - return input; + public static String abbreviate(String str, int maxLength) { + if ( str==null ) { return null; } + if (str.length() <= maxLength) { + return str; } else { - return input.substring(0, maxLength); + return str.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/actions/zip/__sample__.yaml b/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/actions/zip/__sample__.yaml new file mode 100644 index 0000000000..ec04570651 --- /dev/null +++ b/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/actions/zip/__sample__.yaml @@ -0,0 +1,225 @@ +# This action documents action syntax through comments for the various sections and +# elements below. Many action properties accept Spring Expression Language (SpEL) +# expressions, either as a simple expression or a template expression, with the +# latter combining regular text with SpEL expressions embedded between ${ and }. +# Please see the Sring Expression Language reference for details: +# https://docs.spring.io/spring-framework/reference/6.0/core/expressions.html + +# This section defines action usage information, consisting of a usage header (shown +# by the 'list' and 'help' commands) and a more detailed description (shown by the +# 'help' command). +usage: + header: Sample Action + description: | + This action documents action syntax to allow users to build their own custom actions. + Note that action syntax is subject to change. Custom action YAML files that work fine + on the current fcli version may not work on either older or newer fcli versions, and + thus may need to be updated when upgrading fcli. Please see this link for details: + https://github.com/fortify/fcli/issues/515 + +# This section lists action parameters that action users can provide as command-line +# options when running the action. Parameter values can be referenced through the +# 'parameters' property in SpEL expressions. Each parameter entry supports the +# following properties: +# - name: Required parameter name, used to generate CLI option name and +# for referencing the parameter in SpEL expressions. +# - cliAliases: Optional CLI option aliases. +# - description: Required parameter description shown in action help output. +# - required: Optional, either 'true' or 'false' to indicate whether the +# parameter is required. Parameters are required by default. +# - defaultValue: Optional SpEL template expression that defines the default value +# for the parameter if not specified by the user. +# - type: Optional parameter type, defaults to 'string'; see below. +# - typeParameters: Map of parameters for the given type; see below. Type parameter +# values may use SpEL template expressions. +# +# By default, parameters values are simple string values as supplied by the user. +# Based on the given type, fcli may convert the user-supplied value to different +# types and/or perform additional processing, potentially generating a complex +# value with sub-properties. The following types are currently supported: +# +# Generic: +# - string: No conversion; use the user-supplied value as a string +# - boolean: Convert the user-supplief value ('true' or 'false') to boolean +# - int: Convert the user-supplied numeric value to an int-value +# - long: Convert the user-supplied numeric value to a long-value +# - double: Convert the user-supplied numeric value to a double-value +# - float: Convert the user-supplied numeric value to a float-value +# +# FoD-only: +# - release_single: Load the release JSON object for the user-supplied release +# name or id +# +# SSC-only: +# - appversion_single: Load the application version JSON object for the user-supplied +# application version name or id +# - filterset: Load the filterset JSON object for the user-supplied filter +# set name or id. Takes a typeParameter named 'appversion.id', +# which defaults to '${appversion.id}'; if a parameter of this +# type is preceded by an appversion_single parameter named +# 'appversion', no type parameters need to be specified. +parameters: + - name: output + cliAliases: o + description: "Output location; either 'stdout', 'stderr' or file name. Default value: sample.json" + required: false + defaultValue: sample.md + - name: github-token + description: 'Required GitHub Token. Default value: GITHUB_TOKEN environment variable.' + required: true + defaultValue: ${#env('GITHUB_TOKEN')} + - name: github-org + cliAliases: gho + description: GitHub owner/organization for which to list repositories + required: false + defaultValue: fortify + # See SSC/FoD-specific actions for examples on how to load application + # versions/releases and SSC filter sets. + +# Optional property for adding request targets. By default, fcli provides request targets +# corresponding to the module that provides a certain action, i.e.: +# - The 'fcli fod' module provides an 'fod' request target for interacting with FoD +# - The 'fcli ssc' module provides an 'ssc' request target for interacting with SSC +# - Not available yet, but 'fcli sc-sast' and 'fcli sc-dast' modules will provide +# both 'ssc' and respectively 'sc-sast'/'sc-dast' request targets. +# Adding request targets allows an action to interact with other (3rd-party) systems +# like GitHub or GitLab. +addRequestTargets: + - name: github + baseUrl: https://api.github.com + headers: + Authorization: Bearer ${parameters['github-token']} + 'X-GitHub-Api-Version': '2022-11-28' + +# This section allows for setting default values for some properties. For now, a default +# request target is the only supported property, avoiding the need to explicitly set a +# request target on every request element. +defaults: + requestTarget: github + +# This section defines the steps to be executed for this action. Steps are executed +# sequentually. The following step types are currently supported: +# - progress: Takes an SpEL expression to generate a progress message +# - requests: Execute one or more requests +# - set: Set a data value for use by subsequent or nested steps +# - write: Write data to stdout, stderr or a file +# See below for more information on each step type. +steps: + # Output a progress message based on an SpEL expression + - progress: Processing organization ${parameters['github-org']} + # Execute one or more requests. Requests within a single requests block + # may not depend on each others output data, as they may be executed + # in parallel or as a single (SSC) bulk request. Only (REST) requests + # that return JSON data are currently supported. Each request element + # supports the following properties: + # - if: Optional; only execute this request if the given SpEL + # expression evaluates to 'true'. + # - name: Required request name. Subsequent steps can reference + # raw request JSON output through the _raw + # property. For FoD/SSC, actual contents (i.e., contents + # of the SSC 'data' property or FoD 'items' property) is + # available through the property. + # - target: Target for this request, i.e., 'ssc', 'fod', ... + # - method: Optional HTTP method, defaults to GET. + # - uri: Required request URI, takes an SpEL template expression. + # - query: Optional map of query parameters; query parameter values + # may be specified as SpEL template expressions. + # - body: Optional request body, takes an SpEL expression. + # - type: Either 'simple' or 'paged', with the latter automatically + # loading subsequent pages (SSC/FoD-only). + # - pagingProgress: Optional; allows for outputting paging-related progress messages. + # prePageLoad: SpEL expression defining progress message to be shown + # before a page is loaded from the target system. + # postPageLoad: SpEL expression defining progress message to be shown + # after a page is loaded from the target system. + # postPageProcess: SpEL expression defining progress message to be shown + # after a page has been processed. + # - forEach: Optional; process each record returned by the target system. + # if: Optional; only process the current record if the given SpEL + # expression evaluates to 'true'. + # breakIf: Optional; stop processing current and subsequent records if + # the given SpEL expression evaluates to 'true'. + # name: Required name for the current record; record data can be + # accessed through the given name in all steps listed in the + # 'do' block. + # embed: Each entry defines another request, for which the output will + # be embedded in the current record under the request 'name' + # property. For SSC, a single bulk request will be executed + # for all records in the current page. + # do: List of steps to be executed for the current record. + - requests: + # + - name: repos + # if: true + # target: github + # method: GET + uri: /orgs/${parameters['github-org']}/repos + query: + type: public + sort: updated + # body: ${reference to previously generated data property} + # type: simple # paged is only supported for FoD/SSC + # pagingProgress: + # prePageLoad: ... + # postPageLoad: ... + # postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + forEach: + # if: true + # breakIf: false + name: repo + # embed: + # - name: releases + # uri: /repos/${repo.full_name}/releases + do: + - set: # See documentation for 'set' below + - name: repositories + operation: append + - name: repositories_json + value: ${repo} + operation: append + # Set one or more data properties for use in later steps. Each set element + # supports the following properties: + # - if: Optional; only set value if given simple SpEL expression + # evaluates to 'true'. + # - name: Required name of the data property to set. + # - value: Optional SpEL template expression defining the value to be set. + # - valueTemplate: Optional reference to a valueTemplate name (see below) used to + # generate the value to be set. If neither 'value' or 'valueTemplate' + # is specified, the value template for which the name matches the + # name of this set element is used to generate the value. + # - operation: Required: Either 'append' (for appending as array entryies) or + # 'replace' + - set: + - name: output + # Write one or more outputs. Each write element supports the following properties: + # - to: Required; either 'stdout', 'stderr' or a file name. + # value: Required; SpEL template expression that generates the output to be written. + - write: + - to: ${parameters.output} + value: ${output} + +# This section defines value templates for use with 'set' steps. Each value template +# supports the following properties: +# - name: Name of this value template +# - contents: Contents of this value template; may either be an SpEL template +# expression that generates a string, or an object tree that defines +# value properties, with each property taking either a nested object +# tree or an SpEL template expression that defines property contents. +valueTemplates: + - name: output + contents: | + # List of repositories + + ${#join('\n',repositories)} + + # Raw repositories JSON + + ``` + ${repositories_json.toString()} + ``` + - name: repositories + contents: | + ## ${repo.name} + + ${repo.description} + 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..85a7791caa 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,16 @@ 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-parameter = Action parameter(s); see 'help' command output to \ + list supported parameters. +cli.action.import.zip = Zip-file containing actions to be imported; may be specified as a path to \ + a local zip-file or a URL. Action names will be based on filenames contained in the zip-file. +cli.action.import.file = Single action YAML file to be imported; may be specified as a path to a \ + local file or a URL. +cli.action.import.name = Name for the imported action. If not specified, the name of the given file \ + (with path and extension removed) is used as the action name. # 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-common/src/test/java/com/fortify/cli/common/rest/wait/WaitHelperTest.java b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/rest/wait/WaitHelperTest.java index 0630c04eef..ac25a30c7a 100644 --- a/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/rest/wait/WaitHelperTest.java +++ b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/rest/wait/WaitHelperTest.java @@ -12,14 +12,9 @@ *******************************************************************************/ package com.fortify.cli.common.rest.wait; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; // TODO Add multithreaded tests that emulate actual state changes @Timeout(value = 5) 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/rest/helper/FoDProductHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDProductHelper.java index fac65477e3..39f6bf65d8 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDProductHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDProductHelper.java @@ -12,12 +12,16 @@ *******************************************************************************/ package com.fortify.cli.fod._common.rest.helper; +import java.net.URI; + import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.output.product.IProductHelper; import com.fortify.cli.common.output.transform.IInputTransformer; import com.fortify.cli.common.rest.paging.INextPageUrlProducer; import com.fortify.cli.common.rest.paging.INextPageUrlProducerSupplier; +import lombok.SneakyThrows; + // IMPORTANT: When updating/adding any methods in this class, FoDRestCallCommand // also likely needs to be updated public class FoDProductHelper implements IProductHelper, IInputTransformer, INextPageUrlProducerSupplier @@ -33,4 +37,24 @@ public INextPageUrlProducer getNextPageUrlProducer() { public JsonNode transformInput(JsonNode input) { return FoDInputTransformer.getItems(input); } + + @SneakyThrows + public String getApiUrl(String url) { + var uri = new URI(url); + if ( !uri.getHost().startsWith("api.") ) { + uri = new URI(uri.getScheme(), uri.getUserInfo(), "api."+uri.getHost(), uri.getPort(), + uri.getPath(), uri.getQuery(), uri.getFragment()); + } + return uri.toString().replaceAll("/+$", ""); + } + + @SneakyThrows + public String getBrowserUrl(String url) { + var uri = new URI(url); + if ( uri.getHost().startsWith("api.") ) { + uri = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost().substring(4), uri.getPort(), + uri.getPath(), uri.getQuery(), uri.getFragment()); + } + return uri.toString().replaceAll("/+$", ""); + } } \ 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/_common/session/cli/mixin/FoDSessionLoginOptions.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java index 0c1cfbd26f..8fd596b5f6 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java @@ -12,12 +12,12 @@ *******************************************************************************/ package com.fortify.cli.fod._common.session.cli.mixin; -import java.net.URI; import java.util.Optional; import com.fortify.cli.common.rest.cli.mixin.UrlConfigOptions; import com.fortify.cli.common.session.cli.mixin.UserCredentialOptions; import com.fortify.cli.common.util.StringUtils; +import com.fortify.cli.fod._common.rest.helper.FoDProductHelper; import com.fortify.cli.fod._common.session.helper.oauth.IFoDClientCredentials; import com.fortify.cli.fod._common.session.helper.oauth.IFoDUserCredentials; @@ -87,13 +87,7 @@ public final boolean hasClientCredentials() { public static final class FoDUrlConfigOptions extends UrlConfigOptions { @Override @SneakyThrows public String getUrl() { - var baseUrl = super.getUrl(); - var uri = new URI(baseUrl); - if ( !uri.getHost().startsWith("api.") ) { - uri = new URI(uri.getScheme(), uri.getUserInfo(), "api."+uri.getHost(), uri.getPort(), - uri.getPath(), uri.getQuery(), uri.getFragment()); - } - return uri.toString(); + return FoDProductHelper.INSTANCE.getApiUrl(super.getUrl()); } @Override 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..9211354af3 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionCommands.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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, + FoDActionHelpCommand.class, + FoDActionImportCommand.class, + FoDActionListCommand.class, + FoDActionResetCommand.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/FoDActionHelpCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionHelpCommand.java new file mode 100644 index 0000000000..8f9b48a197 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionHelpCommand.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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.AbstractActionHelpCommand; + +import picocli.CommandLine.Command; + +@Command(name = "help") +public class FoDActionHelpCommand extends AbstractActionHelpCommand { + @Override + protected final String getType() { + return "FoD"; + } +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionImportCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionImportCommand.java new file mode 100644 index 0000000000..2ce2497985 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionImportCommand.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.AbstractActionImportCommand; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = OutputHelperMixins.Import.CMD_NAME) +public class FoDActionImportCommand extends AbstractActionImportCommand { + @Getter @Mixin OutputHelperMixins.Import 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/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/FoDActionResetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionResetCommand.java new file mode 100644 index 0000000000..1079efe43f --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionResetCommand.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.AbstractActionResetCommand; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = "reset") +public class FoDActionResetCommand extends AbstractActionResetCommand { + @Getter @Mixin OutputHelperMixins.TableNoQuery 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..b91489bba0 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionRunCommand.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.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 FoDProductHelper.INSTANCE.getBrowserUrl(unirestInstanceSupplier.getSessionDescriptor().getUrlConfig().getUrl()); + } + } + + 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..d33cdbfb36 --- /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 + cliAliases: ro + 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 + cliAliases: ao + 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 + cliAliases: r + 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: + - set: + - name: annotations + operation: append + - write: + - to: ${parameters['annotations-output']} + value: ${annotations?:{}} + - to: ${parameters['report-output']} + valueTemplate: report + +valueTemplates: + - 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} + + - 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..1f2bd07057 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-sast-report.yaml @@ -0,0 +1,139 @@ +# For now, github-sast-report and sarif-report actions are exactly the same, apart from usage +# information and default output file name. The reason for having two similar but separate +# actions is two-fold: +# - We want to explicitly show that fcli supports both GitHub Code Scanning integration (which +# just happens to be based on SARIF) and generic SARIF capabilities. +# - Potentially, outputs for GitHub and generic SARIF may deviate in the future, for example if +# we want to add SARIF properties that are not supported by GitHub. +# Until the latter situation arises, we should make sure though that both actions stay in sync; +# when updating one, the other should also be updated. and ideally we should have functional tests +# that compare the outputs of both actions. + +usage: + header: Generate a GitHub Code Scanning report listing FoD SAST 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 + cliAliases: o + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gh-fortify-sast.sarif" + required: false + defaultValue: gh-fortify-sast.sarif + - name: release + cliAliases: r + 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: + - set: + - name: rules + operation: append + - name: results + operation: append + - write: + - to: ${parameters.output} + valueTemplate: github-sast-report + +valueTemplates: + - 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: ${rules?:{}} + results: ${#check(results?.size()>1000 , "GitHub does not support importing more than 1000 vulnerabilities. Please clean the scan results or update vulnerability search criteria.")?results?:{}:{}} + + - 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..eeda6c2f81 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-dast-report.yaml @@ -0,0 +1,151 @@ +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 + cliAliases: o + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-dast.json" + required: false + defaultValue: gl-fortify-dast.json + - name: release + cliAliases: r + 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: + - set: + - name: vulnerabilities + operation: append + - write: + - to: ${parameters.output} + valueTemplate: gitlab-dast-report + +valueTemplates: + - 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: ${vulnerabilities?:{}} + # remediations: ... + + - 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..63dc00dc1d --- /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 + cliAliases: o + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-sast.json" + required: false + defaultValue: gl-fortify-sast.json + - name: release + cliAliases: r + 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: + - set: + - name: vulnerabilities + operation: append + - write: + - to: ${parameters.output} + valueTemplate: gitlab-sast-report + +valueTemplates: + - 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: ${vulnerabilities?:{}} + + - 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/sarif-report.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sarif-report.yaml new file mode 100644 index 0000000000..653b19dcd7 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sarif-report.yaml @@ -0,0 +1,141 @@ +# For now, github-sast-report and sarif-report actions are exactly the same, apart from usage +# information and default output file name. The reason for having two similar but separate +# actions is two-fold: +# - We want to explicitly show that fcli supports both GitHub Code Scanning integration (which +# just happens to be based on SARIF) and generic SARIF capabilities. +# - Potentially, outputs for GitHub and generic SARIF may deviate in the future, for example if +# we want to add SARIF properties that are not supported by GitHub. +# Until the latter situation arises, we should make sure though that both actions stay in sync; +# when updating one, the other should also be updated. and ideally we should have functional tests +# that compare the outputs of both actions. + +usage: + header: Generate SARIF report listing SSC SAST vulnerabilities. + description: | + This action generates a SARIF report listing Fortify SAST vulnerabilities, which + may be useful for integration with various 3rd-party tools that can ingest SARIF + reports. For more information about SARIF, please see + https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html + +defaults: + requestTarget: fod + +parameters: + - name: output + cliAliases: o + description: "Output location; either 'stdout', 'stderr' or file name. Default value: fortify-sast.sarif" + required: false + defaultValue: fortify-sast.sarif + - name: release + cliAliases: r + 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: + - set: + - name: rules + operation: append + - name: results + operation: append + - write: + - to: ${parameters.output} + valueTemplate: github-sast-report + +valueTemplates: + - 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: ${rules?:{}} + results: ${#check(results?.size()>1000 , "GitHub does not support importing more than 1000 vulnerabilities. Please clean the scan results or update vulnerability search criteria.")?results?:{}:{}} + + - 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/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..5bb8749652 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sonarqube-sast-report.yaml @@ -0,0 +1,64 @@ +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 + cliAliases: o + 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 + cliAliases: pfx + description: "Optional prefix for issue file paths" + required: false + defaultValue: "" + - name: release + cliAliases: r + 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: + - set: + - name: sq_issues + operation: append + + - write: + - to: ${parameters.output} + valueTemplate: sq-sast-report + +valueTemplates: + - name: sq-sast-report + contents: + issues: ${sq_issues?:{}} + + - name: sq_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..e0724f8dfe 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,42 @@ 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. - -### For the "fod access-control" command ### +# fcli fod action +fcli.fod.action.usage.header = Run a wide variety of FoD-related actions (data export, integration, automation). +fcli.fod.action.usage.description = Actions are defined in YAML files that define action parameters, workflow \ + steps and (optional) outputs. Fcli ships with various built-in actions, currently mostly focused on exporting \ + issue data to 3rd-party systems or into various output formats. Please see the output of the 'list' command to \ + see the list of available built-in actions. \ + %n%nFcli also supports custom actions, which can be imported using the 'import' command. You can design your \ + custom actions from scratch, or use the built-in actions as a basis for your own custom actions, for example \ + if you wish to modify behavior or output. The 'get' command can be used to view the contents of built-in or \ + imported actions. +fcli.fod.action.get.usage.header = Get action contents. +fcli.fod.action.get.usage.description = This command shows the contents of a built-in or custom action YAML file. \ + This may be useful if you want to use an existing action YAML file as a basis for developing a custom action. +fcli.fod.action.help.usage.header = Show action usage help. +fcli.fod.action.import.usage.header = (PREVIEW) Import one or more actions. +fcli.fod.action.import.usage.description = Import one or more custom actions. You can import either a single \ + action YAML file, or a zip-file containing one or more action YAML files. Imported actions will take precedence \ + over built-in action if they have the same name. \ + %n%nPlease note that actions may have full access to both local and remote system resources, limited only \ + by current user permissions. As such, you should only import & run actions that are provided by trusted sources, \ + and/or review action contents. \ + %n%nThe reason that this command is marked as 'PREVIEW' is that the action YAML file structure is subject to \ + change. Custom action YAML files that work fine on the current fcli version may not work on either older or \ + newer fcli versions, and thus may need to be updated when upgrading fcli. Please see \ + https://github.com/fortify/fcli/issues/515 for details. +fcli.fod.action.list.usage.header = List built-in actions. +fcli.fod.action.reset.usage.header = Remove all custom actions. +fcli.fod.action.run.usage.header = Run an action. +fcli.fod.action.run.usage.description = Run a built-in or custom action. Available actions can be viewed using the \ + 'list' command. \ + %n%nPlease note that actions may have full access to both local and remote system resources, limited only \ + by current user permissions. As such, you should only import & run actions that are provided by trusted sources, \ + and/or review action contents. + + +# 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 +230,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 +278,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 +298,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 +332,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 +348,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 +379,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 +449,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 +612,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 +670,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 +705,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 +761,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.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 1b82ba32a2..0000000000 Binary files a/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/data_extract/templates/SC-SAST.zip and /dev/null differ 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..ce20fde313 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionCommands.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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 = { + SSCActionGetCommand.class, + SSCActionHelpCommand.class, + SSCActionImportCommand.class, + SSCActionListCommand.class, + SSCActionResetCommand.class, + SSCActionRunCommand.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/SSCActionHelpCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionHelpCommand.java new file mode 100644 index 0000000000..fd5c34bee8 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionHelpCommand.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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.AbstractActionHelpCommand; + +import picocli.CommandLine.Command; + +@Command(name = "help") +public class SSCActionHelpCommand extends AbstractActionHelpCommand { + @Override + protected final String getType() { + return "SSC"; + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionImportCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionImportCommand.java new file mode 100644 index 0000000000..e86ffd4a14 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionImportCommand.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.AbstractActionImportCommand; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = OutputHelperMixins.Import.CMD_NAME) +public class SSCActionImportCommand extends AbstractActionImportCommand { + @Getter @Mixin OutputHelperMixins.Import 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/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/SSCActionResetCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionResetCommand.java new file mode 100644 index 0000000000..9f7c20d0a3 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionResetCommand.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.AbstractActionResetCommand; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = "reset") +public class SSCActionResetCommand extends AbstractActionResetCommand { + @Getter @Mixin OutputHelperMixins.TableNoQuery 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..e931b51b70 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bitbucket-sast-report.yaml @@ -0,0 +1,142 @@ +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 + cliAliases: ro + 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 + cliAliases: ao + 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 + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filterset + cliAliases: fs + 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: + - set: + - name: annotations + operation: append + - write: + - to: ${parameters['annotations-output']} + value: ${annotations?:{}} + - to: ${parameters['report-output']} + valueTemplate: report + +valueTemplates: + - name: report + contents: + # uuid: + title: Fortify Scan Report + details: Fortify detected ${annotations?.size()?:0} static ${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} + + - 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..1b150b7b19 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-decoration.yaml @@ -0,0 +1,138 @@ +usage: + header: Generate GitHub Pull Request decoration. + description: | + This template generates GitHub Pull Request review comments. + +parameters: + - name: appversion + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filterset + cliAliases: fs + 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. Default value: GITHUB_TOKEN environment variable.' + required: true + defaultValue: ${#env('GITHUB_TOKEN')} + - name: github-owner + description: 'Required GitHub repository owner. Default value: GITHUB_REPOSITORY_OWNER environment variable.' + required: true + defaultValue: ${#env('GITHUB_REPOSITORY_OWNER')} + - name: github-repo + description: 'Required GitHub repository. Default value: Taken from GITHUB_REPOSITORY environment variable.' + required: true + defaultValue: ${#substringAfter(#env('GITHUB_REPOSITORY'),'/')} + - name: pr + description: 'Required PR number. Default value: Taken from GITHUB_REF_NAME environment variable.' + required: true + defaultValue: ${#substringBefore(#env('GITHUB_REF_NAME'),'/')} + - name: commit + description: 'Required commit hash. Default value: GITHUB_SHA environment variable.' + required: true + defaultValue: ${#env('GITHUB_SHA')} + +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) + set: + - name: removedIssues + operation: append + - if: scanDates.contains(issue.foundDate) && issue.engineType=='SCA' + set: + - name: newStaticIssues + operation: append + - if: scanDates.contains(issue.foundDate) && issue.engineType!='SCA' + set: + - name: newOtherIssues + operation: append + - progress: Sending GitHub request + - set: + - 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: ${reviewRequestBody} + +valueTemplates: + - name: reviewBody + contents: | + ${newStaticIssues==null && newOtherIssues==null + ? "Fortify didn't detect any new potential vulnerabilities" + : "Fortify detected potential vulnerabilities"} + ${newStaticIssues==null + ? '' + : '### New Issues (SAST)\n\nSee file comments below.'} + ${newOtherIssues==null + ? '' + : ('### New Issues (non-SAST)\n\n* '+#join('\n* ',newOtherIssues))} + ${removedIssues==null + ? '' + : ('### Removed Issues\n\n* '+#join('\n* ',removedIssues))} + - name: reviewRequestBody + contents: + owner: ${parameters['github-owner']} + repo: ${parameters['github-repo']} + pull_number: ${parameters['pr']} + commit_id: ${parameters['commit']} + body: ${reviewBody} + event: COMMENT + comments: ${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..26457f4098 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-sast-report.yaml @@ -0,0 +1,149 @@ +# For now, github-sast-report and sarif-report actions are exactly the same, apart from usage +# information and default output file name. The reason for having two similar but separate +# actions is two-fold: +# - We want to explicitly show that fcli supports both GitHub Code Scanning integration (which +# just happens to be based on SARIF) and generic SARIF capabilities. +# - Potentially, outputs for GitHub and generic SARIF may deviate in the future, for example if +# we want to add SARIF properties that are not supported by GitHub. +# Until the latter situation arises, we should make sure though that both actions stay in sync; +# when updating one, the other should also be updated. and ideally we should have functional tests +# that compare the outputs of both actions. + +usage: + header: Generate a GitHub Code Scanning report listing SSC SAST 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 + cliAliases: o + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gh-fortify-sast.sarif" + required: false + defaultValue: gh-fortify-sast.sarif + - name: appversion + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filterset + cliAliases: fs + 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: + - set: + - name: rules + operation: append + - name: results + operation: append + - write: + - to: ${parameters.output} + valueTemplate: github-sast-report + +valueTemplates: + - 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: ${rules?:{}} + results: ${#check(results?.size()>1000 , "GitHub does not support importing more than 1000 vulnerabilities. Please clean the scan results or update vulnerability search criteria.")?results?:{}:{}} + + - 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..2bceafd022 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-dast-report.yaml @@ -0,0 +1,157 @@ +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 + cliAliases: o + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-dast.json" + required: false + defaultValue: gl-fortify-dast.json + - name: appversion + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filterset + cliAliases: fs + 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: + - set: + - name: vulnerabilities + operation: append + - write: + - to: ${parameters.output} + valueTemplate: gitlab-dast-report + +valueTemplates: + - 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: ${vulnerabilities?:{}} + # remediations: ... + + - 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..9d05b01ebc --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-debricked-report.yaml @@ -0,0 +1,117 @@ +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 + cliAliases: o + 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 + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filterset + cliAliases: fs + 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: + - set: + - name: vulnerabilities + operation: append + - write: + - to: ${parameters.output} + valueTemplate: gitlab-debricked-report + +valueTemplates: + - 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: ${vulnerabilities?:{}} + + - 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..4ed1d16275 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sast-report.yaml @@ -0,0 +1,115 @@ +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 + cliAliases: o + description: "Output location; either 'stdout', 'stderr' or file name. Default value: gl-fortify-sast.json" + required: false + defaultValue: gl-fortify-sast.json + - name: appversion + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filterset + cliAliases: fs + 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: + - set: + - name: vulnerabilities + operation: append + - write: + - to: ${parameters.output} + valueTemplate: gitlab-sast-report + + +valueTemplates: + - 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: ${vulnerabilities?:{}} + - 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..83e4f5eb4e --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sonatype-report.yaml @@ -0,0 +1,116 @@ +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 + cliAliases: o + 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 + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filterset + cliAliases: fs + 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: + - set: + - name: vulnerabilities + operation: append + - write: + - to: ${parameters.output} + valueTemplate: gitlab-sonatype-report + +valueTemplates: + - 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: ${vulnerabilities?:{}} + - 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/sarif-report.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sarif-report.yaml new file mode 100644 index 0000000000..5218689ff3 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sarif-report.yaml @@ -0,0 +1,151 @@ +# For now, github-sast-report and sarif-report actions are exactly the same, apart from usage +# information and default output file name. The reason for having two similar but separate +# actions is two-fold: +# - We want to explicitly show that fcli supports both GitHub Code Scanning integration (which +# just happens to be based on SARIF) and generic SARIF capabilities. +# - Potentially, outputs for GitHub and generic SARIF may deviate in the future, for example if +# we want to add SARIF properties that are not supported by GitHub. +# Until the latter situation arises, we should make sure though that both actions stay in sync; +# when updating one, the other should also be updated. and ideally we should have functional tests +# that compare the outputs of both actions. + +usage: + header: Generate SARIF report listing SSC SAST vulnerabilities. + description: | + This action generates a SARIF report listing Fortify SAST vulnerabilities, which + may be useful for integration with various 3rd-party tools that can ingest SARIF + reports. For more information about SARIF, please see + https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html + +defaults: + requestTarget: ssc + +parameters: + - name: output + cliAliases: o + description: "Output location; either 'stdout', 'stderr' or file name. Default value: fortify-sast.sarif" + required: false + defaultValue: fortify-sast.sarif + - name: appversion + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filterset + cliAliases: fs + 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: + - set: + - name: rules + operation: append + - name: results + operation: append + - write: + - to: ${parameters.output} + valueTemplate: github-sast-report + +valueTemplates: + - 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: ${rules?:{}} + results: ${#check(results?.size()>1000 , "GitHub does not support importing more than 1000 vulnerabilities. Please clean the scan results or update vulnerability search criteria.")?results?:{}:{}} + + - 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/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..a30f20ad8d --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sonarqube-sast-report.yaml @@ -0,0 +1,67 @@ +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 + cliAliases: o + 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 + cliAliases: pfx + description: "Optional prefix for issue file paths" + required: false + defaultValue: "" + - name: appversion + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filterset + cliAliases: fs + 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: + - set: + - name: sq_issues + operation: append + - write: + - to: ${parameters.output} + valueTemplate: sq_output + +valueTemplates: + - name: sq_output + contents: + issues: ${sq_issues?:{}} + - name: sq_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..a625bb353f 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,43 @@ 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 = Run a wide variety of SSC-related actions (data export, integration, automation). +fcli.ssc.action.usage.description = Actions are defined in YAML files that define action parameters, workflow \ + steps and (optional) outputs. Fcli ships with various built-in actions, currently mostly focused on exporting \ + issue data to 3rd-party systems or into various output formats. Please see the output of the 'list' command to \ + see the list of available built-in actions. \ + %n%nFcli also supports custom actions, which can be imported using the 'import' command. You can design your \ + custom actions from scratch, or use the built-in actions as a basis for your own custom actions, for example \ + if you wish to modify behavior or output. The 'get' command can be used to view the contents of built-in or \ + imported actions. +fcli.ssc.action.get.usage.header = Get action contents. +fcli.ssc.action.get.usage.description = This command shows the contents of a built-in or custom action YAML file. \ + This may be useful if you want to use an existing action YAML file as a basis for developing a custom action. +fcli.ssc.action.help.usage.header = Show action usage help. +fcli.ssc.action.import.usage.header = (PREVIEW) Import one or more actions. +fcli.ssc.action.import.usage.description = Import one or more custom actions. You can import either a single \ + action YAML file, or a zip-file containing one or more action YAML files. Imported actions will take precedence \ + over built-in action if they have the same name. \ + %n%nPlease note that actions may have full access to both local and remote system resources, limited only \ + by current user permissions. As such, you should only import & run actions that are provided by trusted sources, \ + and/or review action contents. \ + %n%nThe reason that this command is marked as 'PREVIEW' is that the action YAML file structure is subject to \ + change. Custom action YAML files that work fine on the current fcli version may not work on either older or \ + newer fcli versions, and thus may need to be updated when upgrading fcli. Please see \ + https://github.com/fortify/fcli/issues/515 for details. +fcli.ssc.action.list.usage.header = List built-in actions. +fcli.ssc.action.reset.usage.header = Remove all custom actions. +fcli.ssc.action.run.usage.header = Run an action. +fcli.ssc.action.run.usage.description = Run a built-in or custom action. Available actions can be viewed using the \ + 'list' command. \ + %n%nPlease note that actions may have full access to both local and remote system resources, limited only \ + by current user permissions. As such, you should only import & run actions that are provided by trusted sources, \ + and/or review 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 +502,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.output.table.options = name,isCustomString,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..18adaafd5d 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/**']) + fileTree(dir: 'src/main/resources', excludes: ['**/i18n/**', 'META-INF/**', '**/zip/**']) .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;