From 194a8a91dce320fecc2c45917d6b8e94d08ea55d Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:23:51 +0100 Subject: [PATCH] chore: Partial data-extract implementation --- fcli-core/fcli-common/build.gradle | 6 + .../cli/cmd/AbstractActionGetCommand.java | 30 + .../cli/cmd/AbstractActionHelpCommand.java | 47 ++ .../cli/cmd/AbstractActionImportCommand.java | 54 ++ .../cli/cmd/AbstractActionListCommand.java | 37 + .../cli/cmd/AbstractActionResetCommand.java | 37 + .../cli/cmd/AbstractActionRunCommand.java | 78 ++ .../action/helper/ActionDescriptor.java | 480 ++++++++++++ .../common/action/helper/ActionHelper.java | 276 +++++++ .../action/helper/ActionParameterHelper.java | 70 ++ .../common/action/helper/ActionRunner.java | 737 ++++++++++++++++++ .../action/helper/ActionSpelFunctions.java | 157 ++++ .../common/cli/util/SimpleOptionsParser.java | 139 ++++ .../cli/common/json/AbstractJsonWalker.java | 75 ++ .../common/json/JSONDateTimeConverter.java | 85 ++ .../fortify/cli/common/json/JsonHelper.java | 116 ++- .../output/standard/StandardOutputWriter.java | 9 +- .../progress/helper/ProgressWriterType.java | 18 +- .../rest/cli/cmd/AbstractRestCallCommand.java | 2 +- .../cli/common/rest/paging/PagingHelper.java | 19 + .../IConfigurableSpelEvaluator.java | 21 + .../spring/expression/ISpelEvaluator.java | 20 + .../spring/expression/SpelEvaluator.java | 67 +- ...ctions.java => SpelFunctionsStandard.java} | 2 +- .../common/spring/expression/SpelHelper.java | 14 + .../expression/wrapper/SimpleExpression.java | 47 ++ .../wrapper/SimpleExpressionDeserializer.java | 55 ++ .../wrapper/TemplateExpression.java | 47 ++ .../TemplateExpressionDeserializer.java | 55 ++ .../expression/wrapper/WrappedExpression.java | 292 +++++++ .../fortify/cli/common/util/FileUtils.java | 5 + .../fortify/cli/common/util/StringUtils.java | 12 +- .../json/JsonPropertyAccessor.java | 20 +- .../integration/json/package-info.java | 6 +- .../cli/common/actions/zip/__sample__.yaml | 233 ++++++ .../common/i18n/FortifyCLIMessages.properties | 10 + .../json/JsonNodeDeepCopyWalkerTest.java | 105 +++ .../cli/common/rest/wait/WaitHelperTest.java | 5 - fcli-core/fcli-fod/build.gradle | 6 + .../_common/rest/helper/FoDProductHelper.java | 24 + .../cmd/AbstractFoDScanFileUploadCommand.java | 1 + .../cli/cmd/AbstractFoDScanSetupCommand.java | 6 +- .../scan/cli/mixin/FoDDastFileTypeMixins.java | 1 + .../scan/helper/FoDFileUploadDescriptor.java | 1 + .../FoDScanAssessmentTypeDescriptor.java | 6 +- .../_common/scan/helper/FoDScanHelper.java | 27 +- .../_common/scan/helper/FoDScanStatus.java | 6 +- .../dast/FoDScanDastAutomatedHelper.java | 2 +- .../FoDScanDastAutomatedSetupBaseRequest.java | 8 +- ...DScanDastAutomatedSetupGraphQlRequest.java | 1 + .../FoDScanDastAutomatedSetupGrpcRequest.java | 1 + ...DScanDastAutomatedSetupOpenApiRequest.java | 1 + ...DScanDastAutomatedSetupPostmanRequest.java | 5 +- ...DScanDastAutomatedSetupWebsiteRequest.java | 14 +- ...ScanDastAutomatedSetupWorkflowRequest.java | 14 +- .../cli/mixin/FoDSessionLoginOptions.java | 10 +- .../cli/fod/_main/cli/cmd/FoDCommands.java | 2 + .../fod/action/cli/cmd/FoDActionCommands.java | 31 + .../action/cli/cmd/FoDActionGetCommand.java | 26 + .../action/cli/cmd/FoDActionHelpCommand.java | 25 + .../cli/cmd/FoDActionImportCommand.java | 30 + .../action/cli/cmd/FoDActionListCommand.java | 30 + .../action/cli/cmd/FoDActionResetCommand.java | 30 + .../action/cli/cmd/FoDActionRunCommand.java | 87 +++ .../FoDDastAutomatedScanGetConfigCommand.java | 1 + .../FoDDastAutomatedScanSetupApiCommand.java | 8 +- ...DDastAutomatedScanSetupWebsiteCommand.java | 7 +- ...DastAutomatedScanSetupWorkflowCommand.java | 5 +- .../cmd/FoDDastAutomatedScanStartCommand.java | 2 +- .../cmd/FoDDastLegacyScanStartCommand.java | 2 +- .../cli/cmd/FoDDastScanFileUploadCommand.java | 1 + .../FoDScanConfigDastAutomatedDescriptor.java | 1 + .../fod/report/cli/cmd/FoDReportCommands.java | 1 + .../cli/cmd/FoDReportCreateCommand.java | 2 +- .../cli/cmd/FoDReportDeleteCommand.java | 2 +- .../cli/cmd/FoDReportDownloadCommand.java | 6 +- .../report/cli/cmd/FoDReportGetCommand.java | 3 +- .../report/cli/cmd/FoDReportListCommand.java | 4 +- .../cli/cmd/FoDReportTemplateListCommand.java | 1 + .../cli/cmd/FoDReportWaitForCommand.java | 6 +- .../cli/mixin/FoDReportResolverMixin.java | 10 +- ...ReportTemplateByNameOrIdResolverMixin.java | 1 + .../report/helper/FoDReportCreateRequest.java | 7 +- .../report/helper/FoDReportDescriptor.java | 1 + .../fod/report/helper/FoDReportHelper.java | 1 + .../fod/report/helper/FoDReportStatus.java | 6 +- .../helper/FoDReportTemplateDescriptor.java | 1 + .../helper/FoDReportTemplateHelper.java | 7 +- .../actions/zip/bitbucket-sast-report.yaml | 108 +++ .../fod/actions/zip/github-pr-comment.yaml | 152 ++++ .../fod/actions/zip/github-sast-report.yaml | 142 ++++ .../fod/actions/zip/gitlab-dast-report.yaml | 152 ++++ .../fod/actions/zip/gitlab-sast-report.yaml | 104 +++ .../cli/fod/actions/zip/sarif-report.yaml | 144 ++++ .../actions/zip/sonarqube-sast-report.yaml | 64 ++ .../cli/fod/i18n/FoDMessages.properties | 62 +- .../cli/data_extract/templates/SC-SAST.zip | Bin 200 -> 0 bytes .../sc_sast/i18n/SCSastMessages.properties | 3 +- fcli-core/fcli-ssc/build.gradle | 6 + .../rest/helper/SSCInputTransformer.java | 2 +- .../cli/ssc/_main/cli/cmd/SSCCommands.java | 2 + .../ssc/action/cli/cmd/SSCActionCommands.java | 31 + .../action/cli/cmd/SSCActionGetCommand.java | 26 + .../action/cli/cmd/SSCActionHelpCommand.java | 25 + .../cli/cmd/SSCActionImportCommand.java | 30 + .../action/cli/cmd/SSCActionListCommand.java | 30 + .../action/cli/cmd/SSCActionResetCommand.java | 30 + .../action/cli/cmd/SSCActionRunCommand.java | 136 ++++ .../actions/zip/bitbucket-sast-report.yaml | 142 ++++ .../ssc/actions/zip/github-pr-comment.yaml | 155 ++++ .../actions/zip/github-pr-comment.yaml.bak | 158 ++++ .../ssc/actions/zip/github-sast-report.yaml | 152 ++++ .../ssc/actions/zip/gitlab-dast-report.yaml | 157 ++++ .../actions/zip/gitlab-debricked-report.yaml | 117 +++ .../ssc/actions/zip/gitlab-sast-report.yaml | 115 +++ .../actions/zip/gitlab-sonatype-report.yaml | 116 +++ .../cli/ssc/actions/zip/sarif-report.yaml | 154 ++++ .../actions/zip/sonarqube-sast-report.yaml | 67 ++ .../cli/ssc/i18n/SSCMessages.properties | 37 + fcli-other/fcli-bom/build.gradle | 6 +- .../src/docs/asciidoc/versioned/index.adoc | 11 + fcli-other/fcli-gradle/fcli-java.gradle | 53 +- 122 files changed, 6526 insertions(+), 132 deletions(-) create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionGetCommand.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionHelpCommand.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionImportCommand.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionListCommand.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionResetCommand.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionHelper.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionParameterHelper.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSpelFunctions.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/SimpleOptionsParser.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/AbstractJsonWalker.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JSONDateTimeConverter.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/IConfigurableSpelEvaluator.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/ISpelEvaluator.java rename fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/{StandardSpelFunctions.java => SpelFunctionsStandard.java} (98%) create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpression.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpressionDeserializer.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpression.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpressionDeserializer.java create mode 100644 fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/WrappedExpression.java create mode 100644 fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/actions/zip/__sample__.yaml create mode 100644 fcli-core/fcli-common/src/test/java/com/fortify/cli/common/json/JsonNodeDeepCopyWalkerTest.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionCommands.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionGetCommand.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionHelpCommand.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionImportCommand.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionListCommand.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionResetCommand.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionRunCommand.java create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/bitbucket-sast-report.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-pr-comment.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-sast-report.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-dast-report.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-sast-report.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sarif-report.yaml create mode 100644 fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sonarqube-sast-report.yaml delete mode 100644 fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/data_extract/templates/SC-SAST.zip create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionCommands.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionGetCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionHelpCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionImportCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionListCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionResetCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionRunCommand.java create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bitbucket-sast-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml.bak create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-sast-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-dast-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-debricked-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sast-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sonatype-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sarif-report.yaml create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sonarqube-sast-report.yaml diff --git a/fcli-core/fcli-common/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..b97268b66c --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionDescriptor.java @@ -0,0 +1,480 @@ +/** + * 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
  • + *
  • 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 exception message template expression for this step element */ + @JsonProperty("throw") private TemplateExpression _throw; + /** 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 steps to be executed on the response before executing forEach steps */ + private List onResponse; + /** Optional forEach block to be repeated for every response element */ + private ActionStepForEachDescriptor forEach; + /** Optional action to take on request failure */ + private ActionStepRequestOnFail onFail = ActionStepRequestOnFail.error; + + /** + * 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 + } + + public static enum ActionStepRequestOnFail { + error, warn, ignore + } + + @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..5d5b957840 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionRunner.java @@ -0,0 +1,737 @@ +/** + * 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.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.UnirestException; +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(); + /** Logger */ + private static final Logger LOG = LoggerFactory.getLogger(ActionRunner.class); + /** 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::get_throw, this::processThrowStep); + 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 processThrowStep(TemplateExpression message) { + throw new RuntimeException(spelEvaluator.evaluate(message, 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); + processOnResponse(requestDescriptor); + processForEach(requestDescriptor); + } + + private final void processOnResponse(ActionStepRequestDescriptor requestDescriptor) { + var onResponseSteps = requestDescriptor.getOnResponse(); + processSteps(onResponseSteps); + } + + 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 onFail = getOnFailConsumer(requestDescriptor); + var requestData = new IActionRequestHelper.ActionRequestDescriptor(requestDescriptor.getMethod().toString(), uri, query, body, onFail, 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 Consumer getOnFailConsumer(ActionStepRequestDescriptor requestDescriptor) { + return e->{ + switch ( requestDescriptor.getOnFail() ) { + case error: + throw e; + case warn: + progressWriter.writeWarning(e.getMessage()); + LOG.debug("WARN: request failure", e); + break; + case ignore: + LOG.debug("Ignoring request failure", e); + } + }; + } + + 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 onFail; + 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(); + try { + PagingHelper.processPages(unirest, request, nextPageUrlProducer, r->{ + requestDescriptor.postPageLoad(); + requestDescriptor.getResponseConsumer().accept(r.getBody()); + requestDescriptor.postPageProcess(); + }); + } catch ( UnirestException e ) { + requestDescriptor.getOnFail().accept(e); + } + } + @Override + public void executeSimpleRequests(List requestDescriptors) { + var unirest = getUnirestInstance(); + requestDescriptors.forEach(r->executeSimpleRequest(unirest, r)); + } + private void executeSimpleRequest(UnirestInstance unirest, ActionRequestDescriptor requestDescriptor) { + try { + createRequest(unirest, requestDescriptor) + .asObject(JsonNode.class) + .ifSuccess(r->requestDescriptor.getResponseConsumer().accept(r.getBody())); + } catch ( UnirestException e ) { + requestDescriptor.getOnFail().accept(e); + } + } + + 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..a487ae82bf --- /dev/null +++ b/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/actions/zip/__sample__.yaml @@ -0,0 +1,233 @@ +# 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). + # - onFail: Action to take if request fails, either 'error', 'warn' or 'ignore'. + # Default value: 'error' + # - 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. + # - onResponse: Optional; list of steps to execute after a response has been + # received. These steps are executed for every page (if this is + # a paged request), and before any forEach block is executed. + # - 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 + # onFail: error + # pagingProgress: + # prePageLoad: ... + # postPageLoad: ... + # postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + # onResponse: + # - steps + 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-pr-comment.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-pr-comment.yaml new file mode 100644 index 0000000000..b0a00730ab --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-pr-comment.yaml @@ -0,0 +1,152 @@ +# For now, this template uses latest release state to generate PR decorations. +# See corresponding .bak file is SSC module for an example of how to better +# implement this, once FoD supports retrieving new/re-introduced/removed isses +# for a particular scan id/PR number/commit id. +usage: + header: (PREVIEW) Add GitHub Pull Request review comments. + description: | + This action adds review comments to a GitHub Pull Request. Currently + this is marked as PREVIEW as we build out this functionality; later + versions may have different behavior and/or require different action + parameters. In particular, note that comments are generated based on + current (latest) FoD release state, i.e., based on the last uploaded + scan results. As such, to ensure the comments are accurate for the + given PR/commit id, this action should be run immediately after scan + results have been published, before any subsequent scans are being + published. Also, for now this action doesn't generate any source code + annotations, as GitHub will return an error if vulnerability path & file + name don't match exactly with repository path & file name. + +parameters: + - name: release + cliAliases: r + description: "Required release id or :[:]" + type: release_single + - 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')} + - name: dryrun + description: "Set to true to just output PR decoration JSON; don't actually update any PR" + type: boolean + required: false + defaultValue: false + +addRequestTargets: + - name: github + baseUrl: https://api.github.com + headers: + Authorization: Bearer ${parameters['github-token']} + 'X-GitHub-Api-Version': '2022-11-28' + +defaults: + requestTarget: fod + +steps: + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities?limit=50 + query: + includeFixed: true + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.totalCount} issues + forEach: + name: issue + if: issue.status!='Existing' + embed: + - name: details + uri: /api/v3/releases/${parameters.release.releaseId}/vulnerabilities/${issue.vulnId}/details + do: + - if: issue.status=='Fix Validated' + set: + - name: removedIssues + valueTemplate: mdIssueListItem + operation: append + - if: (issue.status=='New' || issue.status=='Reopen') + set: + - name: newIssues + valueTemplate: mdIssueListItem + operation: append + - if: (issue.status=='New' || issue.status=='Reopen') && issue.engineType=='Static' + set: + - name: jsonSourceCodeComments + valueTemplate: jsonSourceCodeComment + operation: append + + - progress: Generating GitHub request + - set: + - name: reviewBody + - name: reviewRequestBody + - if: parameters.dryrun + write: + - to: stdout + value: ${reviewRequestBody} + - if: '!parameters.dryrun' + 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: reviewRequestBody + contents: + owner: ${parameters['github-owner']} + repo: ${parameters['github-repo']} + pull_number: ${parameters['pr']} + commit_id: ${parameters['commit']} + body: ${reviewBody} + event: COMMENT + # For now, we don't include any source code comments, as this will cause + # GitHub to return an error if the source file doesn't exist in the repo. + comments: ${{}} + # comments: ${jsonSourceCodeComments?:{}} + + - name: reviewBody + contents: | + ### New Issues + + ${newIssues==null + ? "* No new or re-introduced issues were detected" + : ("* "+#join('\n* ',newIssues))} + + ### Removed Issues + + ${removedIssues==null + ? "* No removed issues were detected" + : ("* "+#join('\n* ',removedIssues))} + + - name: jsonSourceCodeComment + contents: + path: ${issue.primaryLocationFull} + line: ${issue.lineNumber==0?1:issue.lineNumber} + body: | +

Security Scanning / Fortify SAST

+

${issue.severityString} - ${issue.category}

+

${#htmlToText(issue.details?.summary)}

+
+

More information

+ - name: mdIssueListItem + contents: > + ${issue.status} (${issue.scantype}): [${issue.primaryLocationFull}${issue.lineNumber==null?'':':'+issue.lineNumber} - ${issue.category}](${#fod.issueBrowserUrl(issue)}) + + + \ No newline at end of file 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..9d47bc0bfb --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-sast-report.yaml @@ -0,0 +1,142 @@ +# 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 + onResponse: + - if: issues_raw.totalCount>1000 + throw: GitHub does not support importing more than 1000 vulnerabilities. Please clean the scan results or update vulnerability search criteria. + 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: ${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..1fd6f343c5 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-dast-report.yaml @@ -0,0 +1,152 @@ +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 + onFail: ignore + - 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..7633eb5815 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sarif-report.yaml @@ -0,0 +1,144 @@ +# For now, github-sast-report and sarif-report actions are exactly the same, apart from the +# following: +# - Different usage information +# - Different default output file name +# - The sarif-report doesn't impose a limit of 1000 issues +# 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: ${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 1b82ba32a22f924c0992f82213ceb0041b8bc8f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200 zcmWIWW@h1H00Dj%j|eaWN=P!uFr;UeWRxc9Cg-Q5>J}#_=H+GPrR!BD=H`Tka56CW zHXlg^;nE6j21b?_%nS@*BEXxGNsbwpp%QRAmNbHxc&uQBSb0l6t F0RY0&CsY6c diff --git a/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties b/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties index 9c79d702ee..a08ea38f98 100644 --- a/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties +++ b/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties @@ -100,7 +100,8 @@ fcli.sc-sast.session.list.usage.description.1 = For sessions created using user expired. Similarly, any changes to token validity will not be reflected in the output of this command. %n fcli.sc-sast.session.list.usage.description.2 = For sessions created using a pre-generated token, fcli cannot \ display session expiration date or status, as SSC doesn't allow for obtaining this information. - + + # fcli sc-sast scan fcli.sc-sast.scan.usage.header = Manage ScanCentral SAST scans. fcli.sc-sast.scan.cancel.usage.header = Cancel a previously submitted scan request. diff --git a/fcli-core/fcli-ssc/build.gradle b/fcli-core/fcli-ssc/build.gradle index 8e063597b9..afc0644179 100644 --- a/fcli-core/fcli-ssc/build.gradle +++ b/fcli-core/fcli-ssc/build.gradle @@ -1 +1,7 @@ +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") + include '*.yaml' +} 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-comment.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml new file mode 100644 index 0000000000..65f19f66aa --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml @@ -0,0 +1,155 @@ +# For now, this template uses latest application state to generate PR decorations. +# See corresponding .bak file for a better but incomplete (due to SSC limitations) +# implementation based on artifact id. +usage: + header: (PREVIEW) Add GitHub Pull Request review comments. + description: | + This action adds review comments to a GitHub Pull Request. Currently + this is marked as PREVIEW as we build out this functionality; later + versions may have different behavior and/or require different action + parameters. In particular, note that comments are generated based on + current (latest) SSC application state, i.e., based on the last + uploaded scan results. As such, to ensure the comments are accurate + for the given PR/commit id, this action should be run immediately + after scan results have been published (and approved if necessary), + before any subsequent scans are being published. Also, for now this + action doesn't generate any source code annotations, as GitHub will + return an error if vulnerability path & file name don't match exactly + with repository path & file name. + +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: 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')} + - name: dryrun + description: "Set to true to just output PR decoration JSON; don't actually update any PR" + type: boolean + required: false + defaultValue: false + +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: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues?limit=100&orderby=engineType + query: + showremoved: true + filterset: ${parameters.filterset.guid} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + if: issue.scanStatus!='UPDATED' + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - if: issue.scanStatus=='REMOVED' + set: + - name: removedIssues + valueTemplate: mdIssueListItem + operation: append + - if: (issue.scanStatus=='NEW' || issue.scanStatus=='REINTRODUCED') + set: + - name: newIssues + valueTemplate: mdIssueListItem + operation: append + - if: (issue.scanStatus=='NEW' || issue.scanStatus=='REINTRODUCED') && issue.engineType=='SCA' + set: + - name: jsonSourceCodeComments + valueTemplate: jsonSourceCodeComment + operation: append + + - progress: Generating GitHub request + - set: + - name: reviewBody + - name: reviewRequestBody + - if: parameters.dryrun + write: + - to: stdout + value: ${reviewRequestBody} + - if: '!parameters.dryrun' + 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: reviewRequestBody + contents: + owner: ${parameters['github-owner']} + repo: ${parameters['github-repo']} + pull_number: ${parameters['pr']} + commit_id: ${parameters['commit']} + body: ${reviewBody} + event: COMMENT + # For now, we don't include any source code comments, as this will cause + # GitHub to return an error if the source file doesn't exist in the repo. + comments: ${{}} + # comments: ${jsonSourceCodeComments?:{}} + + - name: reviewBody + contents: | + ### New Issues + + ${newIssues==null + ? "* No new or re-introduced issues were detected" + : ("* "+#join('\n* ',newIssues))} + + ### Removed Issues + + ${removedIssues==null + ? "* No removed issues were detected" + : ("* "+#join('\n* ',removedIssues))} + + - name: jsonSourceCodeComment + 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 information

+ - name: mdIssueListItem + contents: > + ${issue.scanStatus} (${issue.engineCategory}): [${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-pr-comment.yaml.bak b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml.bak new file mode 100644 index 0000000000..7e5e7c045d --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml.bak @@ -0,0 +1,158 @@ +# This template would ideally get the list of new, removed and re-introduced issues +# based on a given artifact id/scan id/commit# (in scan label)/pr# (in scan label). +# However, although current implementation works for new and removed issues by matching +# scan date against issue foundDate/removedDate, it's impossible to identify re-introduced +# issues due to SSC REST API limitatations. For re-introduced issues, SSC keeps the +# original foundDate and resets removedDate to null (so neither of these dates match +# current scan date), and there's no issue reintroducedDate or similar field. +# As such, for now we provide a separate template that's based on current application +# state, rather than trying to identify issues for a particular scan. Based on the +# '.bak' extension of this file, it won't be included in fcli artifacts. +usage: + header: Generate GitHub Pull Request decoration. + description: | + This action 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')} + - name: dryrun + description: "Set to true to just output PR decoration JSON; don't actually update any PR" + type: boolean + required: false + defaultValue: false + +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: Generating GitHub request + - set: + - name: reviewBody + - name: reviewRequestBody + - if: parameters.dryrun + write: + - to: stdout + value: ${reviewRequestBody} + - if: '!parameters.dryrun' + 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..53030a385c --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-sast-report.yaml @@ -0,0 +1,152 @@ +# 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 + onResponse: + - if: issues_raw.count>1000 + throw: GitHub does not support importing more than 1000 vulnerabilities. Please clean the scan results or update vulnerability search criteria. + 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: ${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..7559c45bdd --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sarif-report.yaml @@ -0,0 +1,154 @@ +# For now, github-sast-report and sarif-report actions are exactly the same, apart from the +# following: +# - Different usage information +# - Different default output file name +# - The sarif-report doesn't impose a limit of 1000 issues +# 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: ${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-doc/src/docs/asciidoc/versioned/index.adoc b/fcli-other/fcli-doc/src/docs/asciidoc/versioned/index.adoc index d247efef90..afead5b95a 100644 --- a/fcli-other/fcli-doc/src/docs/asciidoc/versioned/index.adoc +++ b/fcli-other/fcli-doc/src/docs/asciidoc/versioned/index.adoc @@ -15,6 +15,7 @@ Some of the fcli highlights: * Support for configuring option values through link:#_environment_variables[environment variables] * Support for link:#_fcli_variables[fcli variables]; pass data between fcli commands * Support for installing other Fortify-related tools +* Support for running multi-purpose YAML-based link:#_actions[actions] The following Fortify products are currently supported by fcli: @@ -445,6 +446,16 @@ fcli util variable contents myVersions -o 'expr={id}\n' --output-to-file myVersi Note: variable-related syntax and behavior was changed in fcli version 2.0.0. If you are using an older fcli version, please refer to the documentation for that version. When upgrading from 1.x.x to 2.x.x, you may need to update your fcli invocations to match the new syntax. See link:https://github.com/fortify/fcli/issues/160[Issue 160] for information on why syntax and behavior was changed. +== Actions + +Various fcli modules support managing and running multi-purpose actions through the `fcli action *` commands. For now, actions are supported for FoD and SSC modules, but we can easily add support on ScanCentral SAST & DAST modules if there's any demand. Actions are defined in action YAML files that describe action parameters and steps to be executed. These steps currently focus on executing and processing series of REST requests against FoD, SSC, or third-party systems. Future versions may provide additional functionality, like additional processing logic, or running a workflow of fcli commands. + +Fcli ships with many built-in actions but custom actions are also supported. For now, custom actions are considered `PREVIEW` functionality, primarily because action YAML syntax may change between fcli versions, i.e., an action that successfully runs on fcli 2.4.0 may fail to run on fcli 2.5.0, or vice versa. We plan on stabilizing the action YAML syntax over the next couple of fcli releases, and also implement proper version checks to prevent older fcli versions from attempting to run action YAML files designed for newer fcli versions. + +The `+fcli action get __sample__+` command outputs sample action YAML contents, listing all supported action YAML elements together with a description. Of course, if you want to modify the behavior or output of any of the built-in actions, you can use the same `get` command to view action YAML contents and customize as needed. + +Current built-in actions mostly focus on providing vulnerability export capabilities similar to link:https://github.com/fortify/FortifyVulnerabilityExporter[FortifyVulnerabilityExporter], allowing us to eventually deprecate FortifyVulnerabilityExporter. Fcli also provides some new actions though, for example for generating GitHub Pull Request decorations. Over time, we'll be adding more built-in actions. + == Manual Pages Manual pages are automatically generated and contain the same information as fcli help output. Manual pages in HTML and Linux man-page formats can be downloaded for offline use from the fcli releases page at https://github.com/fortify/fcli/releases, or can be viewed online at https://fortify.github.io/fcli. 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;