diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01e971638b..889ca9e978 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: env: native_image_opts: --verbose -H:Log=registerResource:verbose -H:+PrintClassInitialization graal_distribution: graalvm-community - graal_java_version: 17 + graal_java_version: 21 jobs: build: @@ -101,7 +101,6 @@ jobs: with: distribution: ${{ env.graal_distribution }} java-version: ${{ env.graal_java_version }} - components: 'native-image' native-image-musl: true github-token: ${{ secrets.GITHUB_TOKEN }} @@ -151,7 +150,6 @@ jobs: with: distribution: ${{ env.graal_distribution }} java-version: ${{ env.graal_java_version }} - components: 'native-image' github-token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/download-artifact@v4 @@ -165,10 +163,11 @@ jobs: - name: Create native fcli run: native-image ${{ env.native_image_opts }} -march=compatibility -H:ExcludeResources="org/fusesource/jansi/internal/native/Windows/.*" -H:ExcludeResources="org/fusesource/jansi/internal/native/Linux/.*" -H:ExcludeResources="org/fusesource/jansi/internal/native/FreeBSD/.*" -jar ./artifacts/release-assets/fcli.jar fcli - - name: Compress native fcli - uses: svenstaro/upx-action@v2 - with: - files: fcli + # Disabled for now, as compressed binaries crash on macOS Ventura or above + #- name: Compress native fcli + # uses: svenstaro/upx-action@v2 + # with: + # files: fcli - name: Basic test of native fcli run: ./fcli --help && ./fcli get --help @@ -190,7 +189,6 @@ jobs: with: distribution: ${{ env.graal_distribution }} java-version: ${{ env.graal_java_version }} - components: 'native-image' github-token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/download-artifact@v4 @@ -236,6 +234,17 @@ jobs: with: path: ./artifacts merge-multiple: true + - run: | + cd ./artifacts/release-assets + for f in *; do + sha256sum ${f} > ${f}.sha256 + done + for f in *; do + openssl dgst -sha256 -passin env:SIGN_PASSPHRASE -sign <(echo "${SIGN_KEY}") -out ${f}.rsa_sha256 ${f} + done + env: + SIGN_PASSPHRASE: ${{ secrets.SIGN_PASSPHRASE }} + SIGN_KEY: ${{ secrets.SIGN_KEY }} - uses: actions/upload-artifact@v4 with: path: ./artifacts @@ -292,7 +301,7 @@ jobs: publishPages: name: publishPages if: needs.build.outputs.do_release - needs: [build, release, combine-artifacts] + needs: [build] runs-on: ubuntu-latest steps: - name: Check-out existing docs from gh-pages branch @@ -305,7 +314,7 @@ jobs: uses: actions/download-artifact@v4 with: path: ./artifacts - name: combined-artifacts + name: build-output - name: Update documentation from artifact run: | @@ -335,7 +344,7 @@ jobs: ls -d dev_* | sort | while read line; do echo "- '$line'"; done > _data/versions/dev.yml - name: Deploy documentation - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs diff --git a/build.gradle b/build.gradle index f5e8d93602..d61bf27898 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,9 @@ plugins { - id('com.github.jk1.dependency-license-report') version '2.5' apply false + id('com.github.jk1.dependency-license-report') version '2.6' apply false id("com.github.johnrengelman.shadow") version "8.1.1" apply false - id "org.asciidoctor.jvm.convert" version "3.3.2" apply false - id "io.freefair.lombok" version "8.1.0" apply false + id "org.asciidoctor.jvm.convert" version "4.0.2" apply false + //id "org.asciidoctor.jvm.convert" version "3.3.2" apply false + id "io.freefair.lombok" version "8.6" apply false } group = "com.fortify.cli" @@ -13,6 +14,7 @@ ext { def result = project.findProperty('version'); return !result || result=='unspecified' ? buildTime.format('0.yyyyMMdd.HHmmss') : result; } + fcliActionSchemaUrl = "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${fcliActionSchemaVersion}.json" } allprojects { diff --git a/fcli-core/fcli-app/build.gradle b/fcli-core/fcli-app/build.gradle index fa1372fe36..72a7c5a429 100644 --- a/fcli-core/fcli-app/build.gradle +++ b/fcli-core/fcli-app/build.gradle @@ -20,29 +20,6 @@ dependencies { runtimeOnly("org.fusesource.jansi:jansi") } -// Generate build properties and associated resource-config.json file -ext.buildPropertiesDir = "${buildDir}/generated-build-properties" -task generateFcliBuildProperties { - doLast { - def outputDir = "${buildPropertiesDir}/com/fortify/cli/app" - mkdir "${outputDir}" - ant.propertyfile(file: "${outputDir}/fcli-build.properties") { - entry(key: "projectName", value: "fcli") - entry(key: "projectVersion", value: project.version) - entry(key: "buildDate", value: buildTime.format('yyyy-MM-dd HH:mm:ss')) - } - def resourceConfigOutputDir = "${buildPropertiesDir}/META-INF/native-image/fcli-build-properties" - mkdir "${resourceConfigOutputDir}" - def contents = - '{"resources":[\n' + - ' {"pattern":"com/fortify/cli/app/fcli-build.properties"}\n' + - ']}\n' - file("${resourceConfigOutputDir}/resource-config.json").text = contents; - println contents - } -} -sourceSets.main.output.dir buildPropertiesDir, builtBy: generateFcliBuildProperties - // Generate reflect-config.json for picocli-related classes ext.generatedPicocliReflectConfigDir = "${buildDir}/generated-reflect-config" task generatePicocliReflectConfig(type: JavaExec) { diff --git a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDynamicInitializer.java b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDynamicInitializer.java index 786a820d88..3b9eae2000 100644 --- a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDynamicInitializer.java +++ b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIDynamicInitializer.java @@ -111,12 +111,13 @@ private CommandLine createGenericOptionsCommandLine() { @Command(name = "fcli") @RequiredArgsConstructor - public static final class FortifyCLIInitializerCommand extends AbstractRunnableCommand implements Runnable { + public static final class FortifyCLIInitializerCommand extends AbstractRunnableCommand { private final Consumer consumer; @Override - public void run() { + public Integer call() { consumer.accept(getGenericOptions()); + return 0; } } } diff --git a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIStaticInitializer.java b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIStaticInitializer.java index 1ab7330243..4351ce651e 100644 --- a/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIStaticInitializer.java +++ b/fcli-core/fcli-app/src/main/java/com/fortify/cli/app/runner/util/FortifyCLIStaticInitializer.java @@ -15,12 +15,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Locale; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import com.fortify.cli.common.action.helper.ActionSchemaHelper; import com.fortify.cli.common.http.ssl.truststore.helper.TrustStoreConfigDescriptor; import com.fortify.cli.common.http.ssl.truststore.helper.TrustStoreConfigHelper; import com.fortify.cli.common.i18n.helper.LanguageHelper; @@ -56,28 +58,33 @@ public void initialize() { initializeSCDastProperties(); initializeSCSastProperties(); initializeSSCProperties(); + initializeActionProperties(); } private void initializeFoDProperties() { - System.setProperty("fcli.fod.scan.states", getValuesString(FoDScanStatus.values())); - System.setProperty("fcli.fod.scan.states.complete", getValuesString(FoDScanStatus.getDefaultCompleteStates())); + System.setProperty("fcli.fod.scan.states", getValueNamesString(FoDScanStatus.values())); + System.setProperty("fcli.fod.scan.states.complete", getValueNamesString(FoDScanStatus.getDefaultCompleteStates())); } private void initializeSCDastProperties() { - System.setProperty("fcli.sc-dast.scan.states", getValuesString(SCDastScanStatus.values())); - System.setProperty("fcli.sc-dast.scan.states.complete", getValuesString(SCDastScanStatus.getDefaultCompleteStates())); + System.setProperty("fcli.sc-dast.scan.states", getValueNamesString(SCDastScanStatus.values())); + System.setProperty("fcli.sc-dast.scan.states.complete", getValueNamesString(SCDastScanStatus.getDefaultCompleteStates())); } private void initializeSCSastProperties() { - System.setProperty("fcli.sc-sast.scan.jobStates", getValuesString(SCSastControllerScanJobState.values())); - System.setProperty("fcli.sc-sast.scan.jobStates.complete", getValuesString(SCSastControllerScanJobState.getDefaultCompleteStates())); - System.setProperty("fcli.sc-sast.scan.jobArtifactStates", getValuesString(SCSastControllerScanJobArtifactState.values())); - System.setProperty("fcli.sc-sast.scan.jobArtifactStates.complete", getValuesString(SCSastControllerScanJobArtifactState.getDefaultCompleteStates())); + System.setProperty("fcli.sc-sast.scan.jobStates", getValueNamesString(SCSastControllerScanJobState.values())); + System.setProperty("fcli.sc-sast.scan.jobStates.complete", getValueNamesString(SCSastControllerScanJobState.getDefaultCompleteStates())); + System.setProperty("fcli.sc-sast.scan.jobArtifactStates", getValueNamesString(SCSastControllerScanJobArtifactState.values())); + System.setProperty("fcli.sc-sast.scan.jobArtifactStates.complete", getValueNamesString(SCSastControllerScanJobArtifactState.getDefaultCompleteStates())); } private void initializeSSCProperties() { - System.setProperty("fcli.ssc.artifact.states", getValuesString(SSCArtifactStatus.values())); - System.setProperty("fcli.ssc.artifact.states.complete", getValuesString(SSCArtifactStatus.getDefaultCompleteStates())); + System.setProperty("fcli.ssc.artifact.states", getValueNamesString(SSCArtifactStatus.values())); + System.setProperty("fcli.ssc.artifact.states.complete", getValueNamesString(SSCArtifactStatus.getDefaultCompleteStates())); + } + + private void initializeActionProperties() { + System.setProperty("fcli.action.supportedSchemaVersions", ActionSchemaHelper.getSupportedSchemaVersionsString()); } private void initializeTrustStore() { @@ -103,8 +110,12 @@ private void initializeTrustStore() { private void initializeLocale() { Locale.setDefault(LanguageHelper.getConfiguredLanguageDescriptor().getLocale()); } - - private String getValuesString(Enum[] values) { - return Stream.of(values).map(Enum::name).collect(Collectors.joining(", ")); + + private String getValueNamesString(Enum[] values) { + return getValuesString(values, Enum::name); + } + + private String getValuesString(Enum[] values, Function, String> f) { + return Stream.of(values).map(f).collect(Collectors.joining(", ")); } } diff --git a/fcli-core/fcli-common/build.gradle b/fcli-core/fcli-common/build.gradle index 4f8368616f..f4a7a793e2 100644 --- a/fcli-core/fcli-common/build.gradle +++ b/fcli-core/fcli-common/build.gradle @@ -1 +1,36 @@ -apply from: "${sharedGradleScriptsDir}/fcli-java.gradle" \ No newline at end of file +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") { + // TODO We should also sign file; how do we invoke a sign operation from Gradle? + filter(line->project.version.startsWith('0.') + ? line + : line.replaceAll("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json", "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${fcliActionSchemaVersion}.json")) + } +} + +apply from: "${sharedGradleScriptsDir}/fcli-java.gradle" + +// Generate build properties and associated resource-config.json file +ext.buildPropertiesDir = "${buildDir}/generated-build-properties" +task generateFcliBuildProperties { + doLast { + def outputDir = "${buildPropertiesDir}/com/fortify/cli/common" + mkdir "${outputDir}" + ant.propertyfile(file: "${outputDir}/fcli-build.properties") { + entry(key: "projectName", value: "fcli") + entry(key: "projectVersion", value: project.version) + entry(key: "buildDate", value: buildTime.format('yyyy-MM-dd HH:mm:ss')) + entry(key: "actionSchemaVersion", value: fcliActionSchemaVersion) + } + def resourceConfigOutputDir = "${buildPropertiesDir}/META-INF/native-image/fcli-build-properties" + mkdir "${resourceConfigOutputDir}" + def contents = + '{"resources":[\n' + + ' {"pattern":"com/fortify/cli/common/fcli-build.properties"}\n' + + ']}\n' + file("${resourceConfigOutputDir}/resource-config.json").text = contents; + println contents + } +} +sourceSets.main.output.dir buildPropertiesDir, builtBy: generateFcliBuildProperties \ 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..7f8322538c --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionGetCommand.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * 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.cli.mixin.ActionResolverMixin; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; +import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; + +import picocli.CommandLine.Mixin; + +public abstract class AbstractActionGetCommand extends AbstractRunnableCommand { + @Mixin private ActionResolverMixin.RequiredParameter actionResolver; + + @Override + public final Integer call() { + initMixins(); + System.out.println(actionResolver.loadActionContents(getType(), ActionValidationHandler.WARN)); + return 0; + } + + 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..4f10a26f60 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionHelpCommand.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * 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.stream.StreamSupport; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.action.cli.mixin.ActionResolverMixin; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; +import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.action.runner.ActionParameterHelper; +import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureMetadata; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureStatus; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.util.StringUtils; + +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Unmatched; + +public abstract class AbstractActionHelpCommand extends AbstractRunnableCommand { + @Mixin private ActionResolverMixin.RequiredParameter actionResolver; + @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 Integer call() { + initMixins(); + var action = actionResolver.loadAction(getType(), ActionValidationHandler.WARN); + System.out.println(getActionHelp(action)); + return 0; + } + + private final String getActionHelp(Action action) { + var metadata = action.getMetadata(); + var usage = action.getUsage(); + return String.format( + "\nAction: %s\n"+ + "\n%s\n"+ + "\n%s\n"+ + "Metadata:\n"+ + "%s"+ + "\nAction options:\n"+ + "%s", + metadata.getName(), usage.getHeader(), usage.getDescription(), getMetadata(action), ActionParameterHelper.getSupportedOptionsTable(action)); + } + + private final String getMetadata(Action action) { + var metadata = action.getMetadata(); + var signatureDescriptor = metadata.getSignatureDescriptor(); + var signatureMetadata = signatureDescriptor==null ? null : signatureDescriptor.getMetadata(); + if ( signatureMetadata==null ) { signatureMetadata = SignatureMetadata.builder().build(); } + var extraSignatureInfo = signatureMetadata.getExtraInfo(); + var publicKeyDescriptor = metadata.getPublicKeyDescriptor(); + if ( publicKeyDescriptor==null ) { publicKeyDescriptor = PublicKeyDescriptor.builder().build(); } + var signatureStatus = metadata.getSignatureStatus(); + var data = JsonHelper.getObjectMapper().createObjectNode(); + data.put("Origin", metadata.isCustom()?"CUSTOM":"FCLI"); + data.put("Signature status", signatureStatus.toString()); + data.put("Author", StringUtils.ifBlank(action.getAuthor(), "N/A")); + if ( signatureStatus!=SignatureStatus.UNSIGNED ) { + data.put("Signed by", StringUtils.ifBlank(signatureMetadata.getSigner(), "N/A")); + } + switch (signatureStatus) { + case NO_PUBLIC_KEY: + data.put("Required public key", StringUtils.ifBlank(signatureDescriptor.getPublicKeyFingerprint(), "N/A")); + break; + case VALID: + data.put("Certified by", StringUtils.ifBlank(publicKeyDescriptor.getName(), + StringUtils.ifBlank(publicKeyDescriptor.getFingerprint(), "N/A"))); + break; + default: break; + } + if ( extraSignatureInfo!=null && extraSignatureInfo.size()>0 ) { + data.set("Extra signature info", extraSignatureInfo); + } + return toString(data, " "); + } + + private static final String toString(ObjectNode data, String indent) { + var sb = new StringBuffer(); + Iterable iterable = () -> data.fieldNames(); + var nameLength = StreamSupport.stream(iterable.spliterator(), false) + .mapToInt(String::length) + .max().getAsInt(); + var fmt = indent+"%-"+(nameLength+1)+"s %s\n"; + data.fields().forEachRemaining(e->sb.append(String.format(fmt, e.getKey()+":", toValue(e.getValue(), indent)))); + return sb.toString(); + } + + private static final String toValue(JsonNode value, String originalIndent) { + if ( value instanceof ObjectNode ) { + return "\n"+toString((ObjectNode)value, originalIndent+" "); + } else { + return value.asText(); + } + } + + 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..fc445d3f9b --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionImportCommand.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * 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.cli.mixin.ActionResolverMixin; +import com.fortify.cli.common.action.cli.mixin.ActionValidationMixin; +import com.fortify.cli.common.action.helper.ActionImportHelper; +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; + +import picocli.CommandLine.Mixin; + +public abstract class AbstractActionImportCommand extends AbstractOutputCommand implements IJsonNodeSupplier, IActionCommandResultSupplier { + @Mixin private ActionResolverMixin.OptionalParameter actionResolver; + @Mixin private ActionValidationMixin actionValidationMixin; + + @Override + public final JsonNode getJsonNode() { + var source = actionResolver.getActionSourceResolver().getSource(); + var action = actionResolver.getAction(); + var actionValidationHandler = actionValidationMixin.getActionValidationHandler(); + if ( action!=null) { + return ActionImportHelper.importAction(getType(), source, action, actionValidationHandler); + } else { + var zip = actionResolver.getActionSourceResolver().getSource(); + if ( zip!=null ) { + return ActionImportHelper.importZip(getType(), zip, actionValidationHandler); + } else { + throw new IllegalArgumentException("Either action and/or --from-zip option must be specified"); + } + } + } + @Override + public String getActionCommandResult() { + return "IMPORTED"; + } + @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..113ea525ea --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionListCommand.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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.cli.mixin.ActionSourceResolverMixin; +import com.fortify.cli.common.action.helper.ActionLoaderHelper; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; +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; + +import picocli.CommandLine.Mixin; + +public abstract class AbstractActionListCommand extends AbstractOutputCommand implements IJsonNodeSupplier { + @Mixin private ActionSourceResolverMixin.OptionalOption actionSourceResolver; + + @Override + public final JsonNode getJsonNode() { + return ActionLoaderHelper + .streamAsJson(actionSourceResolver.getActionSources(getType()), ActionValidationHandler.IGNORE) + .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..c36233fb86 --- /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.ActionImportHelper; +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 ActionImportHelper.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..c7fcc206a0 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionRunCommand.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * 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 java.util.concurrent.Callable; + +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +import com.fortify.cli.common.action.cli.mixin.ActionResolverMixin; +import com.fortify.cli.common.action.cli.mixin.ActionValidationMixin; +import com.fortify.cli.common.action.runner.ActionParameterHelper; +import com.fortify.cli.common.action.runner.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.progress.helper.ProgressWriterType; +import com.fortify.cli.common.util.DisableTest; +import com.fortify.cli.common.util.DisableTest.TestType; + +import lombok.SneakyThrows; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Unmatched; + +public abstract class AbstractActionRunCommand extends AbstractRunnableCommand { + @Mixin private ActionResolverMixin.RequiredParameter actionResolver; + @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 private CommandHelperMixin commandHelper; + @Mixin private ActionValidationMixin actionValidationMixin; + @Unmatched private String[] actionArgs; + + @Override @SneakyThrows + public final Integer call() { + initMixins(); + var action = actionResolver.loadAction(getType(), actionValidationMixin.getActionValidationHandler()); + Callable delayedConsoleWriter = null; + try ( var progressWriter = progressWriterFactory.overrideAutoIfNoConsole(ProgressWriterType.none) ) { + try ( var actionRunner = ActionRunner.builder() + .onValidationErrors(this::onValidationErrors) + .action(action) + .progressWriter(progressWriter) + .rootCommandLine(commandHelper.getRootCommandLine()) + .build() ) + { + delayedConsoleWriter = run(actionRunner, progressWriter); + } + } + return delayedConsoleWriter.call(); + } + + private Callable run(ActionRunner actionRunner, IProgressWriterI18n progressWriter) { + actionRunner.getSpelEvaluator().configure(context->configure(actionRunner, context)); + progressWriter.writeProgress("Executing action %s", actionRunner.getAction().getMetadata().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/cli/cmd/AbstractActionSignCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionSignCommand.java new file mode 100644 index 0000000000..6278c68564 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionSignCommand.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * 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.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.cli.mixin.CommonOptionMixins; +import com.fortify.cli.common.crypto.helper.SignatureHelper; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureMetadata; +import com.fortify.cli.common.crypto.helper.impl.TextSigner; +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; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; +import com.fortify.cli.common.util.FcliBuildPropertiesHelper; + +import lombok.Getter; +import lombok.SneakyThrows; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +public class AbstractActionSignCommand extends AbstractOutputCommand implements IJsonNodeSupplier, IActionCommandResultSupplier { + private static final ObjectMapper objectMapper = JsonHelper.getObjectMapper(); + private static final Logger LOG = LoggerFactory.getLogger(AbstractActionSignCommand.class); + @Getter @Mixin OutputHelperMixins.TableNoQuery outputHelper; + @Option(names = "--in", required=true, descriptionKey="fcli.action.sign.in") + private Path actionFileToSign; + @Option(names = "--out", required=true, descriptionKey="fcli.action.sign.out") + private Path signedActionFile; + @Option(names = "--info", required=false, descriptionKey="fcli.action.sign.info") + private Path extraInfoPath; + @Option(names = "--signer", required=false, descriptionKey="fcli.action.sign.signer") + private String signer; + @Option(names="--with", required=true, descriptionKey="fcli.action.sign.with") + private Path privateKeyPath; + @Option(names="--pubout", required=false, descriptionKey="fcli.action.sign.pubout") + private Path publicKeyPath; + @Option(names = {"--password", "-p"}, interactive = true, echo = false, arity = "0..1", required = false, descriptionKey="fcli.action.sign.password") + private char[] privateKeyPassword; + @Mixin private CommonOptionMixins.RequireConfirmation confirm; + + @Override @SneakyThrows + public JsonNode getJsonNode() { + var keyPairCreated = createKeyPair(); + deleteExistingOutputFile(); + var metadata = createMetadata(); + var signer = SignatureHelper.textSigner(privateKeyPath, privateKeyPassword); + signer.signAndWrite(actionFileToSign, signedActionFile, metadata); + writePublicKey(signer, !keyPairCreated); + + return JsonHelper.getObjectMapper().createObjectNode() + .put("in", actionFileToSign.toString()) + .put("out", signedActionFile.toString()) + .put("publicKeyFingerprint", signer.publicKeyFingerprint()) + .set("metadata", JsonHelper.getObjectMapper().valueToTree(metadata)); + } + + private void writePublicKey(TextSigner signer, boolean doWritePublicKey) { + if ( doWritePublicKey && publicKeyPath!=null ) { + if ( Files.exists(publicKeyPath) ) { + LOG.warn("WARN: Not writing public key as file already exists"); + } else { + signer.writePublicKey(publicKeyPath); + } + } + } + + private final SignatureMetadata createMetadata() { + var extraInfo = createExtraInfo(); + var signer = getSigner(extraInfo); + return SignatureMetadata.builder() + .extraInfo(extraInfo) + .fcliVersion(FcliBuildPropertiesHelper.getFcliVersion()) + .signer(signer) + .build(); + } + + private final String getSigner(ObjectNode extraInfo) { + var signerNode = extraInfo.remove("signer"); + return StringUtils.isNotBlank(this.signer) + ? this.signer + : signerNode!=null + ? signerNode.asText() + : System.getProperty("user.name"); + } + + private final ObjectNode createExtraInfo() { + ObjectNode extraInfo = objectMapper.createObjectNode(); + if ( extraInfoPath!=null ) { + try { + extraInfo.setAll((ObjectNode)JsonHelper.getObjectMapper().valueToTree(Files.readString(extraInfoPath))); + } catch ( Exception e ) { + LOG.warn("WARN: Error parsing extra info file contents"); + } + } + return extraInfo; + } + + private final void deleteExistingOutputFile() throws IOException { + if ( Files.exists(signedActionFile) ) { + confirm.checkConfirmed(signedActionFile.toString()); + Files.deleteIfExists(signedActionFile); + } + } + + private final boolean createKeyPair() { + if ( Files.exists(privateKeyPath) ) { + return false; + } else { + if ( publicKeyPath==null ) { + throw new IllegalStateException("Private key file "+privateKeyPath+" doesn't exist, and not generating new key file as --pubOut hasn't been specified"); + } else { + SignatureHelper.keyPairGenerator(privateKeyPassword).writePem(privateKeyPath, publicKeyPath); + return true; + } + } + } + + @Override + public final String getActionCommandResult() { + return "SIGNED"; + } + + @Override + public final boolean isSingular() { + return false; + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/mixin/ActionResolverMixin.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/mixin/ActionResolverMixin.java new file mode 100644 index 0000000000..cd7ca9ea57 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/mixin/ActionResolverMixin.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * 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.mixin; + +import com.fortify.cli.common.action.helper.ActionLoaderHelper; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionLoadResult; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; +import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.cli.mixin.CommonOptionMixins.AbstractTextResolverMixin; + +import lombok.Getter; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +public class ActionResolverMixin { + public static abstract class AbstractActionResolverMixin { + @Getter @Mixin private ActionSourceResolverMixin.OptionalOption actionSourceResolver; + @Mixin private PublicKeyResolverMixin publicKeyResolver; + public abstract String getAction(); + + public ActionLoadResult load(String type, ActionValidationHandler actionValidationHandler) { + var action = getAction(); + return action==null + ? null + : ActionLoaderHelper.load(actionSourceResolver.getActionSources(type), + action, actionValidationHandler.toBuilder().extraPublicKey(publicKeyResolver.getText()).build()); + } + + public Action loadAction(String type, ActionValidationHandler actionValidationHandler) { + return load(type, actionValidationHandler).getAction(); + } + + public String loadActionContents(String type, ActionValidationHandler actionValidationHandler) { + return load(type, actionValidationHandler).getActionText(); + } + } + + public static class RequiredParameter extends AbstractActionResolverMixin { + @Getter @Parameters(arity="1", descriptionKey="fcli.action.nameOrLocation") private String action; + } + + public static class OptionalParameter extends AbstractActionResolverMixin { + @Getter @Parameters(arity="0..1", descriptionKey="fcli.action.nameOrLocation") private String action; + } + + private static class PublicKeyResolverMixin extends AbstractTextResolverMixin { + @Getter @Option(names={"--pubkey"}, required = false, descriptionKey = "fcli.action.resolver.pubkey", paramLabel = "source") private String textSource; + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/mixin/ActionSourceResolverMixin.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/mixin/ActionSourceResolverMixin.java new file mode 100644 index 0000000000..f82f501a1b --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/mixin/ActionSourceResolverMixin.java @@ -0,0 +1,39 @@ +/** + * 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.cli.mixin; + +import java.util.List; + +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionSource; +import com.fortify.cli.common.util.StringUtils; + +import lombok.Getter; +import picocli.CommandLine.Option; + +public class ActionSourceResolverMixin { + public static abstract class AbstractActionSourceResolverMixin { + public abstract String getSource(); + + public List getActionSources(String type) { + var source = getSource(); + return StringUtils.isBlank(source) + ? ActionSource.defaultActionSources(type) + : ActionSource.externalActionSources(source); + } + } + + public static class OptionalOption extends AbstractActionSourceResolverMixin { + @Option(names={"--from-zip", "-z"}, required = false, descriptionKey = "fcli.action.resolver.from-zip") + @Getter private String source; + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/mixin/ActionValidationMixin.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/mixin/ActionValidationMixin.java new file mode 100644 index 0000000000..82bcf77e2c --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/mixin/ActionValidationMixin.java @@ -0,0 +1,42 @@ +/** + * 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.cli.mixin; + +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler.ActionInvalidSchemaVersionHandler; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler.ActionInvalidSignatureHandler; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureStatus; + +import lombok.Getter; +import picocli.CommandLine.Option; + +public class ActionValidationMixin { + @Option(names={"--on-invalid-signature"}, defaultValue = "prompt", descriptionKey="fcli.action.on-invalid-signature") + @Getter private ActionInvalidSignatureHandler onInvalidSignature; + @Option(names={"--on-unsigned"}, defaultValue = "prompt", descriptionKey="fcli.action.on-unsigned") + @Getter private ActionInvalidSignatureHandler onUnsigned; + @Option(names={"--on-no-public-key"}, defaultValue = "prompt", descriptionKey="fcli.action.on-no-public-key") + @Getter private ActionInvalidSignatureHandler onNoPublicKey; + @Option(names={"--on-invalid-version"}, defaultValue = "prompt", descriptionKey="fcli.action.on-invalid-version") + @Getter private ActionInvalidSchemaVersionHandler onUnsupportedVersion; + + public ActionValidationHandler getActionValidationHandler() { + return ActionValidationHandler.builder() + .onSignatureStatus(SignatureStatus.MISMATCH, onInvalidSignature) + .onSignatureStatus(SignatureStatus.NO_PUBLIC_KEY, onNoPublicKey) + .onSignatureStatus(SignatureStatus.UNSIGNED, onUnsigned) + .onUnsupportedSchemaVersion(onUnsupportedVersion) + .build(); + } +} + diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionImportHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionImportHelper.java new file mode 100644 index 0000000000..5443376d49 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionImportHelper.java @@ -0,0 +1,138 @@ +/** + * 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.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.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +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.action.helper.ActionLoaderHelper.ActionLoadResult; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionLoader; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionSource; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; +import com.fortify.cli.common.action.model.Action.ActionMetadata; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; +import com.fortify.cli.common.util.Break; +import com.fortify.cli.common.util.ZipHelper; + +import lombok.SneakyThrows; + +public class ActionImportHelper { + @SneakyThrows + public static ArrayNode importAction(String type, String externalSource, String action, ActionValidationHandler actionValidationHandler) { + var result = JsonHelper.getObjectMapper().createArrayNode(); + try ( var fs = createOutputZipFileSystem(type) ) { + var actionLoadResult = new ActionLoader(ActionSource.externalActionSources(externalSource), actionValidationHandler) + .load(action); + result.add(importAction(fs, actionLoadResult)); + } + return result; + } + + @SneakyThrows + public static ArrayNode importZip(String type, String zip, ActionValidationHandler actionValidationHandler) { + var result = JsonHelper.getObjectMapper().createArrayNode(); + var loader = new ActionLoader(null, actionValidationHandler); + try ( var fs = createOutputZipFileSystem(type); var is = createZipFileInputStream(zip) ) { + ZipHelper.processZipEntries(is, (zis, entry)-> + importAction(fs, result, loader, zis, entry)); + } + return result; + } + + @SneakyThrows + private static final InputStream createZipFileInputStream(String zip) { + try { + return new URL(zip).openStream(); + } catch ( MalformedURLException e ) { + return Files.newInputStream(Path.of(zip)); + } + } + + @SneakyThrows + private static final FileSystem createOutputZipFileSystem(String type) { + Map env = Collections.singletonMap("create", "true"); + var zipPath = ActionLoaderHelper.customActionsZipPath(type); + return FileSystems.newFileSystem(zipPath, env); + } + + private static final Break importAction(FileSystem fs, ArrayNode result, ActionLoader loader, ZipInputStream zis, ZipEntry entry) { + var metadata = ActionMetadata.builder() + .custom(true).name(entry.getName()).build(); + try { + result.add(importAction(fs, loader.load(zis, metadata))); + } catch ( Exception e ) { + result.add(createErrorEntry(metadata)); + } + return Break.FALSE; + } + + private static JsonNode createErrorEntry(ActionMetadata metadata) { + return JsonHelper.getObjectMapper().createObjectNode() + .put("name", metadata.getName()) + .put(IActionCommandResultSupplier.actionFieldName, "SKIPPED"); + } + + @SneakyThrows + private static final ObjectNode importAction(FileSystem fs, ActionLoadResult actionLoadResult) { + var metadata = actionLoadResult.getMetadata(); + actionLoadResult.getAction(); // Validate action + var contents = actionLoadResult.getOriginalText(); + var path = fs.getPath(getTargetFileName(metadata)); + Files.write(path, contents.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + var result = actionLoadResult.getSummaryObjectNode(); + return result.put("name", cleanActionName(path.getFileName().toString())); + } + + private static final String cleanActionName(String name) { + return name.replaceAll("\\.([^.]*)$", "[.$1]"); + } + + private static final String getTargetFileName(ActionMetadata metadata) { + var path = metadata.getName(); // May be simple name, path or URL + // TODO May be can use URI instead, to handle both URLs and local files? + try { + path = new URL(path).getPath(); + } catch ( MalformedURLException e) {} + if ( !path.endsWith(".yaml") ) { path+=".yaml"; } + return Path.of(path).getFileName().toString(); + } + + @SneakyThrows + public static final ArrayNode reset(String type) { + var zipPath = ActionLoaderHelper.customActionsZipPath(type); + if ( !Files.exists(zipPath) ) { + return JsonHelper.getObjectMapper().createArrayNode(); + } else { + var result = ActionLoaderHelper + .streamAsJson(ActionSource.importedActionSources(type), ActionValidationHandler.IGNORE) + .collect(JsonHelper.arrayNodeCollector()); + Files.delete(zipPath); + return result; + } + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionLoaderHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionLoaderHelper.java new file mode 100644 index 0000000000..a0de259daf --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionLoaderHelper.java @@ -0,0 +1,517 @@ +/** + * 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.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.action.model.Action.ActionMetadata; +import com.fortify.cli.common.cli.mixin.CommonOptionMixins.RequireConfirmation.AbortedByUserException; +import com.fortify.cli.common.crypto.helper.SignatureHelper; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeySource; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureMetadata; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureStatus; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureValidator; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignedTextDescriptor; +import com.fortify.cli.common.crypto.helper.impl.SignedTextReader; +import com.fortify.cli.common.util.Break; +import com.fortify.cli.common.util.FcliBuildPropertiesHelper; +import com.fortify.cli.common.util.FcliDataHelper; +import com.fortify.cli.common.util.FileUtils; +import com.fortify.cli.common.util.ZipHelper; +import com.fortify.cli.common.util.ZipHelper.IZipEntryWithContextProcessor; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.Singular; +import lombok.SneakyThrows; + +public class ActionLoaderHelper { + private static final Logger LOG = LoggerFactory.getLogger(ActionLoaderHelper.class); + private ActionLoaderHelper() {} + + public static final ActionLoadResult load(List sources, String name, ActionValidationHandler actionValidationHandler) { + return new ActionLoader(sources, actionValidationHandler).load(name); + } + + public static final Stream streamAsJson(List sources, ActionValidationHandler actionValidationHandler) { + return _streamAsJson(sources, actionValidationHandler); + } + + private static final Stream _streamAsJson(List sources, ActionValidationHandler actionValidationHandler) { + Map result = new HashMap<>(); + new ActionLoader(sources, actionValidationHandler) + .processActions(loadResult->{ + result.putIfAbsent(loadResult.getMetadata().getName(), loadResult.getSummaryObjectNode()); + return Break.FALSE; + }); + return result.values().stream() + .sorted((a,b)->a.get("name").asText().compareTo(b.get("name").asText())); + } + + public static final String getSignatureStatusMessage(ActionMetadata metadata, SignatureStatus signatureStatus) { + var name = metadata.getName(); + switch (signatureStatus) { + case MISMATCH: return "Signature for action "+name+" is invalid."; + case NO_PUBLIC_KEY: return "No trusted public key found to verify "+name+" action signature."; + case UNSIGNED: return "Action "+name+" is not signed."; + case NOT_VERIFIED: return "Signature verification skipped for action "+name+"."; + case VALID: return "Action "+name+" has a valid signature."; + default: throw new RuntimeException("Unknown signature status: "+signatureStatus); + } + } + + @RequiredArgsConstructor + static final class ActionLoader { + private static final SignedTextReader signedTextReader = SignatureHelper.signedTextReader(); + private final List sources; + private final ActionValidationHandler actionValidationHandler; + + public final ActionLoadResult load(String source) { + // We first load from zips, in case a file happens to exist with + // same name as an existing action, to avoid errors if for example + // a user saved a SARIF SAST report named 'sarif-sast-report' (with + // no extension) in the current working directory. + // TODO We may want to consider making this a bit smarter. For example, + // we may require action names to only use [a-zA-Z0-9-_]+ (also + // for imported actions), and only try to load from zips if source + // matches this regex. + var result = loadFromZips(source); + if ( result==null ) { result = loadFromFileOrUrl(source); } + if ( result==null ) { throw new IllegalArgumentException("Action not found: "+source); } + return result; + } + + public final void processActions(ActionLoadResultProcessor actionLoadResultProcessor) { + processZipEntries(zipEntryProcessor(actionLoadResultProcessor)); + } + + private final ActionLoadResult loadFromFileOrUrl(String source) { + try ( var is = createSourceInputStream(source, false) ) { + if ( is!=null ) { + var metadata = ActionMetadata.builder() + .custom(true).name(source).build(); + return load(is, metadata); + } + } catch (Exception e) { + if ( e instanceof AbortedByUserException ) { throw (AbortedByUserException)e; } + throw wrapException("Error loading action from "+source, e); + } + return null; + } + + private final ActionLoadResult loadFromZips(String name) { + try { + AtomicReference result = new AtomicReference<>(); + processZipEntries(singleZipEntryProcessor(name, result::set)); + return result.get(); + } catch ( RuntimeException e ) { + throw wrapException("Error loading action "+name, e); + } + } + + private final void processZipEntries(IZipEntryWithContextProcessor processor) { + for ( var source: sources ) { + var _break = ZipHelper.processZipEntries(source.getInputStreamSupplier(), + processor, source.getMetadata()); + if ( _break.doBreak() ) { break; } + } + } + + private final IZipEntryWithContextProcessor zipEntryProcessor(ActionLoadResultProcessor loadResultProcessor) { + return (zis, ze, metadata) -> loadResultProcessor.process(load(zis, ze, metadata)); + } + + private final IZipEntryWithContextProcessor singleZipEntryProcessor(String name, Consumer loadResultConsumer) { + return (zis, ze, metadata) -> processSingleZipEntry(zis, ze, name, loadResultConsumer, metadata); + } + + private Break processSingleZipEntry(ZipInputStream zis, ZipEntry ze, String name, Consumer loadResultConsumer, ActionMetadata metadata) { + var fileName = name+".yaml"; + if (ze.getName().equals(fileName)) { + loadResultConsumer.accept(load(zis, ze, metadata)); + return Break.TRUE; + } + return Break.FALSE; + } + + private final ActionLoadResult load(ZipInputStream zis, ZipEntry ze, ActionMetadata metadata) { + metadata = metadata.toBuilder().name(getActionName(ze.getName())).build(); + return load(zis, metadata); + } + + final ActionLoadResult load(InputStream is, ActionMetadata metadata) { + return new ActionLoadResult(actionValidationHandler, loadSignedTextDescriptor(metadata, is), metadata); + } + + private final SignedTextDescriptor loadSignedTextDescriptor(ActionMetadata metadata, InputStream is) { + return signedTextReader.load(is, StandardCharsets.UTF_8, + // TODO For now, we only evaluate/check signatures for custom actions, + // until we've figured out how to sign internal actions during (or + // potentially after) Gradle build. + metadata.isCustom() + ? actionValidationHandler.getSignatureValidator(metadata) + : null); + } + + private final String getActionName(String fileName) { + return Path.of(fileName).getFileName().toString().replace(".yaml", ""); + } + } + + @Data + public static final class ActionLoadResult { + private static final ObjectMapper yamlObjectMapper = new ObjectMapper(new YAMLFactory()); + private static final Pattern schemaPattern = Pattern.compile("(?m)(^\\$schema:\\s+(?\\S+)\\s*$)|(^#\\s+yaml-language-server:\\s+\\$schema=(?\\S+)\\s*$)"); + private final ActionValidationHandler actionValidationHandler; + private final SignedTextDescriptor signedTextDescriptor; + private final ActionMetadata metadata; + + ActionLoadResult(ActionValidationHandler actionValidationHandler, SignedTextDescriptor signedTextDescriptor, ActionMetadata metadata) { + this.actionValidationHandler = actionValidationHandler; + this.signedTextDescriptor = signedTextDescriptor; + this.metadata = updateMetadata(metadata, signedTextDescriptor); + } + + /** + * @return Deserialized and initialized {@link Action} instance. + */ + public final Action getAction() { + try { + checkSchema(); + var result = yamlObjectMapper.readValue(getActionText(), Action.class); + result.postLoad(metadata); + return result; + } catch ( Exception e ) { + throw createException(e); + } + } + + /** + * @return Textual action contents. + */ + public final String getActionText() { + return signedTextDescriptor.getPayload(); + } + + /** + * @return Original action file contents, including + * signature if available. + */ + public final String getOriginalText() { + return signedTextDescriptor.getOriginal(); + } + + public final ActionSummaryDescriptor getSummaryDescriptor() { + return ActionSummaryDescriptor.fromActionLoadResult(this); + } + + public final ObjectNode getSummaryObjectNode() { + return getSummaryDescriptor().asObjectNode(); + } + + private static final ActionMetadata updateMetadata(ActionMetadata metadata, SignedTextDescriptor signedTextDescriptor) { + var custom = metadata.isCustom(); + return metadata.toBuilder() + .signatureDescriptor(getSignatureDescriptor(custom, signedTextDescriptor)) + .signatureStatus(getSignatureStatus(custom, signedTextDescriptor)) + .publicKeyDescriptor(getPublicKeyDescriptor(custom, signedTextDescriptor)) + .build(); + } + + private static SignatureDescriptor getSignatureDescriptor(boolean custom, SignedTextDescriptor signedTextDescriptor) { + return custom + ? signedTextDescriptor.getSignatureDescriptor() + : SignatureDescriptor.builder() + .signature("N/A") + .publicKeyFingerprint(SignatureHelper.fortifySignatureVerifier().publicKeyFingerPrint()) + .metadata(SignatureMetadata.builder() + .fcliVersion(FcliBuildPropertiesHelper.getFcliVersion()) + .signer("Fortify").build()).build(); + } + + private static SignatureStatus getSignatureStatus(boolean custom, SignedTextDescriptor signedTextDescriptor) { + return custom + ? signedTextDescriptor.getSignatureStatus() + : SignatureStatus.VALID; + } + + private static PublicKeyDescriptor getPublicKeyDescriptor(boolean custom, SignedTextDescriptor signedTextDescriptor) { + return custom + ? signedTextDescriptor.getPublicKeyDescriptor() + : PublicKeyDescriptor.builder() + .fingerprint(SignatureHelper.fortifySignatureVerifier().publicKeyFingerPrint()) + .name("Fortify") + .publicKey(SignatureHelper.FORTIFY_PUBLIC_KEY) + .source(PublicKeySource.INTERNAL) + .build(); + } + + private final void checkSchema() { + var schemaUri = getSchemaUri(); + var schemaVersion = ActionSchemaHelper.getSchemaVersion(schemaUri); + if ( !ActionSchemaHelper.isSupportedSchemaVersion(schemaVersion) ) { + actionValidationHandler.onUnsupportedSchemaVersion(metadata, schemaVersion); + } + } + + final String getSchemaUri() { + var matcher = schemaPattern.matcher(getActionText()); + String propertyValue = null; + String commentValue = null; + while ( matcher.find() ) { + propertyValue = getValue("$schema", matcher.group("schemaPropertyValue"), propertyValue); + commentValue = getValue("# yaml-language-server $schema", matcher.group("schemaCommentValue"), commentValue); + } + if ( StringUtils.isAllBlank(propertyValue, commentValue) ) { + throw new IllegalStateException(getExceptionMessage("Either '$schema' property or '# yaml-language-server $schema' must be specified")); + } else if ( StringUtils.isNoneBlank(propertyValue, commentValue) && !propertyValue.equals(commentValue) ) { + throw new IllegalStateException(getExceptionMessage("If both '$schema' property and '# yaml-language-server $schema' are specified, the schema locations must be identical")); + } else if ( StringUtils.isBlank(propertyValue) ) { + return commentValue; + } else { + return propertyValue; + } + } + + private final String getValue(String type, String newValue, String oldValue) { + if ( StringUtils.isBlank(oldValue) ) { + return newValue; + } else if ( StringUtils.isNotBlank(newValue) ) { + throw new IllegalStateException(getExceptionMessage(type+" may only be specified once")); + } else { + return oldValue; + } + } + + private final RuntimeException createException(Exception e) { + return wrapException(getExceptionMessage(null), e); + } + + private String getExceptionMessage(String detailMessage) { + var msg = "Error loading action "+metadata.getName(); + if ( StringUtils.isNotBlank(detailMessage) ) { msg+=": "+detailMessage; } + return msg; + } + } + + @Data @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static final class ActionSource { + private final Supplier inputStreamSupplier; + private final ActionMetadata metadata; + + public static final List defaultActionSources(String type) { + var result = new ArrayList(); + result.add(imported(type)); + result.add(builtin(type)); + result.add(common(type)); + return result; + } + + public static final List importedActionSources(String type) { + var result = new ArrayList(); + result.add(imported(type)); + return result; + } + + public static final List builtinActionSources(String type) { + var result = new ArrayList(); + result.add(builtin(type)); + result.add(common(type)); + return result; + } + + public static final List externalActionSources(String source) { + var result = new ArrayList(); + if ( StringUtils.isNotBlank(source) ) { + result.add(external(source)); + } + return result; + } + + private static final ActionSource external(String source) { + return new ActionSource(()->createSourceInputStream(source, true), ActionMetadata.create(true)); + } + + private static final ActionSource imported(String type) { + return new ActionSource(customActionsInputStreamSupplier(type), ActionMetadata.create(true)); + } + + private static final ActionSource builtin(String type) { + return new ActionSource(builtinActionsInputStreamSupplier(type), ActionMetadata.create(false)); + } + + private static final ActionSource common(String type) { + return new ActionSource(commonActionsInputStreamSupplier(), ActionMetadata.create(false)); + } + + @SneakyThrows + private static final Supplier customActionsInputStreamSupplier(String type) { + return ()->FileUtils.getInputStream(customActionsZipPath(type)); + } + + private static final Supplier builtinActionsInputStreamSupplier(String type) { + return ()->FileUtils.getResourceInputStream(builtinActionsResourceZip(type)); + } + + private static final Supplier commonActionsInputStreamSupplier() { + return ()->FileUtils.getResourceInputStream(commonActionsResourceZip()); + } + } + + static final Path customActionsZipPath(String type) { + return FcliDataHelper.getFcliConfigPath().resolve("action").resolve(type.toLowerCase()+".zip"); + } + + private static final String builtinActionsResourceZip(String type) { + return String.format("com/fortify/cli/%s/actions.zip", type.toLowerCase().replace('-', '_')); + } + + private static final String commonActionsResourceZip() { + return "com/fortify/cli/common/actions.zip"; + } + + @SneakyThrows + private static final InputStream createSourceInputStream(String source, boolean failOnError) { + try { + return new URL(source).openStream(); + } catch (MalformedURLException mue ) { + try { + return Files.newInputStream(Path.of(source)); + } catch ( IOException ioe ) { + if ( failOnError ) { + throw new IllegalArgumentException("Unable to read from "+source, ioe); + } else { + return null; + } + } + } + } + + @Data @Builder(toBuilder = true) + public static final class ActionValidationHandler { + public static final ActionValidationHandler PROMPT = ActionValidationHandler.builder() + .onSignatureStatusDefault(ActionInvalidSignatureHandler.prompt) + .onUnsupportedSchemaVersion(ActionInvalidSchemaVersionHandler.prompt) + .build(); + public static final ActionValidationHandler WARN = ActionValidationHandler.builder() + .onSignatureStatusDefault(ActionInvalidSignatureHandler.warn) + .onUnsupportedSchemaVersion(ActionInvalidSchemaVersionHandler.warn) + .build(); + public static final ActionValidationHandler IGNORE = ActionValidationHandler.builder() + .onSignatureStatusDefault(ActionInvalidSignatureHandler.ignore) + .onUnsupportedSchemaVersion(ActionInvalidSchemaVersionHandler.ignore) + .build(); + @Singular private final List extraPublicKeys; + @Singular private final Map> onSignatureStatuses; + @Builder.Default private final BiConsumer onSignatureStatusDefault = ActionInvalidSignatureHandler.prompt; + @Builder.Default private final BiConsumer onUnsupportedSchemaVersion = ActionInvalidSchemaVersionHandler.prompt; + + public final SignatureValidator getSignatureValidator(ActionMetadata metadata) { + return new SignatureValidator(d->handleInvalidSignature(metadata, d), extraPublicKeys.toArray(String[]::new)); + } + public final void onUnsupportedSchemaVersion(ActionMetadata metadata, String schemaVersion) { + this.onUnsupportedSchemaVersion.accept(metadata, schemaVersion); + } + private final void handleInvalidSignature(ActionMetadata metadata, SignedTextDescriptor signedTextDescriptor) { + var consumer = onSignatureStatuses.get(signedTextDescriptor.getSignatureStatus()); + if ( consumer==null ) { consumer = onSignatureStatusDefault; } + consumer.accept(metadata, signedTextDescriptor); + } + + @RequiredArgsConstructor + public static enum ActionInvalidSignatureHandler implements BiConsumer { + ignore((p,d)->{}), + warn((p,d)->_warn(signatureFailureMessage(p,d))), + fail((p,d)->_throw(signatureFailureMessage(p,d))), + prompt((p,d)->_prompt(signatureFailureMessage(p,d))); + private final BiConsumer onInvalidSignature; + + @Override + public void accept(ActionMetadata metadata, SignedTextDescriptor descriptor) { + onInvalidSignature.accept(metadata, descriptor); + } + + private static final String signatureFailureMessage(ActionMetadata metadata, SignedTextDescriptor descriptor) { + return getSignatureStatusMessage(metadata, descriptor.getSignatureStatus()); + } + } + + @RequiredArgsConstructor + public static enum ActionInvalidSchemaVersionHandler implements BiConsumer { + ignore((p,v)->{}), + warn((p,v)->_warn(unsupportedSchemaMessage(p,v))), + fail((p,v)->_throw(unsupportedSchemaMessage(p,v))), + prompt((p,v)->_prompt(unsupportedSchemaMessage(p,v))); + private final BiConsumer onInvalidSchemaVersion; + + @Override + public void accept(ActionMetadata metadata, String schemaVersion) { + onInvalidSchemaVersion.accept(metadata, schemaVersion); + } + + public static final String unsupportedSchemaMessage(ActionMetadata metadata, String unsupportedVersion) { + return String.format("Action "+metadata.getName()+" uses unsupported schema version %s and may fail.", unsupportedVersion); + } + } + + private static final void _warn(String msg) { LOG.warn("WARN: "+msg); } + private static final void _throw(String msg) { throw new IllegalStateException(msg); } + private static final void _prompt(String msg) { + if ( System.console()==null ) { + _throw(msg); + } else if (!"Y".equalsIgnoreCase(System.console().readLine(String.format("WARN: %s\n Do you want to continue? (Y/N) ", msg))) ) { + throw new AbortedByUserException("Aborting: operation aborted by user"); + } + } + } + + private static final RuntimeException wrapException(String msg, Exception e) { + if ( e!=null && e instanceof AbortedByUserException ) { return (AbortedByUserException)e; } + return new IllegalStateException(msg, e); + } + + @FunctionalInterface + private static interface ActionLoadResultProcessor { + Break process(ActionLoadResult loadResult); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSchemaHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSchemaHelper.java new file mode 100644 index 0000000000..2ce5587132 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSchemaHelper.java @@ -0,0 +1,57 @@ +/** + * 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.text.MessageFormat; +import java.text.ParseException; + +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.util.FcliBuildPropertiesHelper; +import com.fortify.cli.common.util.SemVer; + +@Reflectable public final class ActionSchemaHelper { + private static final MessageFormat URI_FORMAT = new MessageFormat("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-{0}.json"); + private static final boolean IS_FCLI_DEV_RELEASE = FcliBuildPropertiesHelper.isDevelopmentRelease(); + private static final SemVer CURRENT_SCHEMA_VERSION = new SemVer(FcliBuildPropertiesHelper.getFcliActionSchemaVersion()); + + /** Get the schema URI for the current enum entry by formatting schema version as URI */ + public static final String toURI(String version) { + return URI_FORMAT.format(new Object[] {version}); + } + + /** Check whether given schema/version is supported */ + public static final boolean isSupportedSchemaURI(String schemaURI) { + return isSupportedSchemaVersion(getSchemaVersion(schemaURI)); + } + + public static final String getSchemaVersion(String schemaURI) { + try { + return (String)URI_FORMAT.parse(schemaURI)[0]; + } catch (ParseException e) { + return "unknown"; + } + } + + /** Check whether given schema version is supported */ + public static final boolean isSupportedSchemaVersion(String version) { + return IS_FCLI_DEV_RELEASE + ? true + : CURRENT_SCHEMA_VERSION.isCompatibleWith(version); + } + + public static final String getSupportedSchemaVersionsString() { + return IS_FCLI_DEV_RELEASE + ? "any (as this is an fcli development version)" + : CURRENT_SCHEMA_VERSION.getCompatibleVersionsString().orElse("unknown"); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSummaryDescriptor.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSummaryDescriptor.java new file mode 100644 index 0000000000..50a2dad0f7 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/helper/ActionSummaryDescriptor.java @@ -0,0 +1,119 @@ +/** + * 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 org.apache.commons.lang3.StringUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionLoadResult; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.json.JsonNodeHolder; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.SneakyThrows; + +@Reflectable @Data @Builder @EqualsAndHashCode(callSuper = false) +public class ActionSummaryDescriptor extends JsonNodeHolder { + private static final ObjectMapper yamlObjectMapper = new ObjectMapper(new YAMLFactory()); + private final String name; + private final String status; + private final boolean custom; + private final String origin; + private final String author; + private final String usageHeader; + private final String usageDescription; + private final String signatureStatus; + private final String signedBy; + private final String publicKeyName; + private final String publicKeyFingerprint; + private final ObjectNode signatureExtraInfo; + + public static final ActionSummaryDescriptor fromActionLoadResult(ActionLoadResult actionLoadResult) { + var metadata = actionLoadResult.getMetadata(); + var builder = ActionSummaryDescriptor.builder(); + builder + .name(metadata.getName()) + .status(getStatus(actionLoadResult)) + .custom(metadata.isCustom()) + .origin(metadata.isCustom()?"CUSTOM":"FCLI"); + addSignatureInfo(builder, actionLoadResult); + addActionProperties(builder, actionLoadResult); + return builder.build(); + } + + private static final void addSignatureInfo(ActionSummaryDescriptorBuilder builder, ActionLoadResult actionLoadResult) { + var metadata = actionLoadResult.getMetadata(); + var signatureStatus = metadata.getSignatureStatus(); + var signatureDescriptor = metadata.getSignatureDescriptor(); + var signatureMetadata = signatureDescriptor==null?null:signatureDescriptor.getMetadata(); + var publicKeyDescriptor = metadata.getPublicKeyDescriptor(); + var signer = StringUtils.defaultIfBlank(signatureMetadata==null + ? null : signatureMetadata.getSigner(), "N/A"); + var extraInfo = signatureMetadata==null + ? null : signatureMetadata.getExtraInfo(); + var publicKeyName = StringUtils.defaultIfBlank(publicKeyDescriptor==null + ? null : publicKeyDescriptor.getName(), "N/A"); + var publicKeyFingerprint = StringUtils.defaultIfBlank(publicKeyDescriptor==null + ? null : publicKeyDescriptor.getFingerprint(), "N/A"); + + builder + .signatureStatus(signatureStatus.toString()) + .signedBy(signer) + .publicKeyName(publicKeyName) + .publicKeyFingerprint(publicKeyFingerprint) + .signatureExtraInfo(extraInfo); + } + + @SneakyThrows + private static final void addActionProperties(ActionSummaryDescriptorBuilder builder, ActionLoadResult actionLoadResult) { + var actionNode = getActionObjectNode(actionLoadResult); + if ( actionNode==null ) { + builder + .author("N/A") + .usageHeader("N/A") + .usageDescription("N/A"); + } else { + builder + .author(JsonHelper.evaluateSpelExpression( + actionNode, "author?:'N/A'", String.class)) + .usageHeader(JsonHelper.evaluateSpelExpression( + actionNode, "usage?.header?:'N/A'", String.class)) + .usageDescription(JsonHelper.evaluateSpelExpression( + actionNode, "usage?.description?:'N/A'", String.class)); + } + } + + private static final ObjectNode getActionObjectNode(ActionLoadResult actionLoadResult) { + try { + return yamlObjectMapper.readValue(actionLoadResult.getActionText(), ObjectNode.class); + } catch ( Exception e ) { + // Return null if not valid YAML document + return null; + } + } + + private static final String getStatus(ActionLoadResult actionLoadResult) { + try { + // Validate action + actionLoadResult.getAction(); + } catch ( Exception e ) { + return "INVALID"; + } + return "VALID"; + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/AbstractActionStep.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/AbstractActionStep.java new file mode 100644 index 0000000000..b466498c5e --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/AbstractActionStep.java @@ -0,0 +1,28 @@ +/** + * 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.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Reflectable @NoArgsConstructor +@Data +public abstract class AbstractActionStep implements IActionStep { + @JsonPropertyDescription("Optional SpEL template expression: Only execute this step if the given if-expression evaluates to 'true'") + @JsonProperty(value = "if", required = false) private TemplateExpression _if; +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/AbstractActionStepForEach.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/AbstractActionStepForEach.java new file mode 100644 index 0000000000..ee42bd03a3 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/AbstractActionStepForEach.java @@ -0,0 +1,53 @@ +/** + * 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.model; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This abstract class is the base class for forEach steps/properties. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public abstract class AbstractActionStepForEach extends AbstractActionStep { + @JsonPropertyDescription("Required string: Name to assign to each individual record being processed. Can be referenced in other forEach properties and nested steps using ${[name]}.") + @JsonProperty(required = true) private String name; + + @JsonPropertyDescription("Required list: Steps to be executed for each individual record.") + @JsonProperty(value = "do", required = true) private List _do; + + @JsonPropertyDescription("Optional SpEL template expression: Stop processing any further records if the breakIf expression evaluates to 'true'.") + @JsonProperty(required = false) private TemplateExpression breakIf; + + /** + * This method is invoked by the {@link ActionStep#postLoad()} + * method. It checks that required properties are set, then calls the postLoad() method for + * each sub-step. + */ + public final void postLoad(Action action) { + Action.checkNotBlank("forEach name", name, this); + Action.checkNotNull("forEach do", _do, this); + _postLoad(action); + } + + protected void _postLoad(Action action) {} +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/AbstractActionStepUpdateProperty.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/AbstractActionStepUpdateProperty.java new file mode 100644 index 0000000000..112417eb82 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/AbstractActionStepUpdateProperty.java @@ -0,0 +1,46 @@ +/** + * 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.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This abstract class describes an operation to update a data property. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public abstract class AbstractActionStepUpdateProperty extends AbstractActionStep implements IActionStepValueSupplier { + @JsonPropertyDescription("Required string: Name to assign to the outcome of this operation. Can be referenced in subsequent steps using ${[name]}.") + @JsonProperty(required = true) private String name; + + @JsonPropertyDescription("Required SpEL template expression if 'valueTemplate' is not specified: Value to be assigned or appended to the given name.") + @JsonProperty(required = false) private TemplateExpression value; + + @JsonPropertyDescription("Required string if 'value' is not specified: Name of a value template to be evaluated, assigning or appending the outcome of the value template to the given set/append name.") + @JsonProperty(required = false) private String valueTemplate; + + public final void postLoad(Action action) { + Action.checkNotBlank("set name", name, this); + Action.checkActionValueSupplier(action, this); + _postLoad(action); + } + + protected void _postLoad(Action action) {} +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/Action.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/Action.java new file mode 100644 index 0000000000..26671fa87f --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/Action.java @@ -0,0 +1,260 @@ +/** + * 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.model; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureStatus; +import com.fortify.cli.common.util.StringUtils; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import lombok.ToString; + +/** + * This class describes an action deserialized from an action YAML file, + * containing elements describing things like: + *
    + *
  • Action metadata like name and description
  • + *
  • Action parameters
  • + *
  • Steps to be executed, like executing REST requests and writing output
  • + *
  • Value templates
  • + * + *
+ * + * @author Ruud Senden + */ +@Reflectable @NoArgsConstructor +@Data +@JsonClassDescription("Fortify CLI action definition") +public class Action implements IActionElement { + @JsonPropertyDescription("Required string unless `yaml-language-server` comment with schema location is provided: Schema location.") + @JsonProperty(value = "$schema", required=false) public String schema; + + @JsonPropertyDescription("Required string: Author of this action.") + @JsonProperty(required = true) private String author; + + @JsonPropertyDescription("Required object: Action usage help.") + @JsonProperty(required = true) private ActionUsage usage; + + @JsonPropertyDescription("Optional list: Action parameters.") + @JsonProperty(required = false) private List parameters; + + @JsonPropertyDescription("Optional list: Add target URLs and related properties for REST requests.") + @JsonProperty(required = false) private List addRequestTargets; + + @JsonPropertyDescription("Optional object: Default values for some specific properties. Currently only used to set a default request target.") + @JsonProperty(required = false) private ActionDefaultValues defaults; + + @JsonPropertyDescription("Required list: Steps to be executed when this action is being run.") + @JsonProperty(required = true) private List steps; + + @JsonPropertyDescription("Optional list: Value templates used to format data.") + @JsonProperty(required = false) private List valueTemplates; + + @JsonIgnore ActionMetadata metadata; + /** Maps/Collections listing action elements. + * These get filled by the {@link #visit(Action, Object)} method. */ + @ToString.Exclude @JsonIgnore private final Map valueTemplatesByName = new HashMap<>(); + @ToString.Exclude @JsonIgnore private final List allActionElements = new ArrayList<>(); + + public Map getValueTemplatesByName() { + return Collections.unmodifiableMap(valueTemplatesByName); + } + + public List getAllActionElements() { + return Collections.unmodifiableList(allActionElements); + } + + /** + * This method is invoked by ActionHelper after deserializing an instance + * of this class from a YAML file. It performs the following tasks: + *
    + *
  • Set the given attributes on this action
  • + *
  • Initialize the collections above
  • + *
  • Validate required action elements are present
  • + *
  • Invoke the {@link IActionElement#postLoad(Action)} method + * for every {@link IActionElement} collected during collection + * initialization
  • + *
+ * We need to initialize collections before invoking {@link IActionElement#postLoad(Action)} + * methods, as these methods may utilize the collections. + * @param signatureStatus + */ + public final void postLoad(ActionMetadata metadata) { + this.metadata = metadata; + initializeCollections(); + allActionElements.forEach(elt->elt.postLoad(this)); + } + + /** + * {@link IActionElement#postLoad(Action)} implementation + * for this root action element, checking required elements + * are present. + */ + public final void postLoad(Action action) { + checkNotNull("action usage", usage, this); + checkNotNull("action steps", steps, this); + if ( parameters==null ) { + parameters = Collections.emptyList(); + } + } + + /** + * Utility method for throwing an {@link ActionValidationException} + * if the given boolean value is true. + * @param isFailure + * @param entity + * @param msgSupplier + */ + static final void throwIf(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 + */ + static final void checkNotBlank(String property, String value, Object entity) { + throwIf(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 + */ + static final void checkNotNull(String property, Object value, Object entity) { + throwIf(value==null, entity, ()->String.format("Action %s property must be specified", property)); + } + + /** + * Utility method for checking the attributes of an {@link IActionStepValueSupplier} + * instance. + */ + static final void checkActionValueSupplier(Action action, IActionStepValueSupplier supplier) { + var value = supplier.getValue(); + var valueTemplate = supplier.getValueTemplate(); + throwIf(value!=null && StringUtils.isNotBlank(valueTemplate), supplier, ()->"Either value or valueTemplate must be specified, not both"); + throwIf(value==null && StringUtils.isBlank(valueTemplate), supplier, ()->"Either value or valueTemplate must be specified"); + if ( valueTemplate!=null ) { + throwIf(!action.getValueTemplates().stream().anyMatch(d->d.getName().equals(valueTemplate)), supplier, + ()->"No value template found with name "+valueTemplate); + } + } + + /** + * Initialize the {@link #allActionElements} and {@link #valueTemplatesByName} + * collections, using the reflective visit methods. We use reflection as + * manually navigating the action element tree proved to be too error-prone, + * often forgetting to handle newly added action element types. + */ + private void initializeCollections() { + visit(this, this, elt->{ + allActionElements.add(elt); + if ( elt instanceof ActionValueTemplate ) { + var actionValueTemplate = (ActionValueTemplate)elt; + valueTemplatesByName.put(actionValueTemplate.getName(), actionValueTemplate); + } + }); + } + + /** + * Visit the given action element. If the action element implements + * the {@link IActionElement} interface (i.e., it's not a simple value + * like String or TemplateExpression), it is passed to the given consumer + * and subsequently we recurse into all action element fields. + * If the given action element is a collection, we recurse into each + * collection element. + */ + private final void visit(Action action, Object actionElement, Consumer consumer) { + if ( actionElement!=null ) { + if ( actionElement instanceof IActionElement ) { + var instance = (IActionElement)actionElement; + consumer.accept(instance); + visitFields(action, instance.getClass(), instance, consumer); + } else if ( actionElement instanceof Collection ) { + ((Collection)actionElement).stream() + .forEach(o->visit(action, o, consumer)); + } + } + } + + /** + * Visit all fields of the given class, with field values being + * retrieved from the given action element. + */ + private void visitFields(Action action, Class clazz, Object actionElement, Consumer consumer) { + if ( clazz!=null && IActionElement.class.isAssignableFrom(clazz) ) { + // Visit fields provided by any superclasses of the given class. + visitFields(action, clazz.getSuperclass(), actionElement, consumer); + // Iterate over all declared fields, and invoke the + // postLoad(action, actionElement) for each field value. + Stream.of(clazz.getDeclaredFields()) + .peek(f->f.setAccessible(true)) + .filter(f->f.getAnnotation(JsonIgnore.class)==null) + .map(f->getFieldValue(actionElement, f)) + .forEach(o->visit(action, o, consumer)); + } + } + + /** + * Get the value for the given field from the given action element. + * Only reason to have this method is to handle any exceptions + * through {@link SneakyThrows}, to allow getting field values in + * lambda expressions like above. + */ + @SneakyThrows + private Object getFieldValue(Object actionElement, Field field) { + return field.get(actionElement); + } + + @Reflectable @Data @Builder(toBuilder = true) @AllArgsConstructor + public static final class ActionMetadata { + private final String name; + private final boolean custom; + private final SignatureDescriptor signatureDescriptor; + private final PublicKeyDescriptor publicKeyDescriptor; + @Builder.Default private final SignatureStatus signatureStatus = SignatureStatus.NOT_VERIFIED; + + public static final ActionMetadata create(boolean custom) { + return builder().custom(custom).build(); + } + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionDefaultValues.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionDefaultValues.java new file mode 100644 index 0000000000..b36eda54a4 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionDefaultValues.java @@ -0,0 +1,33 @@ +/** + * 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.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * This class describes default values for various action properties. + */ +@Reflectable @NoArgsConstructor +@Data +public final class ActionDefaultValues implements IActionElement { + @JsonPropertyDescription("Optional string: Default request target to use for REST requests.") + @JsonProperty(required = false) private String requestTarget; + + @Override + public void postLoad(Action action) {} +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionParameter.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionParameter.java new file mode 100644 index 0000000000..2c92c38e69 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionParameter.java @@ -0,0 +1,65 @@ +/** + * 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.model; + +import java.util.Map; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * This class describes a action parameter. + */ +@Reflectable @NoArgsConstructor +@Data +public final class ActionParameter implements IActionElement { + @JsonPropertyDescription("Required string: Action parameter name. This will allow the action to accept CLI options named `--[name]` or `-[name]` for single-letter names. Parameter value can be referenced through ${parameters.[name]} in SpEL template expressions.") + @JsonProperty(required = true) private String name; + + @JsonPropertyDescription("Required string: Action parameter description to be shown in action usage help.") + @JsonProperty(required = true) private String description; + + @JsonPropertyDescription("Optional string: Comma-separated CLI option aliases. This will allow the action to accept CLI options named `--[alias]` or `-[alias]` for single-letter aliases. Aliases cannot be referenced in SpEL expressions.") + @JsonProperty(required = false) private String cliAliases; + + @JsonPropertyDescription("Optional string: Action parameter type. Supported types depends on the fcli module (SSC/FoD) from which the action is being run. See built-in actions for examples of supported types.") + @JsonProperty(required = false) private String type; + + @JsonPropertyDescription("Optional map(string,SpEL template expression): Action parameter type parameters to allow for additional configuration of the type converter configured through 'type'.") + @JsonProperty(required = false) private Map typeParameters; + + @JsonPropertyDescription("Optional SpEL template expression: Default value for this action parameter if no value is specified by the user.") + @JsonProperty(required = false) private TemplateExpression defaultValue; + + @JsonPropertyDescription("Optional boolean: All parameters are required by default, unless this property is set to false.") + @JsonProperty(required = false, defaultValue = "true") private boolean required = true; + + public final void postLoad(Action action) { + Action.checkNotBlank("parameter name", name, this); + Action.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); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionRequestTarget.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionRequestTarget.java new file mode 100644 index 0000000000..decd7bf03c --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionRequestTarget.java @@ -0,0 +1,47 @@ +/** + * 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.model; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * This class describes a request target. + */ +@Reflectable @NoArgsConstructor +@Data +public final class ActionRequestTarget implements IActionElement { + @JsonPropertyDescription("Required string: Request target name, referenceable from 'request' steps.") + @JsonProperty(required = true) private String name; + + @JsonPropertyDescription("Required SpEL template expression: Base URL to use for REST requests to this request target.") + @JsonProperty(required = true) private TemplateExpression baseUrl; + + @JsonPropertyDescription("Optional map(string,SpEL template expression): Headers to be sent to this request target on every request.") + @JsonProperty(required = false) private Map headers; + + // TODO Add support for next page URL producer + // TODO ? Add proxy support ? + + public final void postLoad(Action action) { + Action.checkNotBlank("request target name", name, this); + Action.checkNotNull("request target base URL", baseUrl, this); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStep.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStep.java new file mode 100644 index 0000000000..a6c0879e7b --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStep.java @@ -0,0 +1,94 @@ +/** + * 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.model; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * 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 @EqualsAndHashCode(callSuper = true) +public final class ActionStep extends AbstractActionStep { + @JsonPropertyDescription("Optional list: Execute one or more REST requests.") + @JsonProperty(required = false) private List requests; + + @JsonPropertyDescription("Optional list: Execute one or more fcli commands. For now, only fcli commands that support the standard output options (--output/--store/--to-file) may be used, allowing the JSON output of those commands to be used in subsequent or nested steps. Any console output is suppressed, and any non-zero exit codes will produce an error.") + @JsonProperty(required = false) private List fcli; + + @JsonPropertyDescription("Optional SpEL template expression: Write a progress message.") + @JsonProperty(required = false) private TemplateExpression progress; + + @JsonPropertyDescription("Optional SpEL template expression: Write a warning message to console and log file (if enabled). Note that warning messages will be shown on console only after all action steps have been executed, to not interfere with progress messages.") + @JsonProperty(required = false) private TemplateExpression warn; + + @JsonPropertyDescription("Optional SpEL template expression: Write a debug message to log file (if enabled).") + @JsonProperty(required = false) private TemplateExpression debug; + + @JsonPropertyDescription("Optional SpEL template expression: Throw an exception, thereby terminating action execution.") + @JsonProperty(value = "throw", required = false) private TemplateExpression _throw; + + @JsonPropertyDescription("OptionalSpEL template expression: Terminate action execution and return the given exit code.") + @JsonProperty(value = "exit", required = false) private TemplateExpression _exit; + + @JsonPropertyDescription("Optional list: Set a data value for use in subsequent steps.") + @JsonProperty(required = false) private List set; + + @JsonPropertyDescription("Optional list: Append a data value for use in subsequent steps.") + @JsonProperty(required = false) private List append; + + @JsonPropertyDescription("Optional list: Unset a data value for use in subsequent steps.") + @JsonProperty(required = false) private List unset; + + @JsonPropertyDescription("Optional list: Write data to a file, stdout, or stderr. Note that output to stdout and stderr will be deferred until action termination as to not interfere with progress messages.") + @JsonProperty(required = false) private List write; + + @JsonPropertyDescription("Optional object: Iterate over a given array of values.") + @JsonProperty(required = false) private ActionStepForEach forEach; + + @JsonPropertyDescription("Optional list: Mostly used for security policy and similar actions to define PASS/FAIL criteria. Upon action termination, check results will be written to console and return a non-zero exit code if the outcome of on or more checks was FAIL.") + @JsonProperty(required = false) private List check; + + @JsonPropertyDescription("Optional list: Sub-steps to be executed; useful for grouping or conditional execution of multiple steps.") + @JsonProperty(required = false) private List steps; + + /** + * This method is invoked by the parent element (which may either be another + * step element, or the top-level {@link Action} instance). + * It invokes the postLoad() method on each request descriptor. + */ + public final void postLoad(Action action) {} +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepAppend.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepAppend.java new file mode 100644 index 0000000000..f64441271d --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepAppend.java @@ -0,0 +1,37 @@ +/** + * 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.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This class describes an operation to add a given value to the array or object + * identified by the name property. If the target array or object doesn't exist yet, + * it will be created. Whether this operation operates on objects or arrays depends + * on the presence of the 'property' property; if 'property' is present, we assume + * 'name' references an object, if 'property' is not present, we assume 'name' references + * an array. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public final class ActionStepAppend extends AbstractActionStepUpdateProperty { + @JsonPropertyDescription("Optional SpEL template expression: Property name to be added or updated in the data object specified by 'name'. If specified, 'name' is considered to be an object, otherwise 'name' is considered to be an array.") + @JsonProperty(required = false) private TemplateExpression property; +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepCheck.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepCheck.java new file mode 100644 index 0000000000..0a1dcd3a5d --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepCheck.java @@ -0,0 +1,73 @@ +/** + * 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.model; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This class describes a 'check' operation, mostly useful for actions that + * perform security gate or other checks. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public final class ActionStepCheck extends AbstractActionStep { + @JsonPropertyDescription("Required string: Display name of this check, to be displayed in PASS/FAIL messages.") + @JsonProperty(required = true) private String displayName; + + @JsonPropertyDescription("Required SpEL template expression if 'passIf' not specified: The outcome of this check will be 'FAIL' if the given expression evaluates to 'true', outcome will be 'PASS' otherwise.") + @JsonProperty(required = false) private TemplateExpression failIf; + + @JsonPropertyDescription("Required SpEL template expression if 'failIf' not specified: The outcome of this check will be 'SUCCESS' if the given expression evaluates to 'true', outcome will be 'FAIL' otherwise.") + @JsonProperty(required = false) private TemplateExpression passIf; + + @JsonPropertyDescription("Optional enum value: Define the check result in case the check is being skipped due to conditional execution or no records to be processed in forEach blocks.") + @JsonProperty(required = false, defaultValue = "SKIP") private CheckStatus ifSkipped = CheckStatus.SKIP; + + public final void postLoad(Action action) { + Action.checkNotBlank("check displayName", displayName, this); + Action.throwIf(failIf==null && passIf==null, this, ()->"Either passIf or failIf must be specified on check step"); + Action.throwIf(failIf!=null && passIf!=null, this, ()->"Only one of passIf or failIf may be specified on check step"); + } + + public static enum CheckStatus { + // Statuses must be defined in order of precedence when combining, + // i.e., when combining PASS and FAIL, outcome should be FAIL, so + // FAIL should come before PASS. + FAIL, PASS, SKIP, HIDE; + + public static CheckStatus combine(CheckStatus... statuses) { + return combine(statuses==null ? null : Arrays.asList(statuses)); + } + + public static CheckStatus combine(Collection statuses) { + if ( statuses==null ) { return null; } + var set = new HashSet(statuses); + for ( var s: values() ) { + if ( set.contains(s) ) { return s; } + } + // Can only happen if all statuses are null + return null; + } + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepFcli.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepFcli.java new file mode 100644 index 0000000000..e4d2ddcbb3 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepFcli.java @@ -0,0 +1,58 @@ +/** + * 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.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This class describes a forEach element, allowing iteration over the output of + * a given input. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public final class ActionStepFcli extends AbstractActionStep { + @JsonPropertyDescription("Required SpEL template expression: Arguments to pass to the fcli command, for example 'ssc appversion list --embed=attrValuesByName'.") + @JsonProperty(required = true) private TemplateExpression args; + + @JsonPropertyDescription("Optional string: Name to assign to the outcome of this fcli invocation. Can be referenced in subsequent steps using ${[name]}.") + @JsonProperty(required = false) private String name; + + @JsonPropertyDescription("Optional object: Steps to be executed for each individual record generated by the given fcli invocation.") + @JsonProperty(value = "forEach", required = false) private ActionStepFcli.ActionStepFcliForEachDescriptor forEach; + + /** + * This method is invoked by the {@link ActionStep#postLoad()} + * method. It checks that required properties are set, then calls the postLoad() method for + * each sub-step. + */ + public final void postLoad(Action action) { + Action.checkNotNull("fcli args", args, this); + } + + /** + * This class describes an fcli forEach element, allowing iteration over the output of + * the fcli command. + */ + @Reflectable @NoArgsConstructor + @Data @EqualsAndHashCode(callSuper = true) + public static final class ActionStepFcliForEachDescriptor extends AbstractActionStepForEach { + protected final void _postLoad(Action action) {} + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepForEach.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepForEach.java new file mode 100644 index 0000000000..24120ee715 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepForEach.java @@ -0,0 +1,51 @@ +/** + * 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.model; + +import java.util.function.Function; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.databind.JsonNode; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This class describes a forEach element, allowing iteration over the output of + * a given input. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper=true) +public final class ActionStepForEach extends AbstractActionStepForEach { + /** Processor that runs the forEach steps. This expression must evaluate to an + * IActionStepForEachProcessor instance. */ + @JsonPropertyDescription("Required SpEL template expression if 'values' not specified: Reference to a processor that generates JSON records and executes the given steps for each record. For now, the only available processor is ${#ssc.ruleDescriptionsProcessor(parameters.appversion.id)} for processing rule descriptions from the current state FPR file, used by some built-in actions.") + @JsonProperty(required = false) private TemplateExpression processor; + + @JsonPropertyDescription("Required SpEL template expression if 'processor' not specified: Array of values to be iterated over.") + @JsonProperty(required = false) private TemplateExpression values; + + public final void _postLoad(Action action) {} + + @FunctionalInterface + public static interface IActionStepForEachProcessor { + /** Implementations of this method should invoke the given function for every + * JsonNode to be processed, and terminate processing if the given function + * returns false. */ + public void process(Function consumer); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepRequest.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepRequest.java new file mode 100644 index 0000000000..44105f40eb --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepRequest.java @@ -0,0 +1,111 @@ +/** + * 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.model; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +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.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This class describes a REST request. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public final class ActionStepRequest extends AbstractActionStep { + @JsonPropertyDescription("Required string: Name to assign to the JSON response for this REST request. Can be referenced in subsequent steps using ${[name]} to access transformed data (if applicable) or ${[name]_raw} to access raw, untransformed data.") + @JsonProperty(required = true) private String name; + + @JsonPropertyDescription("Optional string: HTTP method like GET or POST to use for this REST request. Defaults to 'GET'.") + @JsonProperty(required = false, defaultValue = "GET") private String method = HttpMethod.GET.name(); + + @JsonPropertyDescription("Required SpEL template expression: Unqualified REST URI, like '/api/v3/some/api/${parameters.[name].id}' to be appended to the base URL as configured for the given 'target'.") + @JsonProperty(required = true) private TemplateExpression uri; + + @JsonPropertyDescription("Required string if no default target has been configured through defaults.requestTarget: Target on which to execute the REST request. This may be 'fod' (for actions in FoD module), 'ssc' (for actions in SSC module), or a custom request target as configured through 'addRequestTargets'.") + @JsonProperty(required = false) private String target; + + @JsonPropertyDescription("Optional map(string,SpEL template expression): Map of query parameters and corresponding values, for example 'someParam: ${[name].[property]}'.") + @JsonProperty(required = false) private Map query; + + @JsonPropertyDescription("Optional SpEL template expression: Request body to send with the REST request.") + @JsonProperty(required = false) private TemplateExpression body; + + @JsonPropertyDescription("Optional enum value: Flag to indicate whether this is a 'paged' or 'simple' request. If set to 'paged' (only available for 'fod' and 'ssc' request targets for now), all pages will be automatically processed. Defaults to 'simple'.") + @JsonProperty(required = false, defaultValue = "simple") private ActionStepRequest.ActionStepRequestType type = ActionStepRequestType.simple; + + @JsonPropertyDescription("Optional object: Progress messages for various stages of request/response processing.") + @JsonProperty(required = false) private ActionStepRequest.ActionStepRequestPagingProgressDescriptor pagingProgress; + + @JsonPropertyDescription("Optional list: Steps to be executed on the overall response before executing any 'forEach' steps.") + @JsonProperty(required = false) private List onResponse; + + @JsonPropertyDescription("Optional list: Steps to be executed on request failure. If not specified, an exception will be thrown on request failure.") + @JsonProperty(required = false) private List onFail; + + @JsonPropertyDescription("Optional object: Steps to be executed for each record in the REST response.") + @JsonProperty(required = false) private ActionStepRequest.ActionStepRequestForEachDescriptor forEach; + + /** + * This method is invoked by {@link ActionStep#postLoad()} + * method. It checks that required properties are set. + */ + public final void postLoad(Action action) { + Action.checkNotBlank("request name", name, this); + Action.checkNotNull("request uri", uri, this); + if ( StringUtils.isBlank(target) && action.getDefaults()!=null ) { + target = action.getDefaults().getRequestTarget(); + } + Action.checkNotBlank("request target", target, this); + if ( pagingProgress!=null ) { + type = ActionStepRequestType.paged; + } + } + + /** + * This class describes a request 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 @EqualsAndHashCode(callSuper = true) + public static final class ActionStepRequestForEachDescriptor extends AbstractActionStepForEach implements IActionStepIfSupplier { + private List embed; + + protected final void _postLoad(Action action) { + //throw new RuntimeException("test"); + } + } + + @Reflectable + public static enum ActionStepRequestType { + simple, paged + } + + @Reflectable @NoArgsConstructor + @Data + public static final class ActionStepRequestPagingProgressDescriptor { + private TemplateExpression prePageLoad; + private TemplateExpression postPageLoad; + private TemplateExpression postPageProcess; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepSet.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepSet.java new file mode 100644 index 0000000000..bc864df287 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepSet.java @@ -0,0 +1,28 @@ +/** + * 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.model; + +import com.formkiq.graalvm.annotations.Reflectable; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This class describes an operation to explicitly set a data property. + * Note that data properties for request outputs are set automatically. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public final class ActionStepSet extends AbstractActionStepUpdateProperty { +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepUnset.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepUnset.java new file mode 100644 index 0000000000..b8ab78f37f --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepUnset.java @@ -0,0 +1,35 @@ +/** + * 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.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This class describes an operation to explicitly unset a data property. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public final class ActionStepUnset extends AbstractActionStep { + @JsonPropertyDescription("Required string: Name to unset.") + @JsonProperty(required = true) private String name; + + public void postLoad(Action action) { + Action.checkNotBlank("set name", name, this); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepWrite.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepWrite.java new file mode 100644 index 0000000000..d9d605cc3a --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionStepWrite.java @@ -0,0 +1,43 @@ +/** + * 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.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * This class describes a 'write' step. + */ +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public final class ActionStepWrite extends AbstractActionStep implements IActionStepValueSupplier { + @JsonPropertyDescription("Required SpEL template expression: Specify where to write the given data; either 'stdout', 'stderr' or a filename.") + @JsonProperty(required = true) private TemplateExpression to; + + @JsonPropertyDescription("Required SpEL template expression if 'valueTemplate' is not specified: Value to be written to the given output.") + @JsonProperty(required = false) private TemplateExpression value; + + @JsonPropertyDescription("Required string if 'value' is not specified: Name of a value template to be evaluated, writing the outcome of the value template to the given output.") + @JsonProperty(required = false) private String valueTemplate; + + public void postLoad(Action action) { + Action.checkNotNull("write to", to, this); + Action.checkActionValueSupplier(action, this); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionUsage.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionUsage.java new file mode 100644 index 0000000000..cfb8050027 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionUsage.java @@ -0,0 +1,38 @@ +/** + * 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.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.formkiq.graalvm.annotations.Reflectable; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * This class describes action usage header and description. + */ +@Reflectable @NoArgsConstructor +@Data +public final class ActionUsage implements IActionElement { + @JsonPropertyDescription("Required string: Action usage header, displayed in list and help outputs") + @JsonProperty(required = true) private String header; + + @JsonPropertyDescription("Required string: Action usage description, displayed in help output") + @JsonProperty(required = true) private String description; + + public void postLoad(Action action) { + Action.checkNotBlank("usage header", header, this); + Action.checkNotBlank("usage description", description, this); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionValidationException.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionValidationException.java new file mode 100644 index 0000000000..ca541191dc --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionValidationException.java @@ -0,0 +1,40 @@ +/** + * 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.model; + +/** + * Exception class used for action validation errors. + */ +public 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()); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionValueTemplate.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionValueTemplate.java new file mode 100644 index 0000000000..3101a04901 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/ActionValueTemplate.java @@ -0,0 +1,78 @@ +/** + * 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.model; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.expression.ParseException; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +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.TemplateExpression; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * This class describes an output, which can be either a top-level output + * or partial output. + */ +@Reflectable @NoArgsConstructor +@Data +public final class ActionValueTemplate implements IActionElement { + @JsonPropertyDescription("Required string: Name of this value template.") + @JsonProperty(required = true) private String name; + + @JsonPropertyDescription("Required object|string: Text or structured JSON contents, where each text node is assumed to be a template expression.") + @JsonProperty(required = true) private JsonNode contents; + + /** Cached mapping from text node property path to corresponding TemplateExpression instance */ + @JsonIgnore 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(Action action) { + Action.checkNotBlank("(partial) output name", name, this); + Action.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), ActionValueTemplate.this, e); + } + } + super.walkValue(state, path, parent, node); + } + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionElement.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionElement.java new file mode 100644 index 0000000000..f427dc3429 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionElement.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.action.model; + +/** + * + * @author Ruud Senden + */ +public interface IActionElement { + void postLoad(Action action); +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionStep.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionStep.java new file mode 100644 index 0000000000..85b4408ab7 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionStep.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.action.model; + +/** + * + * @author Ruud Senden + */ +public interface IActionStep extends IActionElement, IActionStepIfSupplier { + +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionStepIfSupplier.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionStepIfSupplier.java new file mode 100644 index 0000000000..d63d3b7402 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionStepIfSupplier.java @@ -0,0 +1,19 @@ +/** + * 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.model; + +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +public interface IActionStepIfSupplier { + TemplateExpression get_if(); +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionStepValueSupplier.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionStepValueSupplier.java new file mode 100644 index 0000000000..04f380d43c --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/model/IActionStepValueSupplier.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.action.model; + +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; + +public interface IActionStepValueSupplier { + TemplateExpression getValue(); + String getValueTemplate(); +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java new file mode 100644 index 0000000000..e9ea65a9d6 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java @@ -0,0 +1,71 @@ +/** + * 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.runner; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.action.model.ActionParameter; +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(Action 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, ActionParameter 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(Action 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/runner/ActionRunner.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunner.java new file mode 100644 index 0000000000..4e6e8aa7da --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionRunner.java @@ -0,0 +1,1010 @@ +/** + * 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.runner; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +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.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fortify.cli.common.action.model.AbstractActionStepForEach; +import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.action.model.ActionParameter; +import com.fortify.cli.common.action.model.ActionRequestTarget; +import com.fortify.cli.common.action.model.ActionStep; +import com.fortify.cli.common.action.model.ActionStepAppend; +import com.fortify.cli.common.action.model.ActionStepCheck; +import com.fortify.cli.common.action.model.ActionStepCheck.CheckStatus; +import com.fortify.cli.common.action.model.ActionStepFcli; +import com.fortify.cli.common.action.model.ActionStepForEach; +import com.fortify.cli.common.action.model.ActionStepForEach.IActionStepForEachProcessor; +import com.fortify.cli.common.action.model.ActionStepRequest; +import com.fortify.cli.common.action.model.ActionStepRequest.ActionStepRequestForEachDescriptor; +import com.fortify.cli.common.action.model.ActionStepRequest.ActionStepRequestPagingProgressDescriptor; +import com.fortify.cli.common.action.model.ActionStepRequest.ActionStepRequestType; +import com.fortify.cli.common.action.model.ActionStepSet; +import com.fortify.cli.common.action.model.ActionStepUnset; +import com.fortify.cli.common.action.model.ActionStepWrite; +import com.fortify.cli.common.action.model.ActionValidationException; +import com.fortify.cli.common.action.model.ActionValueTemplate; +import com.fortify.cli.common.action.model.IActionStepIfSupplier; +import com.fortify.cli.common.action.model.IActionStepValueSupplier; +import com.fortify.cli.common.action.runner.ActionRunner.IActionRequestHelper.ActionRequestDescriptor; +import com.fortify.cli.common.action.runner.ActionRunner.IActionRequestHelper.BasicActionRequestHelper; +import com.fortify.cli.common.cli.util.FcliCommandExecutor; +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; +import picocli.CommandLine; + +// TODO Move processing of each descriptor element into a separate class, +// either for all elements or just for step elements. +// For example, each of these classes could have a (static?) +// process(context, descriptor element), with context providing access +// to ActionRunner fields, parent steps, local data, shared methods like +// setDataValue(), ... +@Builder +public class ActionRunner implements AutoCloseable { + /** Jackson {@link ObjectMapper} used for various JSON-related operations */ + private static final ObjectMapper objectMapper = JsonHelper.getObjectMapper(); + /** Jackson {@link ObjectMapper} used for formatting steps in logging/exception messages */ + private static final ObjectMapper yamlObjectMapper = new ObjectMapper(new YAMLFactory()); + /** Logger */ + private static final Logger LOG = LoggerFactory.getLogger(ActionRunner.class); + /** Progress writer, provided through builder method */ + private final IProgressWriterI18n progressWriter; + /** Root CommandLine object for executing fcli commands, provided through builder method */ + private final CommandLine rootCommandLine; + /** Data extract action, provided through builder method */ + @Getter private final Action 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<>(); + /** Check statuses */ + private final Map checkStatuses = new LinkedHashMap<>(); + // We need to delay writing output to console as to not interfere with progress writer + private final List delayedConsoleWriterRunnables = new ArrayList<>(); + /** Save original stdout for delayed output operations */ + private final PrintStream stdout = System.out; + /** Save original stderr for delayed output operations */ + private final PrintStream stderr = System.err; + @Builder.Default private int exitCode = 0; + @Builder.Default private boolean exitRequested = false; + + public final Callable run(String[] args) { + initializeCheckStatuses(); + 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("Action processing finished"); + } + return ()->{ + delayedConsoleWriterRunnables.forEach(Runnable::run); + if ( !checkStatuses.isEmpty() ) { + checkStatuses.entrySet().forEach( + e-> printCheckResult(e.getValue(), e.getKey())); + var overallStatus = CheckStatus.combine(checkStatuses.values()); + stdout.println("Status: "+overallStatus); + if ( exitCode==0 && overallStatus==CheckStatus.FAIL ) { + exitCode = 100; + } + } + return exitCode; + }; + } + + private void initializeCheckStatuses() { + for ( var elt : action.getAllActionElements() ) { + if ( elt instanceof ActionStepCheck ) { + var checkStep = (ActionStepCheck)elt; + var displayName = checkStep.getDisplayName(); + var value = CheckStatus.combine(checkStatuses.get(displayName), checkStep.getIfSkipped()); + checkStatuses.put(displayName, value); + } + } + } + + private final void printCheckResult(CheckStatus status, String displayName) { + if ( status!=CheckStatus.HIDE ) { + // Even when flushing, output may appear in incorrect order if some + // check statuses are written to stdout and others to stderr. + //var out = status==CheckStatus.PASS?stdout:stderr; + var out = stdout; + out.println(String.format("%s: %s", status, displayName)); + //out.flush(); + } + } + + 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; + } + + public 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, ActionParameter 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, ActionParameter 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(ActionParameter 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, ActionParameter parameter) { + var optionName = ActionParameterHelper.getOptionName(parameter.getName()); + return parseResult.getOptionValuesByName().get(optionName); + } + + private JsonNode convertParameterValue(String value, ActionParameter 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(ActionRequestTarget descriptor) { + requestHelpers.put(descriptor.getName(), createBasicRequestHelper(descriptor)); + } + + private IActionRequestHelper createBasicRequestHelper(ActionRequestTarget 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); + } + } + + private final class ActionStepsProcessor { + private final ObjectNode localData; + private final ActionStepsProcessor parent; + + public ActionStepsProcessor(ObjectNode localData, ActionStepsProcessor parent) { + this.localData = localData; + this.parent = 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(ActionStep step) { + if ( _if(step) ) { + processStepSupplier(step::getProgress, this::processProgressStep); + processStepSupplier(step::getWarn, this::processWarnStep); + processStepSupplier(step::getDebug, this::processDebugStep); + processStepSupplier(step::get_throw, this::processThrowStep); + processStepSupplier(step::get_exit, this::processExitStep); + processStepSupplier(step::getRequests, this::processRequestsStep); + processStepSupplier(step::getForEach, this::processForEachStep); + processStepEntries(step::getFcli, this::processFcliStep); + processStepEntries(step::getSet, this::processSetStep); + processStepEntries(step::getAppend, this::processAppendStep); + processStepEntries(step::getUnset, this::processUnsetStep); + processStepEntries(step::getCheck, this::processCheckStep); + processStepEntries(step::getWrite, this::processWriteStep); + processStepEntries(step::getSteps, this::processStep); + } + } + + private void processStepEntries(Supplier> supplier, Consumer consumer) { + var list = supplier.get(); + if ( list!=null ) { list.forEach(value->processStep(value, consumer)); } + } + + private void processStepSupplier(Supplier supplier, Consumer consumer) { + processStep(supplier.get(), consumer); + } + + private void processStep(T value, Consumer consumer) { + if ( _if(value) ) { + String valueString = null; + if ( LOG.isDebugEnabled() ) { + valueString = getStepAsString(valueString, value); + LOG.debug("Start processing:\n"+valueString); + } + try { + consumer.accept(value); + } catch ( Exception e ) { + if ( e instanceof StepProcessingException ) { + throw e; + } else { + valueString = getStepAsString(valueString, value); + throw new StepProcessingException("Error processing:\n"+valueString, e); + } + } + if ( LOG.isDebugEnabled() ) { + valueString = getStepAsString(valueString, value); + LOG.debug("End processing:\n"+valueString); + } + } + } + + private final String getStepAsString(String cachedString, Object value) { + if ( value==null ) { return null; } + if ( cachedString!=null ) { return cachedString; } + try { + cachedString = String.format("%s:\n%s", + StringUtils.indent(value.getClass().getCanonicalName(), " "), + StringUtils.indent(yamlObjectMapper.valueToTree(value).toPrettyString(), " ")); + } catch ( Exception e ) { + cachedString = StringUtils.indent(value.toString(), " "); + } + return cachedString; + } + + private final boolean _if(Object o) { + if (exitRequested || o==null) { return false; } + if (o instanceof IActionStepIfSupplier ) { + var _if = ((IActionStepIfSupplier) o).get_if(); + if ( _if!=null ) { + return spelEvaluator.evaluate(_if, localData, Boolean.class); + } + } + return true; + } + + private void processSetStep(ActionStepSet set) { + var name = set.getName(); + var value = getValue(set); + setDataValue(name, value); + } + + private void processAppendStep(ActionStepAppend append) { + var name = append.getName(); + var property = append.getProperty(); + var currentValue = localData.get(name); + var valueToAppend = getValue(append); + if ( property==null ) { + appendToArray(name, currentValue, valueToAppend); + } else { + appendToObject(name, currentValue, spelEvaluator.evaluate(property, localData, String.class), valueToAppend); + } + } + + private void appendToArray(String name, JsonNode currentValue, JsonNode valueToAppend) { + if ( currentValue==null ) { + currentValue = objectMapper.createArrayNode(); + } + if ( !currentValue.isArray() ) { + throw new IllegalStateException("Cannot append value to non-array node "+currentValue.getNodeType()); + } else { + if ( valueToAppend!=null ) { + ((ArrayNode)currentValue).add(valueToAppend); + } + setDataValue(name, currentValue); // Update copies in parents + } + } + + private void appendToObject(String name, JsonNode currentValue, String property, JsonNode valueToAppend) { + if ( currentValue==null ) { + currentValue = objectMapper.createObjectNode(); + } + if ( !currentValue.isObject() ) { + throw new IllegalStateException(String.format("Cannot append value to non-object node "+currentValue.getNodeType())); + } else { + if ( valueToAppend!=null ) { + ((ObjectNode)currentValue).set(property, valueToAppend); + } + setDataValue(name, currentValue); // Update copies in parents + } + } + + private void processUnsetStep(ActionStepUnset unset) { + unsetDataValue(unset.getName()); + } + + private void setDataValue(String name, JsonNode value) { + if ( LOG.isDebugEnabled() ) { + LOG.debug(String.format("Set %s: %s", name, value.toPrettyString())); + } + if ( "parameters".equals(name) ) { + // If we ever implement some action security analysis, we want to + // consider user-provided parameter values as 'safe' values in + // potentially unsafe actions. As such, actions are not allowed + // to update the parameters object. + throw new IllegalStateException("Action steps are not allowed to modify 'parameters'"); + } + localData.set(name, value); + if ( parent!=null ) { parent.setDataValue(name, value); } + } + + private void unsetDataValue(String name) { + localData.remove(name); + if ( parent!=null ) { parent.unsetDataValue(name); } + } + + private JsonNode getValue(IActionStepValueSupplier supplier) { + var value = supplier.getValue(); + var valueTemplate = supplier.getValueTemplate(); + if ( value!=null ) { return getValue(value); } + else if ( StringUtils.isNotBlank(valueTemplate) ) { return getTemplateValue(valueTemplate); } + else { throw new IllegalStateException("Either value or valueTemplate must be specified"); } + } + + private JsonNode getValue(TemplateExpression valueExpression) { + var value = spelEvaluator.evaluate(valueExpression, localData, Object.class); + return objectMapper.valueToTree(value); + } + + private final JsonNode getTemplateValue(String templateName) { + var valueTemplateDescriptor = action.getValueTemplatesByName().get(templateName); + var outputRawContents = valueTemplateDescriptor.getContents(); + return new JsonNodeOutputWalker(spelEvaluator, valueTemplateDescriptor, localData).walk(outputRawContents); + } + + private void processWriteStep(ActionStepWrite write) { + var to = spelEvaluator.evaluate(write.getTo(), localData, String.class); + var value = asString(getValue(write)); + try { + switch (to.toLowerCase()) { + case "stdout": delayedConsoleWriterRunnables.add(createRunner(stdout, value)); break; + case "stderr": delayedConsoleWriterRunnables.add(createRunner(stderr, 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, StandardCharsets.UTF_8) ) { + 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 processWarnStep(TemplateExpression progress) { + progressWriter.writeWarning(spelEvaluator.evaluate(progress, localData, String.class)); + } + + private void processDebugStep(TemplateExpression progress) { + LOG.debug(spelEvaluator.evaluate(progress, localData, String.class)); + } + + private void processThrowStep(TemplateExpression message) { + throw new StepProcessingException(spelEvaluator.evaluate(message, localData, String.class)); + } + + private void processExitStep(TemplateExpression exitCodeExpression) { + exitCode = spelEvaluator.evaluate(exitCodeExpression, localData, Integer.class); + exitRequested = true; + } + + private void processForEachStep(ActionStepForEach forEach) { + var processorExpression = forEach.getProcessor(); + var valuesExpression = forEach.getValues(); + if ( processorExpression!=null ) { + var processor = spelEvaluator.evaluate(processorExpression, localData, IActionStepForEachProcessor.class); + if ( processor!=null ) { processor.process(node->processForEachStepNode(forEach, node)); } + } else if ( valuesExpression!=null ) { + var values = spelEvaluator.evaluate(valuesExpression, localData, ArrayNode.class); + if ( values!=null ) { + // Process values until processForEachStepNode() returns false + JsonHelper.stream(values) + .allMatch(value->processForEachStepNode(forEach, value)); + } + } + } + + private boolean processForEachStepNode(AbstractActionStepForEach forEach, JsonNode node) { + if ( forEach==null ) { return false; } + var breakIf = forEach.getBreakIf(); + setDataValue(forEach.getName(), node); + if ( breakIf!=null && spelEvaluator.evaluate(breakIf, localData, Boolean.class) ) { + return false; + } + if ( _if(forEach) ) { + processSteps(forEach.get_do()); + } + return true; + } + + private void processCheckStep(ActionStepCheck check) { + var displayName = check.getDisplayName(); + var failIf = check.getFailIf(); + var passIf = check.getPassIf(); + var pass = passIf!=null + ? spelEvaluator.evaluate(passIf, localData, Boolean.class) + : !spelEvaluator.evaluate(failIf, localData, Boolean.class); + var currentStatus = pass ? CheckStatus.PASS : CheckStatus.FAIL; + checkStatuses.compute(displayName, (name,oldStatus)->CheckStatus.combine(oldStatus, currentStatus)); + } + + private void processFcliStep(ActionStepFcli fcli) { + var args = spelEvaluator.evaluate(fcli.getArgs(), localData, String.class); + progressWriter.writeProgress("Executing fcli %s", args); + var cmdExecutor = new FcliCommandExecutor(rootCommandLine, args); + Consumer recordConsumer = null; + var forEach = fcli.getForEach(); + var name = fcli.getName(); + if ( StringUtils.isNotBlank(name) ) { + setDataValue(name, objectMapper.createArrayNode()); + } + if ( forEach!=null || StringUtils.isNotBlank(name) ) { + if ( !cmdExecutor.canCollectRecords() ) { + throw new IllegalStateException("Can't use forEach or name on fcli command: "+args); + } else { + recordConsumer = new FcliRecordConsumer(fcli); + } + } + + // TODO Implement optional output suppression + var output = cmdExecutor.execute(recordConsumer, true); + delayedConsoleWriterRunnables.add(createRunner(System.err, output.getErr())); + delayedConsoleWriterRunnables.add(createRunner(System.out, output.getOut())); + if ( output.getExitCode() >0 ) { + throw new StepProcessingException("Fcli command returned non-zero exit code "+output.getExitCode()); + } + } + @RequiredArgsConstructor + private class FcliRecordConsumer implements Consumer { + private final ActionStepFcli fcli; + private boolean continueProcessing = true; + @Override + public void accept(ObjectNode record) { + var name = fcli.getName(); + if ( StringUtils.isNotBlank(name) ) { + // For name attribute, we want to collect all records, + // independent of break condition in the forEach block. + appendToArray(name, localData.get(name), record); + } + if ( continueProcessing ) { + continueProcessing = processForEachStepNode(fcli.getForEach(), record); + } + } + } + + private void processRequestsStep(List requests) { + if ( requests!=null ) { + var requestsProcessor = new ActionStepRequestsProcessor(); + requestsProcessor.addRequests(requests, this::processResponse, this::processFailure, localData); + requestsProcessor.executeRequests(); + } + } + + private final void processResponse(ActionStepRequest 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); + processRequestStepForEach(requestDescriptor); + } + + private final void processFailure(ActionStepRequest requestDescriptor, UnirestException e) { + var onFailSteps = requestDescriptor.getOnFail(); + if ( onFailSteps==null ) { throw e; } + localData.putPOJO("exception", e); + processSteps(onFailSteps); + } + + private final void processOnResponse(ActionStepRequest requestDescriptor) { + var onResponseSteps = requestDescriptor.getOnResponse(); + processSteps(onResponseSteps); + } + + private final void processRequestStepForEach(ActionStepRequest requestDescriptor) { + var forEach = requestDescriptor.getForEach(); + if ( forEach!=null ) { + var input = localData.get(requestDescriptor.getName()); + if ( input!=null ) { + if ( input instanceof ArrayNode ) { + updateRequestStepForEachTotalCount(forEach, (ArrayNode)input); + processRequestStepForEachEmbed(forEach, (ArrayNode)input); + processRequestStepForEach(forEach, (ArrayNode)input, this::processRequestStepForEachEntryDo); + } else { + throw new ActionValidationException("forEach not supported on node type "+input.getNodeType()); + } + } + } + } + + private final void processRequestStepForEachEmbed(ActionStepRequestForEachDescriptor forEach, ArrayNode source) { + var requestExecutor = new ActionStepRequestsProcessor(); + processRequestStepForEach(forEach, source, getRequestForEachEntryEmbedProcessor(requestExecutor)); + requestExecutor.executeRequests(); + } + + @FunctionalInterface + private interface IRequestStepForEachEntryProcessor { + void process(ActionStepRequestForEachDescriptor forEach, JsonNode currentNode, ObjectNode newData); + } + + private final void processRequestStepForEach(ActionStepRequestForEachDescriptor forEach, ArrayNode source, IRequestStepForEachEntryProcessor 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 updateRequestStepForEachTotalCount(ActionStepRequestForEachDescriptor 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 processRequestStepForEachEntryDo(ActionStepRequestForEachDescriptor forEach, JsonNode currentNode, ObjectNode newData) { + var processor = new ActionStepsProcessor(newData, this); + processor.processSteps(forEach.get_do()); + } + + private IRequestStepForEachEntryProcessor getRequestForEachEntryEmbedProcessor(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)), + this::processFailure, 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, BiConsumer failureConsumer, ObjectNode data) { + if ( requestDescriptors!=null ) { + requestDescriptors.forEach(r->addRequest(r, responseConsumer, failureConsumer, data)); + } + } + + private final void addRequest(ActionStepRequest requestDescriptor, BiConsumer responseConsumer, BiConsumer failureConsumer, ObjectNode data) { + var _if = requestDescriptor.get_if(); + if ( _if==null || spelEvaluator.evaluate(_if, data, Boolean.class) ) { + var method = requestDescriptor.getMethod(); + var uri = spelEvaluator.evaluate(requestDescriptor.getUri(), data, String.class); + checkUri(uri); + var query = evaluateTemplateExpressionMap(requestDescriptor.getQuery(), data, Object.class); + var body = requestDescriptor.getBody()==null ? null : spelEvaluator.evaluate(requestDescriptor.getBody(), data, Object.class); + var requestData = new IActionRequestHelper.ActionRequestDescriptor(method, uri, query, body, r->responseConsumer.accept(requestDescriptor, r), e->failureConsumer.accept(requestDescriptor, e)); + addPagingProgress(requestData, requestDescriptor.getPagingProgress(), data); + if ( requestDescriptor.getType()==ActionStepRequestType.paged ) { + pagedRequests.computeIfAbsent(requestDescriptor.getTarget(), s->new ArrayList()).add(requestData); + } else { + simpleRequests.computeIfAbsent(requestDescriptor.getTarget(), s->new ArrayList()).add(requestData); + } + } + } + + private void checkUri(String uriString) { + try { + var uri = new URI(uriString); + // We don't allow absolute URIs, as this could expose authorization + // headers and other data to systems other than the predefined target + // system. + if ( uri.isAbsolute() ) { + throw new IllegalStateException("Absolute request uri is not allowed: "+uriString); + } + } catch ( URISyntaxException e ) { + throw new IllegalStateException("Invalid request uri: "+uriString); + } + } + + 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 UnirestInstance getUnirestInstance(); + public JsonNode transformInput(JsonNode input); + public void executePagedRequest(ActionRequestDescriptor requestDescriptor); + public void executeSimpleRequests(List requestDescriptor); + public void close(); + + @Data + public static final class ActionRequestDescriptor { + private final String method; + private final String uri; + private final Map queryParams; + private final Object body; + private final Consumer responseConsumer; + private final Consumer failureConsumer; + 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.getFailureConsumer().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.getFailureConsumer().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 Action action; + private final ActionParameter 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 ActionValueTemplate 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); + } + } + } + } + + public static final class StepProcessingException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public StepProcessingException(String message, Throwable cause) { + super(message, cause); + } + + public StepProcessingException(String message) { + super(message); + } + + public StepProcessingException(Throwable cause) { + super(cause); + } + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionSpelFunctions.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionSpelFunctions.java new file mode 100644 index 0000000000..fe3043f513 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionSpelFunctions.java @@ -0,0 +1,265 @@ +/******************************************************************************* + * 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.runner; + +import java.time.LocalDateTime; +import java.time.Year; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.TextNode; +import org.jsoup.parser.Parser; +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 String CODE_START = "\n===== CODE START =====\n"; + private static final String CODE_END = "\n===== CODE END =====\n"; + private static final Pattern CODE_PATTERN = Pattern.compile(String.format("%s(.*?)%s", CODE_START, CODE_END), Pattern.DOTALL); + 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 ? "" : elts.stream().map(Object::toString).collect(Collectors.joining(separator)); + } + + public static final String numberedList(List elts) { + StringBuilder builder = new StringBuilder(); + for ( var i=0; i < elts.size(); i++ ) { + builder.append(i+1).append(". ").append(elts.get(i)).append('\n'); + } + return builder.toString(); + } + + /** + * 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); + } + + public static final String repeat(String text, int count) { + if ( count<0 ) { return ""; } + StringBuilder sb = new StringBuilder(); + for ( int i=0; ie.replaceWith(new TextNode("\n"))); + document.select("p").prepend("\\n\\n"); + // Replace code blocks, either embedding in backticks if inline (no newline characters) + // or indenting with 4 spaces and fencing with CODE_START and CODE_END, which will remain + // in place when cleaning all HTML tags, and removed using pattern matching below. + document.select("span.code").forEach(ActionSpelFunctions::replaceCode); + document.select("code").forEach(ActionSpelFunctions::replaceCode); + document.select("pre").forEach(ActionSpelFunctions::replaceCode); + + // Remove all HTML tags. Note that for now, this keeps escaped characters like > + // We may want to have separate methods or method parameter to allow for escaped + // characters to be unescaped. + var s = Jsoup.clean(document.html().replaceAll("\\\\n", "\n"), "", Safelist.none(), new Document.OutputSettings().prettyPrint(false)); + + var sb = new StringBuilder(); + // Remove CODE_START and CODE_END fences + Matcher m = CODE_PATTERN.matcher(s); + while(m.find()){ + String code = m.group(1); + // Code may contain regex-related characters like ${..}, which we don't + // want to interpret as regex groups. So, we append an empty replacement + // (have Matcher append all text before the code block), then manually + // append the code block. See https://stackoverflow.com/a/948381 + m.appendReplacement(sb, ""); + sb.append(Parser.unescapeEntities(code, false)); + } + m.appendTail(sb); + return sb.toString(); + } + + private static final void replaceCode(Element e) { + var text = e.text(); + if ( text.contains("\n") ) { + text = "\n\n"+CODE_START+StringUtils.indent(text.replaceAll("\t", " "), " ")+CODE_END+"\n\n"; + } else { + text = "`"+text+"`"; + } + e.replaceWith(new TextNode(text)); + } + + public static final String cleanRuleDescription(String description) { + if( description==null ) { return ""; } + Document document = _asDocument(description); + var paragraphs = document.select("Paragraph"); + for ( var p : paragraphs ) { + var altParagraph = p.select("AltParagraph"); + if ( !altParagraph.isEmpty() ) { + p.replaceWith(new TextNode(String.join("\n\n",altParagraph.eachText()))); + } else { + p.remove(); + } + } + document.select("IfDef").remove(); + document.select("ConditionalText").remove(); + return _htmlToText(document); + } + + public static final String cleanIssueDescription(String description) { + if( description==null ) { return ""; } + Document document = _asDocument(description); + document.select("AltParagraph").remove(); + return _htmlToText(document); + } + + /** + * @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... dateStrings) { + var dateString = dateStrings==null||dateStrings.length==0 + ? currentDateTime() + : dateStrings[0]; + return formatDateTimeWithZoneId(pattern, dateString, ZoneId.systemDefault()); + } + + public static final String currentDateTime() { + return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()); + } + + /** + * 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); + } + + public static final Iterable asIterable(Iterator iterator) { + return () -> iterator; + } + + public static final String copyright() { + return String.format("Copyright (c) %s Open Text", Year.now().getValue()); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/AbstractRunnableCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/AbstractRunnableCommand.java index b581c0e6cd..9d2b81ed6f 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/AbstractRunnableCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/AbstractRunnableCommand.java @@ -14,6 +14,7 @@ import java.io.File; import java.util.Map; +import java.util.concurrent.Callable; import com.fortify.cli.common.cli.mixin.ICommandAware; @@ -36,7 +37,7 @@ * * @author Ruud Senden */ -public abstract class AbstractRunnableCommand implements Runnable { +public abstract class AbstractRunnableCommand implements Callable { // Have picocli inject the CommandSpec representing the current command @Spec private CommandSpec commandSpec; diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommandHelperMixin.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommandHelperMixin.java index 0a6d3a96f3..d108c80054 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommandHelperMixin.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommandHelperMixin.java @@ -19,6 +19,7 @@ import com.fortify.cli.common.util.JavaHelper; import lombok.Getter; +import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Model.CommandSpec; @@ -26,11 +27,13 @@ public final class CommandHelperMixin implements ICommandAware { @Getter private CommandSpec commandSpec; @Getter private IMessageResolver messageResolver; + @Getter private CommandLine rootCommandLine; @Override public final void setCommandSpec(CommandSpec commandSpec) { this.commandSpec = commandSpec; this.messageResolver = new CommandSpecMessageResolver(commandSpec); + this.rootCommandLine = _getRootCommandLine(commandSpec); } /** @@ -49,4 +52,13 @@ public final Optional getCommandAs(Class asType) { public final Object getCommand() { return commandSpec.userObject(); } + + public final CommandLine _getRootCommandLine(CommandSpec spec) { + var cl = spec.commandLine(); + while ( true ) { + if ( cl.getParent()==null ) break; + cl = cl.getParent(); + } + return cl; + } } \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommonOptionMixins.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommonOptionMixins.java index 2488882254..a07527ee9b 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommonOptionMixins.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/mixin/CommonOptionMixins.java @@ -13,11 +13,20 @@ package com.fortify.cli.common.cli.mixin; import java.io.File; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Function; + +import org.apache.commons.io.IOUtils; import com.fortify.cli.common.util.PicocliSpecHelper; import com.fortify.cli.common.util.StringUtils; import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import picocli.CommandLine.Mixin; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; @@ -54,7 +63,7 @@ public void checkConfirmed(Object... promptArgs) { if ( response.equalsIgnoreCase(expectedResponse) ) { return; } else { - throw new IllegalStateException("Aborting: operation aborted by user"); + throw new AbortedByUserException("Aborting: operation aborted by user"); } } } @@ -77,5 +86,60 @@ private String getPlainPrompt(CommandSpec spec, Object... promptArgs) { } return prompt; } + + public static final class AbortedByUserException extends IllegalStateException { + private static final long serialVersionUID = 1L; + public AbortedByUserException(String msg) { super(msg); } + } + } + + public static abstract class AbstractTextResolverMixin { + public abstract String getTextSource(); + + @RequiredArgsConstructor + private static enum TextSources { + file(TextSources::resolveFile), + url(TextSources::resolveUrl), + string(TextSources::resolveString), + env(TextSources::resolveEnv); + + private final Function resolver; + + public static final String resolve(String source) { + if ( source==null ) { return null; } + for ( var type : values() ) { + var prefix = type.name()+":"; + if ( source.startsWith(prefix) ) { + return type.resolver.apply(source.replaceFirst(prefix, "")); + } + } + return resolveFile(source); + } + + @SneakyThrows + private static final String resolveFile(String file) { + // As '~' will not be resolved by the shell due to the 'file:' + // prefix, we resolve this manually to user home directory. + file = file.replaceFirst("^~", System.getProperty("user.home")); + return Files.readString(Path.of(file)); + } + + @SneakyThrows + private static final String resolveUrl(String url) { + return IOUtils.toString(new URL(url), StandardCharsets.US_ASCII); + } + + private static final String resolveString(String string) { + return string; + } + + private static final String resolveEnv(String envName) { + return System.getenv(envName); + } + } + + public final String getText() { + return TextSources.resolve(getTextSource()); + } } } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/FcliCommandExecutor.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/FcliCommandExecutor.java new file mode 100644 index 0000000000..6923b9926f --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/FcliCommandExecutor.java @@ -0,0 +1,97 @@ +/** + * 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.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.action.runner.ActionRunner.StepProcessingException; +import com.fortify.cli.common.output.cli.cmd.IOutputHelperSupplier; +import com.fortify.cli.common.output.writer.output.standard.StandardOutputWriter; +import com.fortify.cli.common.util.JavaHelper; +import com.fortify.cli.common.util.OutputCollector; +import com.fortify.cli.common.util.OutputCollector.Output; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import picocli.CommandLine; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParseResult; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Data +public final class FcliCommandExecutor { + private final CommandLine rootCommandLine; + private final ParseResult parseResult; + + public FcliCommandExecutor(CommandLine rootCommandLine, String cmd) { + this(rootCommandLine, parse(rootCommandLine, cmd)); + } + + public FcliCommandExecutor(CommandLine rootCommandLine, String[] args) { + this(rootCommandLine, parse(rootCommandLine, args)); + } + + public final CommandSpec getLeafCommandSpec(ParseResult parseResult) { + var leafCommand = parseResult.subcommand(); + while (leafCommand.hasSubcommand() ) { + leafCommand = leafCommand.subcommand(); + } + return leafCommand.commandSpec(); + } + + public final Optional getLeafCommand(ParseResult parseResult, Class type) { + return JavaHelper.as(getLeafCommandSpec(parseResult).userObject(), type); + } + + public final Output execute() { + return OutputCollector.collectOutput(StandardCharsets.UTF_8, ()->_execute()); + } + + public final boolean canCollectRecords() { + return getLeafCommand(getParseResult(), IOutputHelperSupplier.class).isPresent(); + } + + public final Output execute(Consumer recordConsumer, boolean suppressOutput) { + if ( canCollectRecords() && recordConsumer!=null ) { + StandardOutputWriter.collectRecords(recordConsumer, suppressOutput); + } + return execute(); + } + + private final int _execute() { + try { + rootCommandLine.clearExecutionResults(); + return rootCommandLine.getExecutionStrategy().execute(parseResult); + } catch ( Exception e ) { + throw new StepProcessingException("Fcli command threw an exception", e); + } + } + + private static final ParseResult parse(CommandLine rootCommandLine, String cmd) { + List argsList = new ArrayList(); + Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(cmd); + while (m.find()) { argsList.add(m.group(1).replace("\"", "")); } + return parse(rootCommandLine, argsList.toArray(String[]::new)); + } + + private static final ParseResult parse(CommandLine rootCommandLine, String[] args) { + return rootCommandLine.parseArgs(args); + } +} 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..c0ddbaab2d --- /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/util/EncryptionHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/EncryptionHelper.java similarity index 94% rename from fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/EncryptionHelper.java rename to fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/EncryptionHelper.java index 9674002a6d..c7f055d3b8 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/EncryptionHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/EncryptionHelper.java @@ -10,7 +10,7 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.common.util; +package com.fortify.cli.common.crypto.helper; import java.io.IOException; import java.io.StringWriter; @@ -19,6 +19,9 @@ import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; import org.jasypt.iv.RandomIvGenerator; +import com.fortify.cli.common.util.EnvHelper; +import com.fortify.cli.common.util.StringUtils; + import lombok.RequiredArgsConstructor; public class EncryptionHelper { diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/SignatureHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/SignatureHelper.java new file mode 100644 index 0000000000..0ed0f81fd4 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/SignatureHelper.java @@ -0,0 +1,193 @@ +package com.fortify.cli.common.crypto.helper; + +import java.nio.file.Files; +import java.nio.file.Path; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.crypto.helper.impl.KPGenerator; +import com.fortify.cli.common.crypto.helper.impl.PublicKeyTrustStore; +import com.fortify.cli.common.crypto.helper.impl.SignedTextReader; +import com.fortify.cli.common.crypto.helper.impl.Signer; +import com.fortify.cli.common.crypto.helper.impl.TextSigner; +import com.fortify.cli.common.crypto.helper.impl.Verifier; +import com.fortify.cli.common.json.JsonNodeHolder; +import com.fortify.cli.common.util.StringUtils; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; + + +public class SignatureHelper { + public static final String FORTIFY_PUBLIC_KEY = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArij9U9yJVNc53oEMFWYp" + + "NrXUG1UoRZseDh/p34q1uywD70RGKKWZvXIcUAZZwbZtCu4i0UzsrKRJeUwqanbc" + + "woJvYanp6lc3DccXUN1w1Y0WOHOaBxiiK3B1TtEIH1cK/X+ZzazPG5nX7TSGh8Tp" + + "/uxQzUFli2mDVLqaP62/fB9uJ2joX9Gtw8sZfuPGNMRoc8IdhjagbFkhFT7WCZnk" + + "FH/4Co007lmXLAe12lQQqR/pOTeHJv1sfda1xaHtj4/Tcrq04Kx0ZmGAd5D9lA92" + + "8pdBbzoe/mI5/Sk+nIY3AHkLXB9YAaKJf//Wb1yiP1/hchtVkfXyIaGM+cVyn7AN" + + "VQIDAQAB"; + private static final Verifier FORTIFY_SIGNATURE_VERIFIER = verifier(FORTIFY_PUBLIC_KEY); + + public static final Verifier fortifySignatureVerifier() { + return FORTIFY_SIGNATURE_VERIFIER; + } + + public static final Verifier verifier(byte[] publicKey) { + return new Verifier(publicKey); + } + + public static final Verifier verifier(String pemOrBase64Key) { + return new Verifier(pemOrBase64Key); + } + + @SneakyThrows + public static final Verifier verifier(Path pemOrBase64KeyPath) { + return new Verifier(Files.readString(pemOrBase64KeyPath)); + } + + public static final Signer signer(String pemOrBase64Key, char[] passPhrase) { + return new Signer(pemOrBase64Key, passPhrase); + } + + public static final Signer signer(String pemOrBase64Key, String passPhrase) { + return new Signer(pemOrBase64Key, StringUtils.isBlank(passPhrase) ? null : passPhrase.toCharArray()); + } + + @SneakyThrows + public static final Signer signer(Path pemOrBase64KeyPath, char[] passPhrase) { + return new Signer(Files.readString(pemOrBase64KeyPath), passPhrase); + } + + @SneakyThrows + public static final Signer signer(Path pemOrBase64KeyPath, String passPhrase) { + return signer(pemOrBase64KeyPath, StringUtils.isBlank(passPhrase) ? null : passPhrase.toCharArray()); + } + + @SneakyThrows + public static final TextSigner textSigner(Path pemOrBase64KeyPath, String passPhrase) { + return textSigner(pemOrBase64KeyPath, StringUtils.isBlank(passPhrase) ? null : passPhrase.toCharArray()); + } + + public static final TextSigner textSigner(String pemOrBase64Key, char[] passPhrase) { + return new TextSigner(pemOrBase64Key, passPhrase); + } + + public static final TextSigner textSigner(String pemOrBase64Key, String passPhrase) { + return new TextSigner(pemOrBase64Key, StringUtils.isBlank(passPhrase) ? null : passPhrase.toCharArray()); + } + + @SneakyThrows + public static final TextSigner textSigner(Path pemOrBase64KeyPath, char[] passPhrase) { + return new TextSigner(Files.readString(pemOrBase64KeyPath), passPhrase); + } + + public static final SignedTextReader signedTextReader() { + return SignedTextReader.INSTANCE; + } + + @SneakyThrows + public static final KPGenerator keyPairGenerator(char[] passPhrase) { + return new KPGenerator(passPhrase); + } + + @SneakyThrows + public static final KPGenerator keyPairGenerator(String passPhrase) { + return keyPairGenerator(StringUtils.isBlank(passPhrase) ? null : passPhrase.toCharArray()); + } + + public static final PublicKeyTrustStore publicKeyTrustStore() { + return PublicKeyTrustStore.INSTANCE; + } + + @FunctionalInterface + public static interface InvalidSignatureHandler { + public static final InvalidSignatureHandler IGNORE = null; + public static final InvalidSignatureHandler EVALUATE = d->{}; + + void onInvalidSignature(SignedTextDescriptor descriptor); + } + + public static enum SignatureStatus { + VALID, MISMATCH, NO_PUBLIC_KEY, UNSIGNED, NOT_VERIFIED; + + public void throwIfNotValid(boolean throwIfNotValid) { + if ( throwIfNotValid && this!=VALID ) { + throw new IllegalStateException("Signature is not valid: "+this); + } + } + } + + @Data + public static final class SignatureValidator { + private final InvalidSignatureHandler invalidSignatureHandler; + private final String[] extraPublicKeys; + + public SignatureValidator(InvalidSignatureHandler invalidSignatureHandler, String... extraPublicKeys) { + this.invalidSignatureHandler = invalidSignatureHandler; + this.extraPublicKeys = extraPublicKeys; + } + } + + @Reflectable @NoArgsConstructor + @Data @AllArgsConstructor @Builder + public static final class SignatureDescriptor { + /** Actual signature */ + private String signature; + /** Public key fingerprint */ + private String publicKeyFingerprint; + /** Signature metadata */ + private SignatureMetadata metadata; + } + + @Reflectable @NoArgsConstructor + @Data @AllArgsConstructor @Builder + public static final class SignatureMetadata { + /** Fcli version */ + private String fcliVersion; + /** Free-format string describing who signed this file */ + private String signer; + /** Additional free-format information about the signed file, + * for example information about where the public key can + * be found, information about the signed file, ... */ + private ObjectNode extraInfo; + } + + @Reflectable @NoArgsConstructor + @Data @AllArgsConstructor @Builder + public static final class SignedTextDescriptor { + /** Original text that was parsed, including signature if present */ + private String original; + /** Text that was signed */ + private String payload; + /** Signature descriptor */ + private SignatureDescriptor signatureDescriptor; + /** Signature status */ + private SignatureStatus signatureStatus; + /** Public key descriptor */ + private PublicKeyDescriptor publicKeyDescriptor; + } + + @Reflectable @NoArgsConstructor + @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor @Builder(toBuilder = true) + public static final class PublicKeyDescriptor extends JsonNodeHolder { + /** Name for this public key */ + private String name; + /** Fingerprint for this public key */ + private String fingerprint; + /** Actual public key */ + private String publicKey; + /** Source where public key was loaded from */ + @JsonIgnore private PublicKeySource source; + } + + @Reflectable + public static enum PublicKeySource { + TRUSTSTORE, EXTERNAL, INTERNAL + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/InternalSignatureUtil.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/InternalSignatureUtil.java new file mode 100644 index 0000000000..8d38ded8a6 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/InternalSignatureUtil.java @@ -0,0 +1,130 @@ +/** + * 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.crypto.helper.impl; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.KeyFactory; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +final class InternalSignatureUtil { + // This character is used by TextFileSigner and SignedTextFileReader to + // separate original text document and signature YAML document. + static final char FILE_SEPARATOR = '\u001C'; + static final KeyFactory KEY_FACTORY = InternalSignatureUtil.createKeyFactory(); + + @SneakyThrows + private static final KeyFactory createKeyFactory() { + return KeyFactory.getInstance("RSA"); + } + + static final byte[] parseKey(String pemOrBase64Key) { + if ( pemOrBase64Key==null ) { return null; } + var base64 = pemOrBase64Key.replaceAll("-----(BEGIN|END) [\\sA-Z]+ KEY-----|\\s", ""); + return Base64.getDecoder().decode(base64); + } + + @SneakyThrows + static final void writePem(String type, byte[] key, Path path) { + var pemString = asPem(type, key); + Files.writeString(path, pemString, StandardOpenOption.CREATE_NEW); + } + + @SneakyThrows + static final void writePem(String type, byte[] key, char[] passPhrase, Path path) { + var pemString = asPem(type, key, passPhrase); + Files.writeString(path, pemString, StandardOpenOption.CREATE_NEW); + } + + private static final String asPem(String type, byte[] key, char[] passPhrase) { + if ( "PRIVATE KEY".equals(type) && passPhrase!=null ) { + //throw new RuntimeException("Generating password-protected private key not supported yet"); + return asPem("ENCRYPTED PRIVATE KEY", pemEncrypt(key, passPhrase)); + } else { + return asPem(type, key); + } + } + + private static final String asPem(String type, byte[] key) { + return "-----BEGIN "+type+"-----\n" + + Base64.getMimeEncoder().encodeToString(key) + + "\n-----END "+type+"-----"; + } + + /* Less preferred algorithm, but both encrypting and decrypting the + * private key work on Java 17. + */ + @SneakyThrows + private static byte[] pemEncrypt(byte[] key, char[] passPhrase) { + byte[] salt = new byte[16]; new SecureRandom().nextBytes(salt); + String pbealg = "PBEwithSHA1andDESede"; + SecretKey secretKey = SecretKeyFactory.getInstance(pbealg) + .generateSecret(new PBEKeySpec(passPhrase)); + Cipher cipher = Cipher.getInstance(pbealg); + cipher.init (Cipher.ENCRYPT_MODE, secretKey, new PBEParameterSpec(salt,65536)); + byte[] body = cipher.doFinal(key); + return new EncryptedPrivateKeyInfo(cipher.getParameters(),body).getEncoded(); + } + + static interface ISignatureUpdater { + void updateSignature(Signature signature) throws IOException, SignatureException; + } + + @RequiredArgsConstructor + static final class FileSignatureUpdater implements ISignatureUpdater { + private final File file; + + @Override + public void updateSignature(Signature signature) throws IOException, SignatureException { + try ( var is = new FileInputStream(file); ) { + byte[] buffer = new byte[4096]; + int read = 0; + while ( (read = is.read(buffer)) > 0 ) { + signature.update(buffer, 0, read); + } + } + } + } + + @RequiredArgsConstructor + static final class DataSignatureUpdater implements ISignatureUpdater { + private final byte[] data; + + DataSignatureUpdater(String s, Charset charset) { + this(s.getBytes(charset)); + } + + @Override + public void updateSignature(Signature signature) throws IOException, SignatureException { + signature.update(data); + } + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/KPGenerator.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/KPGenerator.java new file mode 100644 index 0000000000..099dd1e52f --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/KPGenerator.java @@ -0,0 +1,39 @@ +/** + * 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.crypto.helper.impl; + +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +@RequiredArgsConstructor +public final class KPGenerator { + private final char[] passPhrase; + + @SneakyThrows + public final KeyPair generate() { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + return kpg.generateKeyPair(); + } + + @SneakyThrows + public final void writePem(Path privateKeyPath, Path publicKeyPath) { + var kp = generate(); + InternalSignatureUtil.writePem("PRIVATE KEY", kp.getPrivate().getEncoded(), passPhrase, privateKeyPath); + InternalSignatureUtil.writePem("PUBLIC KEY", kp.getPublic().getEncoded(), publicKeyPath); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/PublicKeyTrustStore.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/PublicKeyTrustStore.java new file mode 100644 index 0000000000..930ed138d2 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/PublicKeyTrustStore.java @@ -0,0 +1,91 @@ +/** + * 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.crypto.helper.impl; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; + +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeySource; +import com.fortify.cli.common.util.FcliDataHelper; + +public final class PublicKeyTrustStore { + public static final PublicKeyTrustStore INSTANCE = new PublicKeyTrustStore(); + private PublicKeyTrustStore() {} + + public PublicKeyDescriptor importKey(String publicKey, String name) { + var fingerprint = new Verifier(publicKey).publicKeyFingerPrint(); + if ( StringUtils.isBlank(name) ) { name=fingerprint; } + var descriptor = new PublicKeyDescriptor(name, fingerprint, publicKey, PublicKeySource.EXTERNAL); + FcliDataHelper.saveFile(publicKeyPath(fingerprint), descriptor, true); + return descriptor; + } + + public final PublicKeyDescriptor load(String nameOrFingerprint, boolean failIfNotFound) { + var result = FcliDataHelper.readFile(publicKeyPath(nameOrFingerprint), PublicKeyDescriptor.class, false); + if ( result==null ) { + result = stream().filter(d->d.getName().equals(nameOrFingerprint)) + .findFirst() + .orElseGet(()->{ + if ( !failIfNotFound ) { return null; } + throw new IllegalArgumentException("No public key found with name or fingerprint "+nameOrFingerprint); + }); + } + return result==null ? null : result.toBuilder().source(PublicKeySource.TRUSTSTORE).build(); + } + + public final PublicKeyDescriptor forFingerprint(String fingerprint, String... extraPublicKeys) { + PublicKeyDescriptor result = load(fingerprint, false); + // Try to locate public key for fingerprint from given extra public keys + if ( result==null && extraPublicKeys!=null ) { + for ( var extraPublicKey : extraPublicKeys ) { + if ( StringUtils.isNotBlank(extraPublicKey) ) { + var verifier = new Verifier(extraPublicKey); + if ( fingerprint.equals(verifier.publicKeyFingerPrint()) ) { + result = PublicKeyDescriptor.builder() + .fingerprint(fingerprint) + .name(null) + .publicKey(extraPublicKey) + .source(PublicKeySource.EXTERNAL) + .build(); + } + } + } + } + return result; + } + + public final PublicKeyDescriptor delete(String nameOrFingerprint) { + var descriptor = load(nameOrFingerprint, true); + var publicKeyPath = publicKeyPath(descriptor.getFingerprint()); + FcliDataHelper.deleteFile(publicKeyPath, false); + return descriptor; + } + + public final Stream stream() { + return FcliDataHelper.listFilesInDir(publicKeysPath(), true) + .map(path->load(path.toFile().getName(), false)) + .filter(Objects::nonNull); + } + + private static final Path publicKeyPath(String fingerprint) { + return publicKeysPath().resolve(fingerprint); + } + + private static final Path publicKeysPath() { + return FcliDataHelper.getFcliConfigPath().resolve("public-keys"); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/SignedTextReader.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/SignedTextReader.java new file mode 100644 index 0000000000..cb6b673320 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/SignedTextReader.java @@ -0,0 +1,103 @@ +/** + * 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.crypto.helper.impl; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureStatus; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureValidator; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignedTextDescriptor; +import com.fortify.cli.common.util.FileUtils; + +import lombok.SneakyThrows; + +public final class SignedTextReader { + public static final SignedTextReader INSTANCE = new SignedTextReader(); + private SignedTextReader() {} + public final SignedTextDescriptor load(InputStream is, Charset charset, boolean evaluateSignature) { + return load(FileUtils.readInputStreamAsString(is, charset), evaluateSignature); + } + + public final SignedTextDescriptor load(InputStream is, Charset charset, SignatureValidator signatureValidator) { + return load(FileUtils.readInputStreamAsString(is, charset), signatureValidator); + } + + /** + * Load a {@link SignedTextDescriptor} instance from the given signed or + * unsigned text. + * + * @param signedOrUnsignedText Either signed or unsigned text to be loaded. + * @param onInvalidSingature is invoked if there's no valid signature. If null, + * the signature status will not be evaluated. To evaluate signature status + * without performing any action on failure, simply pass d->{}. + * @return {@link SignedTextDescriptor} instance + */ + public final SignedTextDescriptor load(String signedOrUnsignedText, SignatureValidator signatureValidator) { + var onInvalidSingature = signatureValidator==null ? null : signatureValidator.getInvalidSignatureHandler(); + if ( onInvalidSingature==null ) { return load(signedOrUnsignedText, false); } + var extraPublicKeys = signatureValidator.getExtraPublicKeys(); + var descriptor = load(signedOrUnsignedText, true, extraPublicKeys); + if ( descriptor.getSignatureStatus()!=SignatureStatus.VALID ) { + onInvalidSingature.onInvalidSignature(descriptor); + } + return descriptor; + } + + @SneakyThrows + public final SignedTextDescriptor load(String signedOrUnsignedText, boolean evaluateSignature, String... extraPublicKeys) { + var elts = signedOrUnsignedText.split(String.valueOf(InternalSignatureUtil.FILE_SEPARATOR)); + if ( elts.length>2 ) { + throw new IllegalStateException("Input may contain only single Unicode File Separator character"); + } else if ( elts.length==1) { + return buildUnsignedDescriptor(elts[0]); + } else { + var signatureDescriptor = new ObjectMapper(new YAMLFactory()) + .readValue(elts[1], SignatureDescriptor.class); + return buildSignedDescriptor(signedOrUnsignedText, elts[0], signatureDescriptor, evaluateSignature, extraPublicKeys); + } + } + + private SignedTextDescriptor buildUnsignedDescriptor(String payload) { + return SignedTextDescriptor.builder() + .original(payload) + .payload(payload) + .signatureStatus(SignatureStatus.UNSIGNED) + .build(); + } + + private SignedTextDescriptor buildSignedDescriptor(String original, String payload, SignatureDescriptor signatureDescriptor, boolean evaluateSignatureStatus, String... extraPublicKeys) { + var signatureStatus = SignatureStatus.NOT_VERIFIED; + PublicKeyDescriptor publicKeyDescriptor = null; + if ( evaluateSignatureStatus ) { + var fingerprint = signatureDescriptor.getPublicKeyFingerprint(); + var expectedSignature = signatureDescriptor.getSignature(); + publicKeyDescriptor = PublicKeyTrustStore.INSTANCE.forFingerprint(fingerprint, extraPublicKeys); + signatureStatus = new Verifier(publicKeyDescriptor) + .verify(payload, StandardCharsets.UTF_8, expectedSignature); + } + return SignedTextDescriptor.builder() + .original(original) + .payload(payload) + .signatureDescriptor(signatureDescriptor) + .signatureStatus(signatureStatus) + .publicKeyDescriptor(publicKeyDescriptor) + .build(); + } + +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/Signer.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/Signer.java new file mode 100644 index 0000000000..b58000d2a1 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/Signer.java @@ -0,0 +1,134 @@ +/** + * 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.crypto.helper.impl; + +import java.io.File; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.security.AlgorithmParameters; +import java.security.Key; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +import com.fortify.cli.common.crypto.helper.impl.InternalSignatureUtil.DataSignatureUpdater; +import com.fortify.cli.common.crypto.helper.impl.InternalSignatureUtil.FileSignatureUpdater; +import com.fortify.cli.common.crypto.helper.impl.InternalSignatureUtil.ISignatureUpdater; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +@RequiredArgsConstructor +public final class Signer { + private final byte[] privateKey; + private final char[] passPhrase; + + public Signer(String pemOrBase64Key, char[] passPhrase) { + this(InternalSignatureUtil.parseKey(pemOrBase64Key), passPhrase); + } + + @SneakyThrows + public final PublicKey publicKey() { + var privateKey = (RSAPrivateCrtKey)createKey(); + var keySpec = new RSAPublicKeySpec(privateKey.getModulus(), privateKey.getPublicExponent()); + return InternalSignatureUtil.KEY_FACTORY.generatePublic(keySpec); + } + + @SneakyThrows + public final String publicKeyFingerprint() { + var encodedKey = publicKey().getEncoded(); + byte[] hash = MessageDigest.getInstance("SHA256").digest(encodedKey); + String hexKey = String.format("%064X", new BigInteger(1, hash)); + return hexKey; + } + + public final byte[] signAsBytes(File file) { + return signAsBytes(new FileSignatureUpdater(file)); + } + + public final byte[] signAsBytes(byte[] data) { + return signAsBytes(new DataSignatureUpdater(data)); + } + + public final byte[] signAsBytes(String data, Charset charset) { + return signAsBytes(new DataSignatureUpdater(data, charset)); + } + + public final String sign(File file) { + return sign(new FileSignatureUpdater(file)); + } + + public final String sign(Path path) { + return sign(new FileSignatureUpdater(path.toFile())); + } + + public final String sign(byte[] data) { + return sign(new DataSignatureUpdater(data)); + } + + public final String sign(String data, Charset charset) { + return sign(new DataSignatureUpdater(data, charset)); + } + + private final String sign(ISignatureUpdater updater) { + var signature = signAsBytes(updater); + return signature==null ? null : Base64.getEncoder().encodeToString(signature); + } + + @SneakyThrows + private final byte[] signAsBytes(ISignatureUpdater updater) { + Signature signature = createSignature(); + updater.updateSignature(signature); + return signature.sign(); + } + + @SneakyThrows + private final Signature createSignature() { + var key = createKey(); + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initSign(key); + return signature; + } + + /** This works for AES-encrypted keys on Java 21 */ + @SneakyThrows + private PrivateKey createKey() { + var pkcs8KeySpec = passPhrase==null + ? new PKCS8EncodedKeySpec(privateKey) + : decryptEncodedKeySpec(privateKey); + return InternalSignatureUtil.KEY_FACTORY.generatePrivate(pkcs8KeySpec); + } + + @SneakyThrows + private PKCS8EncodedKeySpec decryptEncodedKeySpec(byte[] bytes) { + EncryptedPrivateKeyInfo encryptPKInfo = new EncryptedPrivateKeyInfo(bytes); + Cipher cipher = Cipher.getInstance(encryptPKInfo.getAlgName()); + PBEKeySpec pbeKeySpec = new PBEKeySpec(passPhrase); + SecretKeyFactory secFac = SecretKeyFactory.getInstance(encryptPKInfo.getAlgName()); + Key pbeKey = secFac.generateSecret(pbeKeySpec); + AlgorithmParameters algParams = encryptPKInfo.getAlgParameters(); + cipher.init(Cipher.DECRYPT_MODE, pbeKey, algParams); + return encryptPKInfo.getKeySpec(cipher); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/TextSigner.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/TextSigner.java new file mode 100644 index 0000000000..fa78e7ea8e --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/TextSigner.java @@ -0,0 +1,104 @@ +/** + * 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.crypto.helper.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.PublicKey; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureMetadata; + +import lombok.SneakyThrows; + +public final class TextSigner { + private final Signer signer; + + public TextSigner(byte[] privateKey, char[] passPhrase) { + this.signer = new Signer(privateKey, passPhrase); + } + + public TextSigner(String pemOrBase64Key, char[] passPhrase) { + this(InternalSignatureUtil.parseKey(pemOrBase64Key), passPhrase); + } + + @SneakyThrows + public final void signAndWrite(Path payloadPath, Path outputPath, SignatureMetadata metadata) { + var content = sign(Files.readString(payloadPath), metadata); + Files.writeString(outputPath, content, StandardOpenOption.CREATE_NEW); + } + + @SneakyThrows + public final String sign(Path payloadPath, SignatureMetadata metadata) { + return sign(Files.readString(payloadPath), metadata); + } + + @SneakyThrows + public final String sign(String textToSign, SignatureMetadata metadata) { + var payload = readBytes(textToSign); + var fingerprint = signer.publicKeyFingerprint(); + var signature = signer.sign(payload); + + var signatureDescriptor = SignatureDescriptor.builder() + .signature(signature) + .publicKeyFingerprint(fingerprint) + .metadata(metadata) + .build(); + return generateOutput(payload, signatureDescriptor); + } + + private byte[] readBytes(String payload) throws IOException { + if ( payload.contains(String.valueOf(InternalSignatureUtil.FILE_SEPARATOR)) ) { + throw new IllegalStateException("Input file may not contain Unicode File Separator character \u001C"); + } + return payload.getBytes(StandardCharsets.UTF_8); + } + + @SneakyThrows + private final String generateOutput(byte[] payload, SignatureDescriptor signatureDescriptor) { + YAMLFactory factory = new YAMLFactory(); + try ( var os = new ByteArrayOutputStream(); + var generator = (YAMLGenerator)factory.createGenerator(os) ) { + os.write(payload); + os.write(InternalSignatureUtil.FILE_SEPARATOR); + os.write('\n'); + generator.configure(YAMLGenerator.Feature.MINIMIZE_QUOTES, true) + .configure(YAMLGenerator.Feature.WRITE_DOC_START_MARKER, true) + .setCodec(new ObjectMapper()) + .disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET) + .useDefaultPrettyPrinter(); + generator.writeObject(signatureDescriptor); + return os.toString(StandardCharsets.UTF_8); + } + } + + public final PublicKey publicKey() { + return signer.publicKey(); + } + + public final String publicKeyFingerprint() { + return signer.publicKeyFingerprint(); + } + + public final void writePublicKey(Path publicKeyPath) { + InternalSignatureUtil.writePem("PUBLIC KEY", publicKey().getEncoded(), publicKeyPath); + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/Verifier.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/Verifier.java new file mode 100644 index 0000000000..a50a67a758 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/crypto/helper/impl/Verifier.java @@ -0,0 +1,104 @@ +/** + * 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.crypto.helper.impl; + +import java.io.File; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignatureStatus; +import com.fortify.cli.common.crypto.helper.impl.InternalSignatureUtil.DataSignatureUpdater; +import com.fortify.cli.common.crypto.helper.impl.InternalSignatureUtil.FileSignatureUpdater; +import com.fortify.cli.common.crypto.helper.impl.InternalSignatureUtil.ISignatureUpdater; +import com.fortify.cli.common.util.StringUtils; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +@RequiredArgsConstructor +public final class Verifier { + private final byte[] publicKey; + + public Verifier(String pemOrBase64Key) { + this(StringUtils.isBlank(pemOrBase64Key) + ? null + : InternalSignatureUtil.parseKey(pemOrBase64Key)); + } + + public Verifier(PublicKeyDescriptor publicKeyDescriptor) { + this(publicKeyDescriptor==null + ? null + : publicKeyDescriptor.getPublicKey()); + } + + public final SignatureStatus verify(File file, String expectedSignature) { + return verify(new FileSignatureUpdater(file), expectedSignature); + } + + public final SignatureStatus verify(byte[] data, String expectedSignature) { + return verify(new DataSignatureUpdater(data), expectedSignature); + } + + public final SignatureStatus verify(String data, Charset charset, String expectedSignature) { + return verify(new DataSignatureUpdater(data, charset), expectedSignature); + } + + @SneakyThrows + private final SignatureStatus verify(ISignatureUpdater updater, String expectedSignature) { + if ( publicKey()==null ) { return SignatureStatus.NO_PUBLIC_KEY; } + Signature signature = createSignature(); + updater.updateSignature(signature); + return verifySignature(signature, expectedSignature); + } + + @SneakyThrows + private final SignatureStatus verifySignature(Signature signature, String expectedSignature) { + if(signature.verify(Base64.getDecoder().decode(expectedSignature))) { + return SignatureStatus.VALID; + } else { + return SignatureStatus.MISMATCH; + } + } + + @SneakyThrows + private final Signature createSignature() { + PublicKey pub = publicKey(); + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initVerify(pub); + return signature; + } + + @SneakyThrows + private PublicKey publicKey() { + if ( publicKey==null ) { return null; } + X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey); + PublicKey pub = InternalSignatureUtil.KEY_FACTORY.generatePublic(spec); + return pub; + } + + @SneakyThrows + public String publicKeyFingerPrint() { + var publicKey = publicKey(); + if ( publicKey==null ) { return null; } + var encodedKey = publicKey.getEncoded(); + byte[] hash = MessageDigest.getInstance("SHA256").digest(encodedKey); + String hexKey = String.format("%064X", new BigInteger(1, hash)); + return hexKey; + } +} \ 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..d143a41cdb --- /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..d6004ab4ad --- /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(); + } + + public 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..004895c20f 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; @@ -64,20 +67,6 @@ public static final R evaluateSpelExpression(JsonNode input, String expressi return SpelEvaluator.JSON_GENERIC.evaluate(expression, input, returnClass); } - public static final ObjectNode getFirstObjectNode(JsonNode input) { - if ( input instanceof ObjectNode ) { - return (ObjectNode)input; - } else if ( input instanceof ArrayNode ) { - ArrayNode array = (ArrayNode)input; - if ( array.size()==0 ) { return null; } - JsonNode node = array.get(0); - if ( node instanceof ObjectNode ) { - return (ObjectNode)node; - } - } - throw new IllegalArgumentException("Input must be an ObjectNode or array of ObjectNodes"); - } - public static final Iterable iterable(ArrayNode arrayNode) { Iterator iterator = arrayNode.iterator(); return () -> iterator; @@ -87,6 +76,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 +144,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/json/JsonNodeHolder.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JsonNodeHolder.java index 1d85179726..a949c9fffe 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JsonNodeHolder.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/json/JsonNodeHolder.java @@ -38,14 +38,16 @@ public JsonNode asJsonNode() { @Override @JsonIgnore public ObjectNode asObjectNode() { - if ( !(jsonNode instanceof ObjectNode) ) { throw new IllegalStateException("JsonNode is not an instance of ObjectNode"); } - return (ObjectNode)jsonNode; + var node = asJsonNode(); + if ( !(node instanceof ObjectNode) ) { throw new IllegalStateException("JsonNode is not an instance of ObjectNode"); } + return (ObjectNode)node; } @Override @JsonIgnore public ArrayNode asArrayNode() { - if ( !(jsonNode instanceof ArrayNode) ) { throw new IllegalStateException("JsonNode is not an instance of ArrayNode"); } - return (ArrayNode)jsonNode; + var node = asJsonNode(); + if ( !(node instanceof ArrayNode) ) { throw new IllegalStateException("JsonNode is not an instance of ArrayNode"); } + return (ArrayNode)node; } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/AbstractCheckCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/AbstractCheckCommand.java new file mode 100644 index 0000000000..0f66aaddd2 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/AbstractCheckCommand.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.output.cli.cmd; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.output.cli.mixin.IOutputHelper; +import com.fortify.cli.common.output.writer.ISingularSupplier; + +public abstract class AbstractCheckCommand extends AbstractRunnableCommand implements ISingularSupplier { + @Override + public final Integer call() { + initMixins(); + IOutputHelper outputHelper = getOutputHelper(); + if ( isPass() ) { + outputHelper.write(getPassResult()); + return 0; + } else { + outputHelper.write(getFailResult()); + return 1; + } + } + + @Override + public final boolean isSingular() { + return true; + } + + protected abstract IOutputHelper getOutputHelper(); + protected abstract boolean isPass(); + + protected String getPropertyName() { return "result"; } + protected String getPassValue() { return "Pass"; } + protected String getFailValue() { return "Fail"; } + protected ObjectNode getPassResult() { + return JsonHelper.getObjectMapper().createObjectNode() + .put(getPropertyName(), getPassValue()); + } + protected ObjectNode getFailResult() { + return JsonHelper.getObjectMapper().createObjectNode() + .put(getPropertyName(), getFailValue()); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/AbstractOutputCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/AbstractOutputCommand.java index 88b5c33d60..2fe69ec604 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/AbstractOutputCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/AbstractOutputCommand.java @@ -19,12 +19,12 @@ import com.fortify.cli.common.output.cli.mixin.IOutputHelper; import com.fortify.cli.common.output.writer.ISingularSupplier; -public abstract class AbstractOutputCommand extends AbstractRunnableCommand implements Runnable, ISingularSupplier { +public abstract class AbstractOutputCommand extends AbstractRunnableCommand implements ISingularSupplier, IOutputHelperSupplier { private static final List> supportedInterfaces = Arrays.asList( IBaseRequestSupplier.class, IJsonNodeSupplier.class); @Override - public final void run() { + public final Integer call() { initMixins(); IOutputHelper outputHelper = getOutputHelper(); if ( isInstance(IBaseRequestSupplier.class) ) { @@ -34,6 +34,7 @@ public final void run() { } else { throw new IllegalStateException(this.getClass().getName()+" must implement exactly one of "+supportedInterfaces); } + return 0; } private boolean isInstance(Class clazz) { @@ -43,5 +44,5 @@ private boolean isInstance(Class clazz) { .noneMatch(c->c.isAssignableFrom(this.getClass())); } - protected abstract IOutputHelper getOutputHelper(); + public abstract IOutputHelper getOutputHelper(); } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/IOutputHelperSupplier.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/IOutputHelperSupplier.java new file mode 100644 index 0000000000..643a50c0d7 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/cmd/IOutputHelperSupplier.java @@ -0,0 +1,25 @@ +/** + * 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.output.cli.cmd; + +import com.fortify.cli.common.output.cli.mixin.IOutputHelper; + +/** + * + * @author Ruud Senden + */ +public interface IOutputHelperSupplier { + + IOutputHelper getOutputHelper(); + +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/mixin/AbstractOutputHelperMixin.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/mixin/AbstractOutputHelperMixin.java index 3e55dfbfaa..2a50641e5b 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/mixin/AbstractOutputHelperMixin.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/mixin/AbstractOutputHelperMixin.java @@ -162,6 +162,9 @@ protected final UnirestInstance getUnirestInstance() { * @param cmd */ protected final void addRecordTransformersForCommand(StandardOutputConfig standardOutputConfig, Object cmd) { + for ( var mixin : commandHelper.getCommandSpec().mixins().values() ) { + addRecordTransformersFromObject(standardOutputConfig, mixin.userObject()); + } addRecordTransformersFromObject(standardOutputConfig, getProductHelper()); addRecordTransformersFromObject(standardOutputConfig, cmd); addCommandActionResultRecordTransformer(standardOutputConfig, cmd); @@ -187,6 +190,7 @@ protected final void addInputTransformersForCommand(StandardOutputConfig standar for ( var mixin : commandHelper.getCommandSpec().mixins().values() ) { addInputTransformersFromObject(standardOutputConfig, mixin.userObject()); } + addInputTransformersFromObject(standardOutputConfig, getProductHelper()); addInputTransformersFromObject(standardOutputConfig, cmd); } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/mixin/OutputHelperMixins.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/mixin/OutputHelperMixins.java index bf7bbf32a2..7cb2b2c3d7 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/mixin/OutputHelperMixins.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/cli/mixin/OutputHelperMixins.java @@ -75,7 +75,12 @@ public static class DetailsWithQuery extends Other { @Getter @Mixin private OutputWriterWithQueryFactoryMixin outputWriterFactory; @Getter private StandardOutputConfig basicOutputConfig = StandardOutputConfig.details(); } - + + public static class Check extends Other { + public static final String CMD_NAME = "check"; + @Getter @Mixin private StandardOutputWriterFactoryMixin outputWriterFactory; + @Getter private StandardOutputConfig basicOutputConfig = StandardOutputConfig.table_plain(); + } public static class Add extends TableNoQuery { public static final String CMD_NAME = "add"; diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/output/standard/StandardOutputConfig.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/output/standard/StandardOutputConfig.java index cd0a2292fa..80e1e8ffd7 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/output/standard/StandardOutputConfig.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/output/standard/StandardOutputConfig.java @@ -85,6 +85,10 @@ public static final StandardOutputConfig table() { return new StandardOutputConfig().defaultFormat(OutputFormat.table); } + public static final StandardOutputConfig table_plain() { + return new StandardOutputConfig().defaultFormat(OutputFormat.table_plain); + } + public static final StandardOutputConfig tree() { return new StandardOutputConfig().defaultFormat(OutputFormat.tree); } 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..5873f25356 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 @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,6 +62,8 @@ public class StandardOutputWriter implements IOutputWriter { private final CommandSpec commandSpec; private final IOutputOptions outputOptions; private final IMessageResolver messageResolver; + private static IRecordWriter recordCollector; + private static boolean suppressOutput; public StandardOutputWriter(CommandSpec commandSpec, IOutputOptions outputOptions, StandardOutputConfig defaultOutputConfig) { // Make sure that we get the CommandSpec for the actual command being invoked, @@ -72,6 +75,24 @@ public StandardOutputWriter(CommandSpec commandSpec, IOutputOptions outputOption this.messageResolver = new CommandSpecMessageResolver(this.commandSpec); } + public static final void collectRecords(Consumer consumer, boolean suppressOutput) { + final var oldRecordCollector = StandardOutputWriter.recordCollector; + final var oldSuppressOutput = StandardOutputWriter.suppressOutput; + StandardOutputWriter.suppressOutput = suppressOutput; + StandardOutputWriter.recordCollector = new IRecordWriter() { + @Override + public void writeRecord(ObjectNode record) { + consumer.accept(record); + } + + @Override + public void close() { + StandardOutputWriter.recordCollector = oldRecordCollector; + StandardOutputWriter.suppressOutput = oldSuppressOutput; + } + }; + } + /** * Write the given {@link JsonNode} to the configured output(s) */ @@ -168,13 +189,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)); } /** @@ -290,7 +306,8 @@ protected JsonNode applyRecordOutputFilters(OutputFormat outputFormat, JsonNode * */ private final class OutputAndVariableRecordWriter implements IRecordWriter { - private final OutputRecordWriter ouputRecordWriter = new OutputRecordWriter(); + private final IRecordWriter outputRecordWriter = createOutputRecordWriter(); + private final IRecordWriter recordCollector = StandardOutputWriter.recordCollector; private final VariableRecordWriter variableRecordWriter = new VariableRecordWriter(); /** @@ -299,7 +316,8 @@ private final class OutputAndVariableRecordWriter implements IRecordWriter { */ @Override public void writeRecord(ObjectNode record) { - ouputRecordWriter.writeRecord(record); + if ( outputRecordWriter!=null ) {outputRecordWriter.writeRecord(record);} + if ( recordCollector!=null ) {recordCollector.writeRecord(record);} if ( variableRecordWriter.isEnabled() ) { variableRecordWriter.writeRecord(record); } @@ -311,11 +329,16 @@ public void writeRecord(ObjectNode record) { */ @Override public void close() { - ouputRecordWriter.close(); + if ( outputRecordWriter!=null ) {outputRecordWriter.close();} + if ( recordCollector!=null ) {recordCollector.close();} if ( variableRecordWriter.isEnabled() ) { variableRecordWriter.close(); } } + + private final IRecordWriter createOutputRecordWriter() { + return StandardOutputWriter.suppressOutput ? null : new OutputRecordWriter(); + } } /** diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/record/AbstractFormattedRecordWriter.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/record/AbstractFormattedRecordWriter.java index 3d638e0f94..2b3b7efccd 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/record/AbstractFormattedRecordWriter.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/record/AbstractFormattedRecordWriter.java @@ -16,6 +16,8 @@ import java.util.Arrays; import java.util.List; +import org.springframework.expression.spel.SpelEvaluationException; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -90,8 +92,12 @@ private static final ObjectNode applyOptionalRecordFlattenTransformation(OutputF } private static final JsonNode evaluateValue(ObjectNode record, String path) { - JsonNode result = JsonHelper.evaluateSpelExpression(record, path, JsonNode.class); - return result!=null ? result : NA_NODE; + try { + JsonNode result = JsonHelper.evaluateSpelExpression(record, path, JsonNode.class); + return result!=null ? result : NA_NODE; + } catch ( SpelEvaluationException e ) { + return NA_NODE; + } } private static final List getFieldPaths(String options) { diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/record/yaml/YamlRecordWriter.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/record/yaml/YamlRecordWriter.java index dbabb238d2..9abc078182 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/record/yaml/YamlRecordWriter.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/output/writer/record/yaml/YamlRecordWriter.java @@ -33,7 +33,9 @@ public YamlRecordWriter(RecordWriterConfig config) { private YAMLGenerator getGenerator() { if ( generator==null ) { YAMLFactory factory = new YAMLFactory(); - this.generator = (YAMLGenerator)factory.createGenerator(getWriter()) + this.generator = (YAMLGenerator)factory.createGenerator(getWriter()); + generator.configure(YAMLGenerator.Feature.MINIMIZE_QUOTES, true) + .configure(YAMLGenerator.Feature.WRITE_DOC_START_MARKER, false) .setCodec(new ObjectMapper()) .disable(Feature.AUTO_CLOSE_TARGET); if ( getConfig().isPretty() ) generator = (YAMLGenerator)generator.useDefaultPrettyPrinter(); diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/cli/mixin/ProgressWriterFactoryMixin.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/cli/mixin/ProgressWriterFactoryMixin.java index e5c1f37189..470f8c8428 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/cli/mixin/ProgressWriterFactoryMixin.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/cli/mixin/ProgressWriterFactoryMixin.java @@ -28,6 +28,17 @@ public class ProgressWriterFactoryMixin { private ProgressWriterType type; public final IProgressWriterI18n create() { - return new ProgressWriterI18n(type, commandHelper.getMessageResolver()); + return create(this.type); + } + + public final IProgressWriterI18n overrideAutoIfNoConsole(ProgressWriterType overrideType) { + var newType = System.console()==null && type==ProgressWriterType.auto + ? overrideType + : this.type; + return create(newType); + } + + private IProgressWriterI18n create(ProgressWriterType progressWriterType) { + return new ProgressWriterI18n(progressWriterType, commandHelper.getMessageResolver()); } } 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 009b03ebce..2dd21410b6 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 @@ -23,7 +23,8 @@ public enum ProgressWriterType { auto(ProgressWriterType::auto), none(NoProgressWriter::new), - simple(SimpleProgressWriter::new), + simple(SimpleProgressWriter::new), + stderr(SimpleStdErrProgressWriter::new), single_line(SingleLineProgressWriter::new), ansi(AnsiProgressWriter::new); @@ -54,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 { @@ -77,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"; @@ -89,6 +98,26 @@ public void writeProgress(String message, Object... args) { public void clearProgress() {} } + private static final class SimpleStdErrProgressWriter extends AbstractProgressWriter { + @Override + public boolean isMultiLineSupported() { + return true; + } + + @Override + public void writeProgress(String message, Object... args) { + String formattedMessage = format(message, args); + if ( formattedMessage.indexOf('\n') > 0 ) { + // Add extra newline to separate multi-line blocks + formattedMessage += "\n"; + } + System.err.println(formattedMessage); + } + + @Override + public void clearProgress() {} + } + private static final class SingleLineProgressWriter extends AbstractProgressWriter { private static final String LINE_START = "\r"; private int lastNumberOfChars; @@ -100,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(); } @@ -127,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 9133764080..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 @@ -14,13 +14,14 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Function; import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.cli.util.EnvSuffix; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; import com.fortify.cli.common.output.cli.cmd.IBaseRequestSupplier; -import com.fortify.cli.common.output.product.IProductHelperSupplier; +import com.fortify.cli.common.output.product.IProductHelper; import com.fortify.cli.common.output.transform.IInputTransformer; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.rest.paging.INextPageUrlProducer; @@ -28,6 +29,7 @@ import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; import com.fortify.cli.common.util.DisableTest; 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; @@ -41,17 +43,11 @@ /** * Abstract base class for 'fcli rest call' commands. Concrete implementations must - * implement the various abstract methods. As dictated by the {@link IProductHelperSupplier}, - * implementations must implement the {@link IProductHelperSupplier#getProductHelper()} method, - * but please note that the provided product helper may not implement any of the - * {@link IInputTransformer}, {@link IRecordTransformer}, {@link INextPageUrlProducerSupplier} - * or {@link INextPageUrlProducer} as this would break the --no-transform and --no-paging options. - * Instead, subclasses should implement the corresponding _* methods defined in this class, - * to enable/disable paging and transformations on demand. + * implement the various abstract methods. * * @author Ruud Senden */ -public abstract class AbstractRestCallCommand extends AbstractOutputCommand implements IBaseRequestSupplier, IProductHelperSupplier, IInputTransformer, IRecordTransformer, INextPageUrlProducerSupplier { +public abstract class AbstractRestCallCommand extends AbstractOutputCommand implements IBaseRequestSupplier, IUnirestInstanceSupplier, IInputTransformer, IRecordTransformer, INextPageUrlProducerSupplier { @EnvSuffix("URI") @Parameters(index = "0", arity = "1..1", descriptionKey = "api.uri") String uri; @Option(names = {"--request", "-X"}, required = false, defaultValue = "GET") @@ -77,13 +73,17 @@ private static class TransformArgGroup { @Override public HttpRequest getBaseRequest() { - if ( getProductHelper() instanceof IUnirestInstanceSupplier ) { - UnirestInstance unirest = ((IUnirestInstanceSupplier)getProductHelper()).getUnirestInstance(); - return prepareRequest(unirest); - } - throw new RuntimeException("Class doesn't implement IUnirestInstanceSupplier: "+getProductHelper().getClass().getName()); + return prepareRequest(getUnirestInstance()); } + @Override + public final UnirestInstance getUnirestInstance() { + return getUnirestInstanceSupplier().getUnirestInstance(); + } + + protected abstract IUnirestInstanceSupplier getUnirestInstanceSupplier(); + protected abstract IProductHelper getProductHelper(); + @Override public boolean isSingular() { return false; @@ -116,9 +116,15 @@ public final INextPageUrlProducer getNextPageUrlProducer() { return result; } - protected abstract JsonNode _transformRecord(JsonNode input); - protected abstract JsonNode _transformInput(JsonNode input); - protected abstract INextPageUrlProducer _getNextPageUrlProducer(); + private final JsonNode _transformRecord(JsonNode input) { + return applyOnProductHelper(IRecordTransformer.class, t->t.transformRecord(input), input); + } + private final JsonNode _transformInput(JsonNode input) { + return applyOnProductHelper(IInputTransformer.class, t->t.transformInput(input), input); + } + private final INextPageUrlProducer _getNextPageUrlProducer() { + return applyOnProductHelper(INextPageUrlProducerSupplier.class, s->s.getNextPageUrlProducer(), null); + } @SneakyThrows protected final HttpRequest prepareRequest(UnirestInstance unirest) { @@ -139,4 +145,8 @@ protected final HttpRequest prepareRequest(UnirestInstance unirest) { return request; } + private final R applyOnProductHelper(Class type, Function f, R defaultValue) { + return JavaHelper.as(getProductHelper(), type).map(f).orElse(defaultValue); + } + } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/cli/cmd/AbstractWaitForCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/cli/cmd/AbstractWaitForCommand.java index 77a6b18760..838436bdf3 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/cli/cmd/AbstractWaitForCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/cli/cmd/AbstractWaitForCommand.java @@ -14,7 +14,6 @@ import com.fortify.cli.common.cli.cmd.AbstractRunnableCommand; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; -import com.fortify.cli.common.output.product.IProductHelperSupplier; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.common.output.writer.ISingularSupplier; import com.fortify.cli.common.rest.cli.mixin.StandardWaitHelperProgressMonitorMixin; @@ -28,24 +27,26 @@ import lombok.Getter; import picocli.CommandLine.Mixin; -public abstract class AbstractWaitForCommand extends AbstractRunnableCommand implements IActionCommandResultSupplier, IProductHelperSupplier, ISingularSupplier, Runnable { +public abstract class AbstractWaitForCommand extends AbstractRunnableCommand implements IActionCommandResultSupplier, IUnirestInstanceSupplier, ISingularSupplier { @Getter @Mixin private OutputHelperMixins.WaitFor outputHelper; @Mixin private WaitHelperControlPropertiesMixin controlProperties; @Mixin private WaitHelperWaitTypeMixin waitTypeSupplier; @Mixin StandardWaitHelperProgressMonitorMixin progressMonitorMixin; @Override - public void run() { + public Integer call() { initMixins(); - var productHelper = getProductHelper(); - if ( productHelper instanceof IUnirestInstanceSupplier ) { - UnirestInstance unirest = ((IUnirestInstanceSupplier)productHelper).getUnirestInstance(); - wait(unirest); - } else { - throw new RuntimeException("Class doesn't implement IUnirestInstanceSupplier: "+productHelper.getClass().getName()); - } + wait(getUnirestInstance()); + return 0; } + @Override + public final UnirestInstance getUnirestInstance() { + return getUnirestInstanceSupplier().getUnirestInstance(); + } + + protected abstract IUnirestInstanceSupplier getUnirestInstanceSupplier(); + private void wait(UnirestInstance unirest) { configure(unirest, WaitHelper.builder() 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/rest/unirest/UnexpectedHttpResponseException.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/unirest/UnexpectedHttpResponseException.java index 12c76fdd94..7611f32ebc 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/unirest/UnexpectedHttpResponseException.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/unirest/UnexpectedHttpResponseException.java @@ -12,11 +12,14 @@ *******************************************************************************/ package com.fortify.cli.common.rest.unirest; +import com.formkiq.graalvm.annotations.Reflectable; + import kong.unirest.HttpRequestSummary; import kong.unirest.HttpResponse; import kong.unirest.UnirestException; import lombok.Getter; +@Reflectable // Required for calling methods like getMessage() in fcli actions public final class UnexpectedHttpResponseException extends UnirestException { private static final long serialVersionUID = 1L; @Getter private int status = 200; diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/unirest/UnirestHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/unirest/UnirestHelper.java index c0b1120438..70058e9980 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/unirest/UnirestHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/rest/unirest/UnirestHelper.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 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..b62190663c --- /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..8a994dc590 --- /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 96% 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..e41e21b294 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 @@ -19,8 +19,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.crypto.helper.EncryptionHelper; import com.fortify.cli.common.util.DateTimePeriodHelper; -import com.fortify.cli.common.util.EncryptionHelper; import com.fortify.cli.common.util.EnvHelper; import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.common.variable.FcliVariableHelper; @@ -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..496f578f01 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpression.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * (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; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +/** + *

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 TemplateExpressionSerializer}.

+ */ +@JsonDeserialize(using = SimpleExpressionDeserializer.class) +@JsonSerialize(using = SimpleExpressionSerializer.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/SimpleExpressionSerializer.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpressionSerializer.java new file mode 100644 index 0000000000..9fe3ee0d33 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/SimpleExpressionSerializer.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * (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 com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.formkiq.graalvm.annotations.Reflectable; + +/** + * This Jackson deserializer allows parsing String values into an + * SpEL Expression object. + */ +@Reflectable +public final class SimpleExpressionSerializer extends StdSerializer { + private static final long serialVersionUID = 1L; + protected SimpleExpressionSerializer() { + super(SimpleExpression.class); + } + + @Override + public void serialize(SimpleExpression value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(value.getExpressionString()); + } + + +} 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..a722fbb874 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpression.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * (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; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +/** + *

This is a simple wrapper class for a Spring {@link Expression} + * instance. It's main use is in combination with + * {@link TemplateExpressionSerializer} 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) +@JsonSerialize(using = TemplateExpressionSerializer.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/TemplateExpressionSerializer.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpressionSerializer.java new file mode 100644 index 0000000000..51d9f67309 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/spring/expression/wrapper/TemplateExpressionSerializer.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * (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 com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.formkiq.graalvm.annotations.Reflectable; + +/** + * This Jackson deserializer allows parsing String values into an + * SpEL Expression object. + */ +@Reflectable +public final class TemplateExpressionSerializer extends StdSerializer { + private static final long serialVersionUID = 1L; + protected TemplateExpressionSerializer() { + super(TemplateExpression.class); + } + + @Override + public void serialize(TemplateExpression value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(value.getExpressionString()); + } +} 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/Break.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/Break.java new file mode 100644 index 0000000000..f74d7079ec --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/Break.java @@ -0,0 +1,33 @@ +/** + * 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.util; + +/** + * This enum is generally used in loops and other types of processing + * to indicate whether processing should continue ({@link #FALSE} or + * should be stopped ({@link #TRUE}. For code readability, this is + * much more clear than a boolean, as potentially some code could + * interpret {@link Boolean#TRUE} as 'continue', whereas other code + * could interpret {@link Boolean#TRUE} as 'break'. + */ +public enum Break { + FALSE, TRUE; + + public boolean doBreak() { + return this==Break.TRUE; + } + + public boolean doContinue() { + return this==Break.FALSE; + } +} \ No newline at end of file diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliBuildPropertiesHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliBuildPropertiesHelper.java index 8c705a2dd7..da3671c328 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliBuildPropertiesHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliBuildPropertiesHelper.java @@ -27,6 +27,11 @@ public static final Properties getBuildProperties() { return buildProperties; } + public static final boolean isDevelopmentRelease() { + var version = getFcliVersion(); + return version.startsWith("0.") || version.equals("unknown"); + } + public static final String getFcliProjectName() { return buildProperties.getProperty("projectName", "fcli"); } @@ -49,6 +54,10 @@ public static final Date getFcliBuildDate() { return null; } + public static final String getFcliActionSchemaVersion() { + return buildProperties.getProperty("actionSchemaVersion", "unknown"); + } + public static final String getFcliBuildInfo() { return String.format("%s version %s, built on %s" , FcliBuildPropertiesHelper.getFcliProjectName() @@ -58,7 +67,7 @@ public static final String getFcliBuildInfo() { private static final Properties loadProperties() { final Properties p = new Properties(); - try (final InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("com/fortify/cli/app/fcli-build.properties")) { + try (final InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("com/fortify/cli/common/fcli-build.properties")) { if ( stream!=null ) { p.load(stream); } } catch ( IOException ioe ) { throw new RuntimeException("Error reading fcli-build.properties from classpath", ioe); diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliDataHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliDataHelper.java index aaa36a2d15..a1793d3918 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliDataHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FcliDataHelper.java @@ -30,6 +30,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fortify.cli.common.crypto.helper.EncryptionHelper; import com.fortify.cli.common.json.JsonHelper; public class FcliDataHelper { 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..9f8da26310 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 @@ -41,6 +41,11 @@ public final class FileUtils { private FileUtils() {} + @SneakyThrows + public static final InputStream getInputStream(Path path) { + return !Files.exists(path) ? null : Files.newInputStream(path); + } + public static final InputStream getResourceInputStream(String resourcePath) { return Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath); } @@ -50,6 +55,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/OutputCollector.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/OutputCollector.java new file mode 100644 index 0000000000..b6b6cbb8d4 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/OutputCollector.java @@ -0,0 +1,50 @@ +/** + * 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.util; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.util.concurrent.Callable; + +import lombok.Data; + +public class OutputCollector { + public static final Output collectOutput(Charset charset, Callable callable) { + var oldOut = System.out; + var oldErr = System.err; + try ( var outStream = new ByteArrayOutputStream(); + var errStream = new ByteArrayOutputStream(); + var outPS = new PrintStream(outStream); + var errPS = new PrintStream(errStream) ) { + System.setOut(outPS); + System.setErr(errPS); + int exitCode = callable.call(); + System.out.flush(); + System.err.flush(); + return new Output(exitCode, outStream.toString(charset), errStream.toString(charset)); + } catch ( Exception e ) { + throw new RuntimeException("Error executing", e); + } finally { + System.setOut(oldOut); + System.setErr(oldErr); + } + } + + @Data + public static final class Output { + private final int exitCode; + private final String out; + private final String err; + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVer.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVer.java new file mode 100644 index 0000000000..36845e12b3 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVer.java @@ -0,0 +1,153 @@ +/** + * 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.util; + +import java.lang.module.ModuleDescriptor.Version; +import java.text.MessageFormat; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.Getter; + +/** + * This class represents an optional semantic version, based on the + * version string passed to the constructor. This version string doesn't + * need to be a proper semantic version, in which case most methods will + * return an empty {@link Optional}, and comparison methods regard a + * non-proper semantic version as not equal to/less than a true semantic + * version. + * + * @author Ruud Senden + */ +@Getter +public final class SemVer { + private static final Pattern semverPattern = Pattern.compile("([1-9]\\d*)\\.(\\d+)\\.(\\d+)(?:-(.*))?"); + private final String semver; + private final boolean properSemver; + private final Optional major; + private final Optional minor; + private final Optional patch; + private final Optional label; + private final Optional majorMinor; + private final Optional majorMinorPatch; + private final Optional version; + + public SemVer(String semver) { + this.semver = semver==null?"":semver; + var matcher = semverPattern.matcher(this.semver); + this.properSemver = matcher.matches(); + this.major = optionalFormat(matcher, "{1}"); + this.minor = optionalFormat(matcher, "{2}"); + this.patch = optionalFormat(matcher, "{3}"); + this.label = optionalFormat(matcher, "{4}"); + this.majorMinor = optionalFormat(matcher, "{1}.{2}"); + this.majorMinorPatch = optionalFormat(matcher, "{1}.{2}.{3}"); + this.version = Optional.ofNullable(!matcher.matches() ? null : Version.parse(semver)); + } + + /** + * @return Minimal compatible version, i.e., major.0.0 + */ + public final Optional getMinimalCompatible() { + return major.map(_major->String.format("%s.0.0", _major)); + } + + /** + * @return Maximal compatible version, i.e., major.minor.* + */ + public final Optional getMaximalCompatibleString() { + return majorMinor.map(_majorMinor->String.format("%s.*", _majorMinor)); + } + + /** + * @return String describing minimal and maximal compatible versions + */ + public final Optional getCompatibleVersionsString() { + var _minCompat = getMinimalCompatible(); + var _maxCompat = getMaximalCompatibleString(); + return _minCompat.equals(getMajorMinorPatch()) + ? _maxCompat + : Optional.ofNullable(!properSemver + ? null + : String.format("%s-%s", _minCompat.get(), _maxCompat.get())); + } + + /** + * + * @param other semver string to compare + * @return true, unless either this or other isn't a proper semver, or if major + * version is different, or if this minor is less than other minor + */ + public final boolean isCompatibleWith(String other) { + return isCompatibleWith(new SemVer(other)); + } + + /** + * @param other semver to compare + * @return true, unless either this or other isn't a proper semver, or if major + * version is different, or if this minor is less than other minor + */ + public final boolean isCompatibleWith(SemVer other) { + if ( !this.properSemver || !other.properSemver ) { return false; } + if ( !this.major.equals(other.major) ) { return false; } + if ( Integer.parseInt(this.minor.orElseThrow()) optionalFormat(Matcher matcher, String format) { + return Optional.ofNullable(!matcher.matches() + ? null + : new MessageFormat(format).format(new Object[] { + matcher.group(0), matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(4) + })); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVerHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVerHelper.java deleted file mode 100644 index 028d65487e..0000000000 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/SemVerHelper.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 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.util; - -import java.lang.module.ModuleDescriptor.Version; -import java.util.Optional; -import java.util.regex.Pattern; - -public final class SemVerHelper { - private static final Pattern semverPattern = Pattern.compile("([1-9]\\d*)\\.(\\d+)\\.(\\d+)(?:-(.*))?"); - /** - * Loosely compare two semantic versions, returning -1 if first semver is lower than - * the second, 0 if they are the same, or 1 if first semver is higher than the - * second. Null, blank, or non-semver values are always considered lower than - * true semvers. - * - * @param semver1 - * @param semver2 - * @return - */ - public static final int compare(String semver1, String semver2) { - var semver1Matcher = semverPattern.matcher(semver1==null?"":semver1); - var semver2Matcher = semverPattern.matcher(semver2==null?"":semver2); - if ( (semver1==null && semver2==null) || semver1.equals(semver2) ) { - return 0; - } else if ( !semver1Matcher.matches() && !semver2Matcher.matches() ) { - return semver1.compareTo(semver2); - } else if ( semver1Matcher.matches() && !semver2Matcher.matches() ) { - return 1; - } else if ( !semver1Matcher.matches() && semver2Matcher.matches() ) { - return -1; - } else { - var version1 = Version.parse(semver1); - var version2 = Version.parse(semver2); - return version1.compareTo(version2); - } - } - - public static final Optional getMajor(String semver) { - var matcher = semverPattern.matcher(semver); - return !matcher.matches() ? Optional.empty() : Optional.of(matcher.group(1)); - } - - public static final Optional getMajorMinor(String semver) { - var matcher = semverPattern.matcher(semver); - return !matcher.matches() - ? Optional.empty() - : Optional.of(String.format("%s.%s", matcher.group(1), matcher.group(2))); - } -} 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..91899e7350 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 @@ -12,6 +12,12 @@ *******************************************************************************/ package com.fortify.cli.common.util; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.formkiq.graalvm.annotations.Reflectable; + +@Reflectable // Required for using these functions in fcli actions public class StringUtils { private StringUtils() {} @@ -28,16 +34,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 +57,22 @@ 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 final 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) + "..."; } } + + public static final String indent(String str, String indentStr) { + if ( str==null ) { return null; } + return Stream.of(str.split("\n")).collect(Collectors.joining("\n"+indentStr, indentStr, "")); + } + + // For use in SpEL expressions + public static final String fmt(String fmt, Object... input) { + return String.format(fmt, input); + } } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/ZipHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/ZipHelper.java new file mode 100644 index 0000000000..65ad2a4c66 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/ZipHelper.java @@ -0,0 +1,113 @@ +/** + * 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.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Supplier; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import lombok.SneakyThrows; + +public class ZipHelper { + /** + * Helper method to process individual zip entries from a zip file + * loaded from the given zipFileInputStream, calling the + * {@link IZipEntryProcessor#process(ZipInputStream, ZipEntry)} + * method for each zip entry. Processing is terminated if the + * processor returns {@link Break#TRUE}. + * + * @param zipFileInputStream The {@link InputStream} to be read as a zip file. + * @param processor The {@link IZipEntryProcessor} used to process each zip entry. + * @return {@link Break#TRUE} if the processor returned {@link Break#TRUE} for any + * entry, {@link Break#FALSE} otherwise. + */ + public static final Break processZipEntries(InputStream zipFileInputStream, IZipEntryProcessor processor) { + if ( zipFileInputStream!=null ) { + try ( ZipInputStream zis = new ZipInputStream(zipFileInputStream) ) { + ZipEntry entry; + while ( (entry = zis.getNextEntry())!=null ) { + if ( processor.process(zis, entry).doBreak() ) { return Break.TRUE; } + } + } catch (IOException e) { + throw new RuntimeException("Error loading zip entry", e); + } + } + return Break.FALSE; + } + + /** + * Same as {@link #processZipEntries(InputStream, IZipEntryProcessor)}, + * but taking an {@link InputStream} {@link Supplier} instead of plain + * {@link InputStream}. + * @param zipFileInputStreamSupplier {@link Supplier} of the {@link InputStream} to be read as a zip file. + * @param processor The {@link IZipEntryProcessor} used to process each zip entry. + * @return {@link Break#TRUE} if the processor returned {@link Break#TRUE} for any + * entry, {@link Break#FALSE} otherwise. + */ + @SneakyThrows + public static final Break processZipEntries(Supplier zipFileInputStreamSupplier, IZipEntryProcessor processor) { + try (var is = zipFileInputStreamSupplier.get()) { + return processZipEntries(is, processor); + } + } + + /** + * Helper method to process individual zip entries from a zip file + * loaded from the given zipFileInputStream, calling the + * {@link IZipEntryWithContextProcessor#process(ZipInputStream, ZipEntry, Object)} + * method for each zip entry, also passing the arbitrary context object + * passed to this method. Processing is terminated if the processor + * returns {@link Break#TRUE}. + * + * @param Type of context object + * @param zipFileInputStream The {@link InputStream} to be read as a zip file. + * @param processor The {@link IZipEntryWithContextProcessor} used to process each zip entry. + * @param context Arbitrary context object to be passed to the {@link IZipEntryWithContextProcessor#process(ZipInputStream, ZipEntry, Object)} method. + * @return {@link Break#TRUE} if the processor returned {@link Break#TRUE} for any + * entry, {@link Break#FALSE} otherwise. + */ + public static final Break processZipEntries(InputStream zipFileInputStream, IZipEntryWithContextProcessor processor, C context) { + return processZipEntries(zipFileInputStream, (zis,ze)->processor.process(zis, ze, context)); + } + + /** + * Same as {@link #processZipEntries(InputStream, IZipEntryWithContextProcessor, Object)} + * but taking an {@link InputStream} {@link Supplier} instead of plain + * {@link InputStream}. + * + * @param Type of context object + * @param zipFileInputStream {@link Supplier} of the {@link InputStream} to be read as a zip file. + * @param processor The {@link IZipEntryWithContextProcessor} used to process each zip entry. + * @param context Arbitrary context object to be passed to the {@link IZipEntryWithContextProcessor#process(ZipInputStream, ZipEntry, Object)} method. + * @return {@link Break#TRUE} if the processor returned {@link Break#TRUE} for any + * entry, {@link Break#FALSE} otherwise. + */ + @SneakyThrows + public static final Break processZipEntries(Supplier zipFileInputStreamSupplier, IZipEntryWithContextProcessor processor, C context) { + try (var is = zipFileInputStreamSupplier.get()) { + return processZipEntries(is, processor, context); + } + } + + @FunctionalInterface + public static interface IZipEntryWithContextProcessor { + Break process(ZipInputStream zis, ZipEntry entry, C context); + } + + @FunctionalInterface + public static interface IZipEntryProcessor { + Break process(ZipInputStream zis, ZipEntry entry); + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/variable/FcliVariableHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/variable/FcliVariableHelper.java index 18f7c950f5..a11a7d74ad 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/variable/FcliVariableHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/variable/FcliVariableHelper.java @@ -27,9 +27,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.crypto.helper.EncryptionHelper; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.json.JsonNodeHolder; -import com.fortify.cli.common.util.EncryptionHelper; import com.fortify.cli.common.util.FcliDataHelper; import com.fortify.cli.common.util.StringUtils; 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..b8a308893a --- /dev/null +++ b/fcli-core/fcli-common/src/main/resources/com/fortify/cli/common/actions/zip/__sample__.yaml @@ -0,0 +1,239 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json +$schema: https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +# Depending on your IDE/YAML editor, the yaml-language-server comment or the $schema property +# above may be used to associate the action YAML file with the action YAML schema. This allows +# for your IDE/editor to provide code completion, displaying property descriptions, and validating +# the action YAML file. Fcli requires at least one of these to be present to determine whether +# current fcli version is compatible with the given schema. If your IDE/editor supports the +# yaml-language-server comment, you can remove the $schema line and vice versa. Note that the +# yaml-language-server line must remain as a comment (don't remove the leading hash-sign), +# whereas $schema must be a regular YAML property. If necessary you can have both, but schema +# location must be the same. By convention, these lines should appear at the top of the YAML +# document. + +# This action documents action syntax through comments for the various sections and +# elements below. Many action properties accept Spring Expression Language (SpEL) +# template expressions, 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). + +author: Fortify +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: file + cliAliases: f + description: "Output file name (or 'stdout' / 'stderr'). 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 +# - append: Set a data value by appending the given value to a new or existing array or object +# - 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 + # template 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 + # template expression evaluates to 'true'. + # breakIf: Optional; stop processing current and subsequent records if + # the given SpEL template 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: + - append: # See documentation for 'append' below + - name: repositories + valueTemplate: repositories + - name: repositories_json + value: ${repo} + # Write one or more outputs. Each write element supports the following properties: + # - to: Required; either 'stdout', 'stderr' or a file name. + # value: Required if no valueTemplate specified; SpEL template expression that generates the output to be written. + # valueTemplate: Required if no value specified; template output for the given template name. + - write: + - to: ${parameters.file} + valueTemplate: output + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +# This section defines value templates for use with 'set', 'append' and 'write' 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..121069314e 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,51 @@ 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.nameOrLocation = The action to load; either simple name or local or remote action \ + YAML file location. Note that custom actions are currently considered PREVIEW functionality, \ + as explained in the 'fcli ${module} action -h' help output. + +fcli.action.run.action-parameter = Action parameter(s); see 'help' command output to \ + list supported parameters. +fcli.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. +fcli.action.import.file = Single action YAML file to be imported; may be specified as a path to a \ + local file or a URL. Action name will be based on the given filename. +fcli.action.sign.in = Action YAML file to sign. +fcli.action.sign.out = Signed action output file. +fcli.action.sign.with = PEM file containing private key used for signing. +fcli.action.sign.password = Private key password. +fcli.action.sign.info = YAML file containing informational properties to be added to signature \ + metadata. For example, this can be used to document where the public key can be retrieved from, \ + or some extra information about the action being signed. +fcli.action.sign.signer = Free-format text string describing who signed this action, for example \ + a person, team or organization name. If not specified, signer will be taken from a property \ + named 'signer' in the file specified with the --info option if available, otherwise the current \ + user name will be used as the signer. +fcli.action.sign.pubout = Public key output file. This option is required when generating a \ + new key pair (if given private key doesn't exist), and may optionally be used for outputting \ + the public key if an already existing private key is being used. +fcli.action.resolver.from-zip = Optional local or remote zip-file from which to load the action if \ + the action is specified as a simple name. This option will be ignored if action is specified as \ + local or remote action YAML file location. +fcli.action.resolver.pubkey = Optional public key to use for verifying action signature. Can \ + be specified as one of: \ + %n file:%n url:%n string:%n env:\ + %n If no prefix is given, is assumed. For security reasons, you should only use \ + trusted public keys from a trusted source. Independent of source, contents must be in PEM \ + (base64-encoded) format. For convenience with string: or env: inputs, the \ + 'BEGIN/END PUBLIC KEY' statements and any whitespace (including newline characters) \ + may be omitted, allowing for having a single-line string: for \ + example. Note that the given public key will be ignored if its fingerprint doesn't match \ + the public key fingerprint stored in the action signature. If no (matching) public key is \ + provided, action signature will be verified against public keys previously imported through \ + the 'fcli config public-key import' command. +fcli.action.on-invalid-signature = Action to take if action signature is invalid. Allowed values: ${COMPLETION-CANDIDATES}. Default value: ${DEFAULT-VALUE}. +fcli.action.on-unsigned = Action to take if action isn't signed. Allowed values: ${COMPLETION-CANDIDATES}. Default value: ${DEFAULT-VALUE}. +fcli.action.on-no-public-key = Action to take if no matching public key was found. Allowed values: ${COMPLETION-CANDIDATES}. Default value: ${DEFAULT-VALUE}. +fcli.action.on-invalid-version = Action to take if action schema version is not supported by this fcli version. Allowed values: ${COMPLETION-CANDIDATES}. Default value: ${DEFAULT-VALUE}. +output.header.signatureStatus = Signature\nStatus # Generic, non command-specific output and query options arggroup.output.heading = Output options:%n @@ -102,6 +147,4 @@ socket-timeout = Socket timeout for this session, for example 30s (30 seconds), # Property default values that are usually set when running fcli, but which may not be available when # generating AsciiDoc man-pages. -fcli.env.default.prefix=FCLI_DEFAULT - - +fcli.env.default.prefix=FCLI_DEFAULT \ No newline at end of file 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/unirest/URIHelperTest.java b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/rest/unirest/URIHelperTest.java index 140e1f6631..5505fea25b 100644 --- a/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/rest/unirest/URIHelperTest.java +++ b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/rest/unirest/URIHelperTest.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 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-common/src/test/java/com/fortify/cli/common/util/SemVerHelperTest.java b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerTest.java similarity index 53% rename from fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerHelperTest.java rename to fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerTest.java index deeedec2a2..e33f56f51f 100644 --- a/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerHelperTest.java +++ b/fcli-core/fcli-common/src/test/java/com/fortify/cli/common/util/SemVerTest.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 @@ -21,7 +21,7 @@ * * @author Ruud Senden */ -public class SemVerHelperTest { +public class SemVerTest { @ParameterizedTest @CsvSource({ ",,0", @@ -43,6 +43,35 @@ public class SemVerHelperTest { "1.2.0-alpha1,1.2.0,-1" }) public void testSemVerCompare(String semver1, String semver2, int expectedResult) throws Exception { - assertEquals(expectedResult, SemVerHelper.compare(semver1, semver2)); + assertEquals(expectedResult, new SemVer(semver1).compareTo(semver2)); + } + + @ParameterizedTest + @CsvSource({ + ",unknown", + "a,unknown", + "1.2.3,1.0.0-1.2.*", + "1.2.3-alpha1,1.0.0-1.2.*", + "2.0.0,2.0.*" + }) + public void testSemVerCompatibleVersionsString(String semver, String expectedResult) throws Exception { + assertEquals(expectedResult, new SemVer(semver).getCompatibleVersionsString().orElse("unknown")); + } + + @ParameterizedTest + @CsvSource({ + ",,false", + "1.2.3,,false", + ",1.2.3,false", + "1.2.3,1.2.3,true", + "1.2.3,1.2.0,true", + "1.2.3,1.2.5,true", + "2.0.0,1.2.3,false", + "1.2.3,2.0.0,false", + "1.2.3,1.4.0,false", + "1.4.0,1.2.3,true" + }) + public void testSemVerIsCompatibleWith(String semver1, String semver2, boolean expectedResult) throws Exception { + assertEquals(expectedResult, new SemVer(semver1).isCompatibleWith(semver2)); } } diff --git a/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/_main/cli/cmd/ConfigCommands.java b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/_main/cli/cmd/ConfigCommands.java index 2fec04a295..a4e5075fe6 100644 --- a/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/_main/cli/cmd/ConfigCommands.java +++ b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/_main/cli/cmd/ConfigCommands.java @@ -15,6 +15,7 @@ import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; import com.fortify.cli.config.language.cli.cmd.LanguageCommands; import com.fortify.cli.config.proxy.cli.cmd.ProxyCommands; +import com.fortify.cli.config.publickey.cli.cmd.PublicKeyCommands; import com.fortify.cli.config.truststore.cli.cmd.TrustStoreCommands; import picocli.CommandLine.Command; @@ -27,6 +28,7 @@ ConfigClearCommand.class, LanguageCommands.class, ProxyCommands.class, + PublicKeyCommands.class, TrustStoreCommands.class } ) diff --git a/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyClearCommand.java b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyClearCommand.java new file mode 100644 index 0000000000..e1ea658019 --- /dev/null +++ b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyClearCommand.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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.config.publickey.cli.cmd; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.cli.mixin.CommonOptionMixins; +import com.fortify.cli.common.crypto.helper.SignatureHelper; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; +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; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = OutputHelperMixins.Clear.CMD_NAME) +public class PublicKeyClearCommand extends AbstractOutputCommand implements IJsonNodeSupplier, IActionCommandResultSupplier { + @Getter @Mixin private OutputHelperMixins.Clear outputHelper; + @Mixin private CommonOptionMixins.RequireConfirmation confirm; + + @Override + public JsonNode getJsonNode() { + confirm.checkConfirmed(); + var trustStore = SignatureHelper.publicKeyTrustStore(); + return trustStore.stream() + .peek(d->trustStore.delete(d.getFingerprint())) + .map(PublicKeyDescriptor::asObjectNode) + .collect(JsonHelper.arrayNodeCollector()); + } + + @Override + public String getActionCommandResult() { + return "DELETED"; + } + + @Override + public boolean isSingular() { + return false; + } +} diff --git a/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyCommands.java b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyCommands.java new file mode 100644 index 0000000000..3acb372938 --- /dev/null +++ b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyCommands.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * 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.config.publickey.cli.cmd; + +import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; + +import picocli.CommandLine.Command; + +@Command( + name = "public-key", + aliases = "pubkey", + subcommands = { + PublicKeyClearCommand.class, + PublicKeyDeleteCommand.class, + PublicKeyGetCommand.class, + PublicKeyListCommand.class, + PublicKeyImportCommand.class + + } +) +public class PublicKeyCommands extends AbstractContainerCommand { +} diff --git a/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyDeleteCommand.java b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyDeleteCommand.java new file mode 100644 index 0000000000..70249f2a1d --- /dev/null +++ b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyDeleteCommand.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * 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.config.publickey.cli.cmd; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.crypto.helper.SignatureHelper; +import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; +import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; +import com.fortify.cli.config.publickey.cli.mixin.PublicKeyResolverMixin; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = OutputHelperMixins.Delete.CMD_NAME) +public class PublicKeyDeleteCommand extends AbstractOutputCommand implements IJsonNodeSupplier, IActionCommandResultSupplier { + @Getter @Mixin private OutputHelperMixins.Delete outputHelper; + @Mixin PublicKeyResolverMixin.PositionalParameter publicKeyResolver; + + @Override + public JsonNode getJsonNode() { + return SignatureHelper.publicKeyTrustStore() + .delete(publicKeyResolver.getNameOrFingerprint()) + .asObjectNode(); + } + + + @Override + public String getActionCommandResult() { + return "DELETED"; + } + + @Override + public boolean isSingular() { + return true; + } +} diff --git a/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyGetCommand.java b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyGetCommand.java new file mode 100644 index 0000000000..3be8e522c8 --- /dev/null +++ b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyGetCommand.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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.config.publickey.cli.cmd; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; +import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.config.publickey.cli.mixin.PublicKeyResolverMixin; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = OutputHelperMixins.Get.CMD_NAME) +public class PublicKeyGetCommand extends AbstractOutputCommand implements IJsonNodeSupplier { + @Getter @Mixin private OutputHelperMixins.Get outputHelper; + @Mixin PublicKeyResolverMixin.PositionalParameter publicKeyResolver; + + @Override + public JsonNode getJsonNode() { + return publicKeyResolver.getPublicKeyDescriptor().asObjectNode(); + } + + @Override + public boolean isSingular() { + return true; + } +} diff --git a/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyImportCommand.java b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyImportCommand.java new file mode 100644 index 0000000000..a977af9918 --- /dev/null +++ b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyImportCommand.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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.config.publickey.cli.cmd; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.cli.mixin.CommonOptionMixins.AbstractTextResolverMixin; +import com.fortify.cli.common.crypto.helper.SignatureHelper; +import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; +import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; + +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@Command(name = OutputHelperMixins.Import.CMD_NAME) +public class PublicKeyImportCommand extends AbstractOutputCommand implements IJsonNodeSupplier, IActionCommandResultSupplier { + @Getter @Mixin private OutputHelperMixins.Import outputHelper; + @Mixin private PublicKeyResolverMixin publicKeyResolver; + @Option(names = {"--name", "-n"}, required = true) private String name; + + @Override + public JsonNode getJsonNode() { + return SignatureHelper.publicKeyTrustStore() + .importKey(publicKeyResolver.getText(), name) + .asObjectNode(); + } + + @Override + public String getActionCommandResult() { + return "IMPORTED"; + } + + @Override + public boolean isSingular() { + return true; + } + + private static class PublicKeyResolverMixin extends AbstractTextResolverMixin { + @Getter @Parameters(arity = "1", descriptionKey = "fcli.config.public-key.resolver", paramLabel = "source") private String textSource; + } +} diff --git a/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyListCommand.java b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyListCommand.java new file mode 100644 index 0000000000..02fc1a59ca --- /dev/null +++ b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/cmd/PublicKeyListCommand.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * 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.config.publickey.cli.cmd; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.crypto.helper.SignatureHelper; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; +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; +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 PublicKeyListCommand extends AbstractOutputCommand implements IJsonNodeSupplier { + @Getter @Mixin private OutputHelperMixins.List outputHelper; + + @Override + public JsonNode getJsonNode() { + return SignatureHelper.publicKeyTrustStore().stream() + .map(PublicKeyDescriptor::asObjectNode) + .collect(JsonHelper.arrayNodeCollector()); + } + + @Override + public boolean isSingular() { + return false; + } +} diff --git a/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/mixin/PublicKeyResolverMixin.java b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/mixin/PublicKeyResolverMixin.java new file mode 100644 index 0000000000..f8ba542975 --- /dev/null +++ b/fcli-core/fcli-config/src/main/java/com/fortify/cli/config/publickey/cli/mixin/PublicKeyResolverMixin.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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.config.publickey.cli.mixin; + +import com.fortify.cli.common.crypto.helper.SignatureHelper; +import com.fortify.cli.common.crypto.helper.SignatureHelper.PublicKeyDescriptor; + +import lombok.Getter; +import picocli.CommandLine.Parameters; + +public class PublicKeyResolverMixin { + + public static abstract class AbstractPublicKeyResolverMixin { + public abstract String getNameOrFingerprint(); + + public PublicKeyDescriptor getPublicKeyDescriptor(){ + return SignatureHelper.publicKeyTrustStore().load(getNameOrFingerprint(), true); + } + } + + public static class PositionalParameter extends AbstractPublicKeyResolverMixin { + @Parameters(index = "0", arity = "1", descriptionKey = "fcli.config.public-key.nameOrFingerprint") + @Getter private String nameOrFingerprint; + } +} diff --git a/fcli-core/fcli-config/src/main/resources/com/fortify/cli/config/i18n/ConfigMessages.properties b/fcli-core/fcli-config/src/main/resources/com/fortify/cli/config/i18n/ConfigMessages.properties index cb9fcb624e..9b14c4d4d5 100644 --- a/fcli-core/fcli-config/src/main/resources/com/fortify/cli/config/i18n/ConfigMessages.properties +++ b/fcli-core/fcli-config/src/main/resources/com/fortify/cli/config/i18n/ConfigMessages.properties @@ -39,6 +39,27 @@ fcli.config.proxy.modules = Comma-separated list of fcli modules / target system fcli.config.proxy.include-hosts = Comma-separated list of target host names on which to apply this proxy configuration. Host names may include wildcard characters, like *.fortifyhosted.net. fcli.config.proxy.exclude-hosts = Comma-separated list of target host names on which not to apply this proxy configuration. Host names may include wildcard characters, like *.myintra.net. +# fcli config public-key +fcli.config.public-key.usage.header = Manage fcli trusted public keys. +fcli.config.public-key.clear.usage.header = Clear all trusted public keys. +fcli.config.public-key.clear.confirm = Confirm clearing all trusted public keys. +fcli.config.public-key.delete.usage.header = Delete a trusted public key. +fcli.config.public-key.get.usage.header = Get trusted public key data. +fcli.config.public-key.import.usage.header = Import a trusted public key. +fcli.config.public-key.import.file = PEM file to be imported. +fcli.config.public-key.import.name = Name for the imported public key. +fcli.config.public-key.list.usage.header = List trusted public keys. +fcli.config.public-key.nameOrFingerprint = Public key name or fingerprint. +fcli.config.public-key.resolver = Public key to be imported into the fcli trusted public \ + key store. Can be specified as one of: \ + %n file:%n url:%n string:%n env:\ + %n If no prefix is given, is assumed. For security reasons, you should only \ + import trusted public keys from a trusted source. Independent of source, contents must be \ + in PEM (base64-encoded) format. For convenience with string: or env: inputs, the \ + 'BEGIN/END PUBLIC KEY' statements and any whitespace (including newline characters) \ + may be omitted. + +# fcli config truststore fcli.config.truststore.usage.header = Manage fcli trust store configuration. fcli.config.truststore.clear.usage.header = Clear SSL trust store configuration to use default trust store. fcli.config.truststore.get.usage.header = Get current SSL trust store configuration. @@ -64,5 +85,6 @@ fcli.config.proxy.output.header.proxyUser = User fcli.config.clear.output.table.options = name,type fcli.config.language.output.table.options = locale,localName,name,active fcli.config.proxy.output.table.options = name,priority,proxyHost,proxyPort,proxyUser,modules,modulesMatchMode,targetHost +fcli.config.public-key.output.table.options = name,fingerprint fcli.config.truststore.output.table.options = path,type diff --git a/fcli-core/fcli-fod/build.gradle b/fcli-core/fcli-fod/build.gradle index 8e063597b9..3e3b71fcca 100644 --- a/fcli-core/fcli-fod/build.gradle +++ b/fcli-core/fcli-fod/build.gradle @@ -1 +1,13 @@ +task zipResources_actions(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") { + // TODO We should also sign file; how do we invoke a sign operation from Gradle? + filter(line->project.version.startsWith('0.') + ? line + : line.replaceAll("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json", "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${fcliActionSchemaVersion}.json")) + } + include '*.yaml' +} + 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/cli/mixin/AbstractFoDEmbedMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/AbstractFoDEmbedMixin.java new file mode 100644 index 0000000000..1c40a42a86 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/cli/mixin/AbstractFoDEmbedMixin.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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._common.cli.mixin; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.cli.mixin.CommandHelperMixin; +import com.fortify.cli.common.output.transform.IRecordTransformer; +import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; +import com.fortify.cli.fod._common.rest.embed.FoDEmbedder; +import com.fortify.cli.fod._common.rest.embed.IFoDEntityEmbedderSupplier; + +import kong.unirest.UnirestInstance; +import picocli.CommandLine.Mixin; + +public abstract class AbstractFoDEmbedMixin implements IRecordTransformer { + @Mixin private CommandHelperMixin commandHelper; + private FoDEmbedder embedder; + + @Override + public final JsonNode transformRecord(JsonNode record) { + if ( embedder==null ) { embedder = new FoDEmbedder(getEmbedSuppliers()); } + UnirestInstance unirest = commandHelper + .getCommandAs(IUnirestInstanceSupplier.class) + .orElseThrow().getUnirestInstance(); + embedder.transformRecord(unirest, record); + return record; + } + + protected abstract IFoDEntityEmbedderSupplier[] getEmbedSuppliers(); +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/AbstractFoDBaseRequestOutputCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/cmd/AbstractFoDBaseRequestOutputCommand.java similarity index 95% rename from fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/AbstractFoDBaseRequestOutputCommand.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/cmd/AbstractFoDBaseRequestOutputCommand.java index bbb64a3cd0..aab09a7908 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/AbstractFoDBaseRequestOutputCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/cmd/AbstractFoDBaseRequestOutputCommand.java @@ -10,7 +10,7 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.fod._common.output.cli; +package com.fortify.cli.fod._common.output.cli.cmd; import com.fortify.cli.common.output.cli.cmd.IBaseRequestSupplier; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/AbstractFoDJsonNodeOutputCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/cmd/AbstractFoDJsonNodeOutputCommand.java similarity index 95% rename from fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/AbstractFoDJsonNodeOutputCommand.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/cmd/AbstractFoDJsonNodeOutputCommand.java index 67684f8c53..80ecde0a60 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/AbstractFoDJsonNodeOutputCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/cmd/AbstractFoDJsonNodeOutputCommand.java @@ -10,7 +10,7 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.fod._common.output.cli; +package com.fortify.cli.fod._common.output.cli.cmd; import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/AbstractFoDOutputCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/cmd/AbstractFoDOutputCommand.java similarity index 73% rename from fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/AbstractFoDOutputCommand.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/cmd/AbstractFoDOutputCommand.java index c24ede2902..a195acf6af 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/AbstractFoDOutputCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/cmd/AbstractFoDOutputCommand.java @@ -10,12 +10,13 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.fod._common.output.cli; +package com.fortify.cli.fod._common.output.cli.cmd; import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; import com.fortify.cli.common.output.product.IProductHelperSupplier; import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; -import com.fortify.cli.fod._common.output.mixin.FoDProductHelperStandardMixin; +import com.fortify.cli.fod._common.rest.helper.FoDProductHelper; +import com.fortify.cli.fod._common.session.cli.mixin.FoDUnirestInstanceSupplierMixin; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -24,9 +25,10 @@ public abstract class AbstractFoDOutputCommand extends AbstractOutputCommand implements IProductHelperSupplier, IUnirestInstanceSupplier { - @Getter @Mixin FoDProductHelperStandardMixin productHelper; + @Getter @Mixin private FoDUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final FoDProductHelper productHelper = FoDProductHelper.INSTANCE; public final UnirestInstance getUnirestInstance() { - return productHelper.getUnirestInstance(); + return unirestInstanceSupplier.getUnirestInstance(); } } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/mixin/FoDOutputHelperMixins.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/mixin/FoDOutputHelperMixins.java similarity index 98% rename from fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/mixin/FoDOutputHelperMixins.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/mixin/FoDOutputHelperMixins.java index 724769a968..5c192b382e 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/mixin/FoDOutputHelperMixins.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/cli/mixin/FoDOutputHelperMixins.java @@ -10,7 +10,7 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.fod._common.output.mixin; +package com.fortify.cli.fod._common.output.cli.mixin; import com.fortify.cli.common.output.cli.mixin.IOutputHelper; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/FoDUrls.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/FoDUrls.java index 8c4d299980..6ff229b9b5 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/FoDUrls.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/FoDUrls.java @@ -56,4 +56,5 @@ public class FoDUrls { public static final String DAST_AUTOMATED_SCANS = ApiBase + "/releases/{relId}/dast-automated-scans"; public static final String REPORTS = ApiBase + "/reports"; public static final String REPORT = ApiBase + "/reports/{reportId}"; + public static final String SCAN_POLLING_SUMMARY = ApiBase + "/releases/{relId}/scans/{scanId}/polling-summary"; } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/embed/FoDEmbedder.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/embed/FoDEmbedder.java new file mode 100644 index 0000000000..231a68e7cc --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/embed/FoDEmbedder.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * 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._common.rest.embed; + +import java.util.Collection; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import kong.unirest.UnirestInstance; + +/** + * This class takes zero or more {@link IFoDEntityEmbedderSupplier} instances + * as constructor argument(s), storing the {@link IFoDEntityEmbedder} instances + * generated by these suppliers, to provide the {@link #transformRecord(UnirestInstance, JsonNode)} + * method that embeds the requested data into a given record. + * + * @author rsenden + * + */ +public class FoDEmbedder { + private final Collection embedders; + + public FoDEmbedder(IFoDEntityEmbedderSupplier... suppliers) { + this.embedders = suppliers==null ? null : Stream.of(suppliers) + .map(IFoDEntityEmbedderSupplier::createEntityEmbedder) + .collect(Collectors.toList()); + } + + public JsonNode transformRecord(UnirestInstance unirest, JsonNode record) { + if ( embedders!=null ) { + if ( !(record instanceof ObjectNode) ) { + throw new RuntimeException("Can't embed data in records of type "+record.getNodeType()); + } + embedders.forEach(e->e.embed(unirest, (ObjectNode)record)); + } + return record; + } +} diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastSSCProductHelperBasicMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/embed/IFoDEntityEmbedder.java similarity index 56% rename from fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastSSCProductHelperBasicMixin.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/embed/IFoDEntityEmbedder.java index 70ab833d7d..c5a20f8edb 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastSSCProductHelperBasicMixin.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/embed/IFoDEntityEmbedder.java @@ -10,19 +10,19 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.sc_sast._common.output.cli.mixin; +package com.fortify.cli.fod._common.rest.embed; -import com.fortify.cli.common.output.product.IProductHelper; -import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; -import com.fortify.cli.sc_sast._common.session.cli.mixin.AbstractSCSastUnirestInstanceSupplierMixin; +import com.fasterxml.jackson.databind.node.ObjectNode; import kong.unirest.UnirestInstance; -public class SCSastSSCProductHelperBasicMixin extends AbstractSCSastUnirestInstanceSupplierMixin - implements IProductHelper, IUnirestInstanceSupplier -{ - @Override - public final UnirestInstance getUnirestInstance() { - return getSscUnirestInstance(); - } +/** + * Interface for executing one or more requests and adding the response data + * as embedded properties to a given record. + * + * @author rsenden + */ +@FunctionalInterface +public interface IFoDEntityEmbedder { + void embed(UnirestInstance unirest, ObjectNode record); } \ No newline at end of file diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastOutputHelperMixins.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/embed/IFoDEntityEmbedderSupplier.java similarity index 73% rename from fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastOutputHelperMixins.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/embed/IFoDEntityEmbedderSupplier.java index c7ddf7cd27..a7302c00d9 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastOutputHelperMixins.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/embed/IFoDEntityEmbedderSupplier.java @@ -10,14 +10,13 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.sc_sast._common.output.cli.mixin; - -import com.fortify.cli.common.output.cli.mixin.IOutputHelper; +package com.fortify.cli.fod._common.rest.embed; /** - *

This class provides SC-SAST-specific {@link IOutputHelper} implementations.

- * + * Interface for supplying an {@link IFoDEntityEmbedder} instance. * @author rsenden */ -public class SCSastOutputHelperMixins { +@FunctionalInterface +public interface IFoDEntityEmbedderSupplier { + IFoDEntityEmbedder createEntityEmbedder(); } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/mixin/FoDProductHelperStandardMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDProductHelper.java similarity index 53% rename from fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/mixin/FoDProductHelperStandardMixin.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDProductHelper.java index 453ed41cd5..39f6bf65d8 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/mixin/FoDProductHelperStandardMixin.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDProductHelper.java @@ -10,20 +10,24 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.fod._common.output.mixin; +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 com.fortify.cli.fod._common.rest.helper.FoDInputTransformer; -import com.fortify.cli.fod._common.rest.helper.FoDPagingHelper; + +import lombok.SneakyThrows; // IMPORTANT: When updating/adding any methods in this class, FoDRestCallCommand // also likely needs to be updated -public class FoDProductHelperStandardMixin extends FoDProductHelperBasicMixin - implements IInputTransformer, INextPageUrlProducerSupplier +public class FoDProductHelper implements IProductHelper, IInputTransformer, INextPageUrlProducerSupplier { + public static final FoDProductHelper INSTANCE = new FoDProductHelper(); + private FoDProductHelper() {} @Override public INextPageUrlProducer getNextPageUrlProducer() { return FoDPagingHelper.nextPageUrlProducer(); @@ -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/AbstractFoDScanCancelCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanCancelCommand.java index ee9f35efdb..dfa32c0b27 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanCancelCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanCancelCommand.java @@ -16,7 +16,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.scan.cli.mixin.FoDScanResolverMixin; import com.fortify.cli.fod._common.scan.helper.FoDScanDescriptor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanConfigGetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanConfigGetCommand.java index 0dd7ddbbee..e6d1769422 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanConfigGetCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanConfigGetCommand.java @@ -19,7 +19,7 @@ import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.DisableTest.TestType; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import kong.unirest.UnirestInstance; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanDownloadCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanDownloadCommand.java index f11a292a49..dbdc9dcf88 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanDownloadCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanDownloadCommand.java @@ -19,7 +19,7 @@ import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.scan.cli.mixin.FoDScanResolverMixin; import com.fortify.cli.fod._common.scan.helper.FoDScanDescriptor; import com.fortify.cli.fod._common.scan.helper.FoDScanHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanDownloadLatestCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanDownloadLatestCommand.java index 97901e267d..81119d3e77 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanDownloadLatestCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanDownloadLatestCommand.java @@ -19,7 +19,7 @@ import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanDescriptor; import com.fortify.cli.fod._common.scan.helper.FoDScanHelper; import com.fortify.cli.fod._common.scan.helper.FoDScanType; 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 be839afbd3..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 @@ -17,9 +17,10 @@ import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +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/AbstractFoDScanGetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanGetCommand.java index 137ab186a4..24aa98ea5c 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanGetCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanGetCommand.java @@ -15,7 +15,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.cli.util.CommandGroup; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.scan.cli.mixin.FoDScanResolverMixin; import com.fortify.cli.fod._common.scan.helper.FoDScanType; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanImportCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanImportCommand.java index b0f4dfc3bc..e011f6feae 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanImportCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanImportCommand.java @@ -19,7 +19,7 @@ import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.rest.helper.FoDFileTransferHelper; import com.fortify.cli.fod._common.scan.helper.FoDScanType; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanListCommand.java index 3d4f961110..940adbe29c 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanListCommand.java @@ -16,7 +16,7 @@ import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDBaseRequestOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDBaseRequestOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.scan.helper.FoDScanHelper; import com.fortify.cli.fod._common.scan.helper.FoDScanType; 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 52caee40d8..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,23 +13,25 @@ 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; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.rest.helper.FoDFileTransferHelper; 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/cmd/AbstractFoDScanStartCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanStartCommand.java index 591a354b87..3a691c6b62 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanStartCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanStartCommand.java @@ -17,7 +17,7 @@ import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanDescriptor; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanWaitForCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanWaitForCommand.java index 509e385a4e..826503f3e1 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanWaitForCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/cmd/AbstractFoDScanWaitForCommand.java @@ -13,25 +13,30 @@ package com.fortify.cli.fod._common.scan.cli.cmd; -import java.util.Set; - +import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.cli.util.CommandGroup; +import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.rest.cli.cmd.AbstractWaitForCommand; import com.fortify.cli.common.rest.wait.WaitHelper.WaitHelperBuilder; -import com.fortify.cli.fod._common.output.mixin.FoDProductHelperStandardMixin; +import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; import com.fortify.cli.fod._common.scan.cli.mixin.FoDScanResolverMixin; +import com.fortify.cli.fod._common.scan.helper.FoDScanHelper; import com.fortify.cli.fod._common.scan.helper.FoDScanStatus; import com.fortify.cli.fod._common.scan.helper.FoDScanStatus.FoDScanStatusIterable; import com.fortify.cli.fod._common.scan.helper.FoDScanType; - +import com.fortify.cli.fod._common.session.cli.mixin.FoDUnirestInstanceSupplierMixin; import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; -@CommandGroup("*-scan") -public abstract class AbstractFoDScanWaitForCommand extends AbstractWaitForCommand { - @Getter @Mixin FoDProductHelperStandardMixin productHelper; +import java.util.Arrays; +import java.util.Set; + +@CommandGroup("*-scan-wait-for") +public abstract class AbstractFoDScanWaitForCommand extends AbstractWaitForCommand implements IRecordTransformer { + @Getter @Mixin private FoDUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Mixin private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins @Mixin private FoDScanResolverMixin.PositionalParameterMulti scansResolver; @Option(names={"-s", "--any-state"}, required=true, split=",", defaultValue="Completed", completionCandidates = FoDScanStatusIterable.class) private Set states; @@ -40,6 +45,7 @@ public abstract class AbstractFoDScanWaitForCommand extends AbstractWaitForComma protected final WaitHelperBuilder configure(UnirestInstance unirest, WaitHelperBuilder builder) { return builder .recordsSupplier(scansResolver::getScanDescriptorJsonNodes) + .recordTransformer(this::transformRecord) .currentStateProperty("analysisStatusType") .knownStates(FoDScanStatus.getKnownStateNames()) .failureStates(FoDScanStatus.getFailureStateNames()) @@ -49,4 +55,9 @@ protected final WaitHelperBuilder configure(UnirestInstance unirest, WaitHelperB // TODO Verify that all given scan id's are of given scan type protected abstract FoDScanType getScanType(); + @Override + public JsonNode transformRecord(JsonNode record) { + return FoDScanHelper.renameFields(record, getScanType()); + } + } 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/cli/mixin/FoDScanResolverMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/mixin/FoDScanResolverMixin.java index c0c4f04c18..26266c32eb 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/mixin/FoDScanResolverMixin.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/cli/mixin/FoDScanResolverMixin.java @@ -12,28 +12,36 @@ *******************************************************************************/ package com.fortify.cli.fod._common.scan.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.common.util.StringUtils; +import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; +import com.fortify.cli.fod._common.cli.mixin.IFoDDelimiterMixinAware; import com.fortify.cli.fod._common.scan.helper.FoDScanDescriptor; import com.fortify.cli.fod._common.scan.helper.FoDScanHelper; import com.fortify.cli.fod._common.scan.helper.FoDScanType; - import kong.unirest.UnirestInstance; import lombok.Getter; +import lombok.Setter; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; +import java.util.stream.Stream; + public class FoDScanResolverMixin { - public static abstract class AbstractFoDScanResolverMixin { - public abstract String getScanId(); + public static abstract class AbstractFoDScanResolverMixin implements IFoDDelimiterMixinAware { + @Setter private FoDDelimiterMixin delimiterMixin; + public abstract String getReleaseQualifiedScanOrId(); public FoDScanDescriptor getScanDescriptor(UnirestInstance unirest) { - return FoDScanHelper.getScanDescriptor(unirest, getScanId()); + var releaseQualifiedScanOrId = getReleaseQualifiedScanOrId(); + return StringUtils.isBlank(releaseQualifiedScanOrId) + ? null + : FoDScanHelper.getScanDescriptor(unirest, releaseQualifiedScanOrId, getDelimiter()); } public FoDScanDescriptor getScanDescriptor(UnirestInstance unirest, FoDScanType scanType) { @@ -47,13 +55,24 @@ public FoDScanDescriptor getScanDescriptor(UnirestInstance unirest, FoDScanType public String getScanId(UnirestInstance unirest) { return getScanDescriptor(unirest).getScanId(); } + + public final String getDelimiter() { + return delimiterMixin.getDelimiter(); + } + } - public static abstract class AbstractFoDMultiScanResolverMixin { - public abstract String[] getScanIds(); + public static abstract class AbstractFoDMultiScanResolverMixin implements IFoDDelimiterMixinAware { + @Setter private FoDDelimiterMixin delimiterMixin; + + public abstract String[] getReleaseQualifiedScanOrIds(); + + public boolean containsReleaseId() { + return Arrays.stream(getReleaseQualifiedScanOrIds()).anyMatch((e) -> e.contains(getDelimiter())); + } public FoDScanDescriptor[] getScanDescriptors(UnirestInstance unirest) { - return Stream.of(getScanIds()).map(id->FoDScanHelper.getScanDescriptor(unirest, id)).toArray(FoDScanDescriptor[]::new); + return Stream.of(getReleaseQualifiedScanOrIds()).map(id -> FoDScanHelper.getScanDescriptor(unirest, id, delimiterMixin.getDelimiter())).toArray(FoDScanDescriptor[]::new); } public Collection getScanDescriptorJsonNodes(UnirestInstance unirest) { @@ -63,26 +82,31 @@ public Collection getScanDescriptorJsonNodes(UnirestInstance unirest) public String[] getScanIds(UnirestInstance unirest) { return Stream.of(getScanDescriptors(unirest)).map(FoDScanDescriptor::getScanId).toArray(String[]::new); } + + public final String getDelimiter() { + return delimiterMixin.getDelimiter(); + } + } public static class RequiredOption extends AbstractFoDScanResolverMixin { @EnvSuffix("SCAN") @Option(names = {"--scan"}, required = true) - @Getter private String scanId; + @Getter private String releaseQualifiedScanOrId; } public static class RequiredOptionMulti extends AbstractFoDMultiScanResolverMixin { @EnvSuffix("SCANS") @Option(names = {"--scans"}, required=true, split=",", descriptionKey = "fcli.fod.scan.scan-id") - @Getter private String[] scanIds; + @Getter private String[] releaseQualifiedScanOrIds; } public static class PositionalParameter extends AbstractFoDScanResolverMixin { @EnvSuffix("SCAN") @Parameters(index = "0", arity = "1", paramLabel="scan-id", descriptionKey = "fcli.fod.scan.scan-id") - @Getter private String scanId; + @Getter private String releaseQualifiedScanOrId; } public static class PositionalParameterMulti extends AbstractFoDMultiScanResolverMixin { @EnvSuffix("SCANS") @Parameters(index = "0", arity = "1..", paramLabel = "scan-id's", descriptionKey = "fcli.fod.scan.scan-id") - @Getter private String[] scanIds; + @Getter private String[] releaseQualifiedScanOrIds; } } 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/FoDScanDescriptor.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanDescriptor.java index fd5df1e003..a84f3fd835 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanDescriptor.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanDescriptor.java @@ -13,16 +13,16 @@ package com.fortify.cli.fod._common.scan.helper; -import java.util.Date; - import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.formkiq.graalvm.annotations.Reflectable; import com.fortify.cli.common.json.JsonNodeHolder; - import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.Date; + @Reflectable @NoArgsConstructor @Data @EqualsAndHashCode(callSuper = true) public class FoDScanDescriptor extends JsonNodeHolder { @@ -34,6 +34,12 @@ public class FoDScanDescriptor extends JsonNodeHolder { private String releaseId; private String microserviceName; private String status; + + @JsonIgnore + public String getReleaseAndScanId() { + return String.format("%s:%s", releaseId, scanId); + } + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyy-MM-dd'T'hh:mm:ss") private Date startedDateTime; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyy-MM-dd'T'hh:mm:ss") 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..8b9ee3a72e 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 @@ -19,6 +19,7 @@ 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.output.transform.fields.RenameFieldsTransformer; 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.*; @@ -51,12 +52,42 @@ public class FoDScanHelper { // max retention period (in years) of FPRs public static int MAX_RETENTION_PERIOD = 2; - public static final FoDScanDescriptor getScanDescriptor(UnirestInstance unirest, String scanId) { - var result = unirest.get(FoDUrls.SCAN + "/summary") - .routeParam("scanId", scanId) - .asObject(ObjectNode.class) - .getBody(); - return getDescriptor(result); + public static final JsonNode renameFields(JsonNode record, FoDScanType scanType) { + var obj = (ObjectNode)new RenameFieldsTransformer(new String[] { + "ScanId:scanId", + "AnalysisStatusId:analysisStatusTypeId", + "AnalysisStatusTypeValue:analysisStatusType", + "IssueCountCritical:issueCountCritical", + "IssueCountHigh:issueCountHigh", + "IssueCountMedium:issueCountMedium", + "IssueCountLow:issueCountLow" + }).transform(record); + if (obj.has("ScanType")) { + obj.put("scanTypeId", obj.get("ScanType").intValue()); + obj.put("scanType", scanType.name()); + obj.remove("ScanType"); + } + return obj; + } + public static final FoDScanDescriptor getScanDescriptor(UnirestInstance unirest, String releaseQualifiedScanOrId, String delimiter) { + String[] elts = releaseQualifiedScanOrId.split(delimiter); + switch (elts.length) { + case 2: + var pollingResult = unirest.get(FoDUrls.SCAN_POLLING_SUMMARY) + .routeParam("relId", elts[0]) + .routeParam("scanId", elts[1]) + .asObject(ObjectNode.class) + .getBody(); + return getDescriptor(pollingResult); + case 1: + var summaryResult = unirest.get(FoDUrls.SCAN + "/summary") + .routeParam("scanId", elts[0]) + .asObject(ObjectNode.class) + .getBody(); + return getDescriptor(summaryResult); + default: + throw new IllegalArgumentException("Scan must be specified in the format " + delimiter + " or "); + } } public static final FoDScanDescriptor getLatestScanDescriptor(UnirestInstance unirest, String relId, @@ -226,6 +257,13 @@ public static HttpRequest getGrpcSetupRequest(UnirestInstance unirest, String .body(setupRequest); } + /*public Collection getScanDescriptorJsonNodes(UnirestInstance unirest, String releaseId, String scanId) { + unirest.get(FoDUrls.SCAN_POLLING_SUMMARY) + .routeParam("relId", releaseId) + .routeParam("scanId", scanId); + return Stream.of(getScanDescriptors(unirest)).map(FoDScanPollingDescriptor::asJsonNode).collect(Collectors.toList()); + }*/ + // private static final FoDScanDescriptor getDescriptor(JsonNode node) { diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanPollingDescriptor.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanPollingDescriptor.java new file mode 100644 index 0000000000..e7ace70286 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/FoDScanPollingDescriptor.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * 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._common.scan.helper; + +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.json.JsonNodeHolder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Reflectable @NoArgsConstructor +@Data @EqualsAndHashCode(callSuper = true) +public class FoDScanPollingDescriptor extends JsonNodeHolder { + private String ScanId; + private String OpenSourceScanId; + private String TenantId; + private String AnalysisStatusId; + private String OpenSourceStatusId; + private String AnalysisStatusTypeValue; + private String AnalysisStatusReasonId; + private String AnalysisStatusReason; + private String AnalysisStatusReasonNotes; + private String IssueCountCritical; + private String IssueCountHigh; + private String IssueCountMedium; + private String IssueCountLow; + private String PassFailStatus; + private String PassFailReasonType; + private String PauseDetails; + private String ScanType; +} 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..1191fd04ec 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; @@ -51,6 +51,7 @@ public static final FoDScanDescriptor startScan(UnirestInstance unirest, FoDRele JsonNode node = objectMapper.createObjectNode() .put("scanId", startScanResponse.getScanId()) .put("scanType", FoDScanType.Dynamic.name()) + .put("releaseAndScanId", String.format("%s:%s", releaseDescriptor.getReleaseId(), startScanResponse.getScanId())) .put("analysisStatusType", "Pending") .put("applicationName", releaseDescriptor.getApplicationName()) .put("releaseName", releaseDescriptor.getReleaseName()) 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/scan/helper/dast/FoDScanDastLegacyHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastLegacyHelper.java index f7b701fcd5..25f3ae5ebe 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastLegacyHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastLegacyHelper.java @@ -53,6 +53,7 @@ public static final FoDScanDescriptor startScan(UnirestInstance unirest, FoDRele JsonNode node = objectMapper.createObjectNode() .put("scanId", startScanResponse.getScanId()) .put("scanType", FoDScanType.Dynamic.name()) + .put("releaseAndScanId", String.format("%s:%s", releaseDescriptor.getReleaseId(), startScanResponse.getScanId())) .put("analysisStatusType", "Pending") .put("applicationName", releaseDescriptor.getApplicationName()) .put("releaseName", releaseDescriptor.getReleaseName()) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/mobile/FoDScanMobileHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/mobile/FoDScanMobileHelper.java index bf3b59b1d6..f10eecef49 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/mobile/FoDScanMobileHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/mobile/FoDScanMobileHelper.java @@ -58,6 +58,7 @@ public static final FoDScanDescriptor startScan(UnirestInstance unirest, IProgre JsonNode node = objectMapper.createObjectNode() .put("scanId", startScanResponse.getScanId()) .put("scanType", FoDScanType.Mobile.name()) + .put("releaseAndScanId", String.format("%s:%s", releaseDescriptor.getReleaseId(), startScanResponse.getScanId())) .put("analysisStatusType", "Pending") .put("applicationName", releaseDescriptor.getApplicationName()) .put("releaseName", releaseDescriptor.getReleaseName()) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/oss/FoDScanOssHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/oss/FoDScanOssHelper.java index d0395ff517..bb5b2bd914 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/oss/FoDScanOssHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/oss/FoDScanOssHelper.java @@ -53,6 +53,7 @@ private static FoDScanDescriptor startScan(UnirestInstance unirest, FoDReleaseDe JsonNode node = objectMapper.createObjectNode() .put("scanId", startScanResponse.getScanId()) .put("scanType", FoDScanType.OpenSource.name()) + .put("releaseAndScanId", String.format("%s:%s", releaseDescriptor.getReleaseId(), startScanResponse.getScanId())) .put("analysisStatusType", "Pending") .put("applicationName", releaseDescriptor.getApplicationName()) .put("releaseName", releaseDescriptor.getReleaseName()) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java index 7c483a3f17..e264689185 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java @@ -88,6 +88,7 @@ private static FoDScanDescriptor startScan(UnirestInstance unirest, FoDReleaseDe JsonNode node = objectMapper.createObjectNode() .put("scanId", startScanResponse.getScanId()) .put("scanType", FoDScanType.Static.name()) + .put("releaseAndScanId", String.format("%s:%s", releaseDescriptor.getReleaseId(), startScanResponse.getScanId())) .put("analysisStatusType", "Pending") .put("applicationName", releaseDescriptor.getApplicationName()) .put("releaseName", releaseDescriptor.getReleaseName()) 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/_common/output/mixin/FoDProductHelperBasicMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDUnirestInstanceSupplierMixin.java similarity index 92% rename from fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/mixin/FoDProductHelperBasicMixin.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDUnirestInstanceSupplierMixin.java index 9aa2b8173c..20fe544097 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/output/mixin/FoDProductHelperBasicMixin.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDUnirestInstanceSupplierMixin.java @@ -10,12 +10,11 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.fod._common.output.mixin; +package com.fortify.cli.fod._common.session.cli.mixin; import org.apache.http.impl.client.HttpClientBuilder; import com.fortify.cli.common.http.proxy.helper.ProxyHelper; -import com.fortify.cli.common.output.product.IProductHelper; import com.fortify.cli.common.rest.unirest.config.UnirestJsonHeaderConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUnexpectedHttpResponseConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUrlConfigConfigurer; @@ -28,8 +27,7 @@ import kong.unirest.UnirestInstance; import kong.unirest.apache.ApacheClient; -public class FoDProductHelperBasicMixin extends AbstractSessionUnirestInstanceSupplierMixin - implements IProductHelper +public final class FoDUnirestInstanceSupplierMixin extends AbstractSessionUnirestInstanceSupplierMixin { @Override protected final FoDSessionDescriptor getSessionDescriptor(String sessionName) { 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..59728d9256 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,8 +15,10 @@ 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.issue.cli.cmd.FoDIssueCommands; import com.fortify.cli.fod.mast_scan.cli.cmd.FoDMastScanCommands; import com.fortify.cli.fod.microservice.cli.cmd.FoDMicroserviceCommands; import com.fortify.cli.fod.oss_scan.cli.cmd.FoDOssScanCommands; @@ -42,6 +44,7 @@ // - If it makes sense to 'group' related entities, like app, microservice // and release FoDSessionCommands.class, + FoDActionCommands.class, FoDAccessControlCommands.class, FoDAppCommands.class, FoDMicroserviceCommands.class, @@ -50,6 +53,7 @@ FoDDastScanCommands.class, FoDMastScanCommands.class, FoDOssScanCommands.class, + FoDIssueCommands.class, FoDReportCommands.class, FoDRestCommands.class, diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupCreateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupCreateCommand.java index 5c62fa20c3..d18b1d0337 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupCreateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupCreateCommand.java @@ -21,7 +21,7 @@ import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.access_control.helper.FoDUserGroupCreateRequest; import com.fortify.cli.fod.access_control.helper.FoDUserGroupHelper; import com.fortify.cli.fod.access_control.helper.FoDUserHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupDeleteCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupDeleteCommand.java index 8621cf1320..0182531e58 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupDeleteCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupDeleteCommand.java @@ -18,7 +18,7 @@ import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.access_control.cli.mixin.FoDUserGroupResolverMixin; import com.fortify.cli.fod.access_control.helper.FoDUserGroupDescriptor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupGetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupGetCommand.java index 15f94e9bd1..7ced6dd002 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupGetCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupGetCommand.java @@ -17,7 +17,7 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.access_control.cli.mixin.FoDUserGroupResolverMixin; import com.fortify.cli.fod.access_control.helper.FoDUserGroupHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupListCommand.java index 3a8f4cfd0a..74e1e19e59 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupListCommand.java @@ -19,7 +19,7 @@ import com.fortify.cli.common.rest.query.IServerSideQueryParamGeneratorSupplier; import com.fortify.cli.common.rest.query.IServerSideQueryParamValueGenerator; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDBaseRequestOutputCommand; +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; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupUpdateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupUpdateCommand.java index e994fccd0a..5a3161ec3c 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupUpdateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDGroupUpdateCommand.java @@ -21,7 +21,7 @@ import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.access_control.cli.mixin.FoDUserGroupResolverMixin; import com.fortify.cli.fod.access_control.helper.FoDUserGroupDescriptor; import com.fortify.cli.fod.access_control.helper.FoDUserGroupHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDRoleListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDRoleListCommand.java index 33688cb73c..7f6c8549d5 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDRoleListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDRoleListCommand.java @@ -19,7 +19,7 @@ import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.output.transform.fields.RenameFieldsTransformer; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDBaseRequestOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDBaseRequestOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.rest.lookup.helper.FoDLookupType; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserCreateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserCreateCommand.java index 302979be40..4d09c8bfdc 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserCreateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserCreateCommand.java @@ -21,7 +21,7 @@ import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.access_control.helper.FoDUserCreateRequest; import com.fortify.cli.fod.access_control.helper.FoDUserGroupHelper; import com.fortify.cli.fod.access_control.helper.FoDUserHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserDeleteCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserDeleteCommand.java index 2b4d16d27a..5b3d8d5e09 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserDeleteCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserDeleteCommand.java @@ -18,7 +18,7 @@ import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.access_control.cli.mixin.FoDUserResolverMixin; import com.fortify.cli.fod.access_control.helper.FoDUserDescriptor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserGetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserGetCommand.java index 4d4cdd3ad4..ce4066b471 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserGetCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserGetCommand.java @@ -17,7 +17,7 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.access_control.cli.mixin.FoDUserResolverMixin; import com.fortify.cli.fod.access_control.helper.FoDUserHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserListCommand.java index 6830295c43..94cd4f37ba 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserListCommand.java @@ -19,7 +19,7 @@ import com.fortify.cli.common.rest.query.IServerSideQueryParamGeneratorSupplier; import com.fortify.cli.common.rest.query.IServerSideQueryParamValueGenerator; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDBaseRequestOutputCommand; +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; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserUpdateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserUpdateCommand.java index 101a20a91d..04eebf1931 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserUpdateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/access_control/cli/cmd/FoDUserUpdateCommand.java @@ -21,7 +21,7 @@ import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.access_control.cli.mixin.FoDUserResolverMixin; import com.fortify.cli.fod.access_control.helper.FoDUserDescriptor; import com.fortify.cli.fod.access_control.helper.FoDUserGroupHelper; 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..f4b9e6cbff --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionCommands.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * 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, + FoDActionSignCommand.class, + } +) +public class FoDActionCommands extends AbstractContainerCommand { +} diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastControllerProductHelperBasicMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionGetCommand.java similarity index 54% rename from fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastControllerProductHelperBasicMixin.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionGetCommand.java index c0e666d58c..3f17f674d3 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastControllerProductHelperBasicMixin.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionGetCommand.java @@ -10,19 +10,17 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.sc_sast._common.output.cli.mixin; +package com.fortify.cli.fod.action.cli.cmd; -import com.fortify.cli.common.output.product.IProductHelper; -import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; -import com.fortify.cli.sc_sast._common.session.cli.mixin.AbstractSCSastUnirestInstanceSupplierMixin; +import com.fortify.cli.common.action.cli.cmd.AbstractActionGetCommand; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; -import kong.unirest.UnirestInstance; +import picocli.CommandLine.Command; -public class SCSastControllerProductHelperBasicMixin extends AbstractSCSastUnirestInstanceSupplierMixin - implements IProductHelper, IUnirestInstanceSupplier -{ +@Command(name = OutputHelperMixins.Get.CMD_NAME) +public class FoDActionGetCommand extends AbstractActionGetCommand { @Override - public final UnirestInstance getUnirestInstance() { - return getControllerUnirestInstance(); + protected final String getType() { + return "FoD"; } -} \ No newline at end of file +} 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..ae15d3d104 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionRunCommand.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright 2021, 2023 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + *******************************************************************************/ +package com.fortify.cli.fod.action.cli.cmd; + +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.action.cli.cmd.AbstractActionRunCommand; +import com.fortify.cli.common.action.runner.ActionRunner; +import com.fortify.cli.common.action.runner.ActionRunner.ParameterTypeConverterArgs; +import com.fortify.cli.common.action.runner.ActionRunner.IActionRequestHelper.BasicActionRequestHelper; +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 @Reflectable + 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/action/cli/cmd/FoDActionSignCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionSignCommand.java new file mode 100644 index 0000000000..fdff8b54c3 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/action/cli/cmd/FoDActionSignCommand.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * 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.AbstractActionSignCommand; + +import picocli.CommandLine.Command; + +@Command(name = "sign") +public class FoDActionSignCommand extends AbstractActionSignCommand {} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppCreateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppCreateCommand.java index bfcb178798..2d3d5ec038 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppCreateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppCreateCommand.java @@ -26,7 +26,7 @@ import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.StringUtils; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.access_control.helper.FoDUserGroupHelper; import com.fortify.cli.fod.access_control.helper.FoDUserHelper; import com.fortify.cli.fod.app.attr.cli.mixin.FoDAttributeUpdateOptions; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppDeleteCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppDeleteCommand.java index b3493da3af..d31f90b6a1 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppDeleteCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppDeleteCommand.java @@ -16,7 +16,7 @@ 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.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; import com.fortify.cli.fod.app.helper.FoDAppDescriptor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppGetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppGetCommand.java index 032c5120d7..fbfc8564f4 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppGetCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppGetCommand.java @@ -15,7 +15,7 @@ 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.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; import com.fortify.cli.fod.app.helper.FoDAppHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppListCommand.java index 7c755fb080..063061f70f 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppListCommand.java @@ -17,7 +17,7 @@ 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.AbstractFoDBaseRequestOutputCommand; +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; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppScanListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppScanListCommand.java index dc823cbd79..7875047ffc 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppScanListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppScanListCommand.java @@ -14,7 +14,7 @@ import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; -import com.fortify.cli.fod._common.output.cli.AbstractFoDBaseRequestOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDBaseRequestOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.scan.helper.FoDScanHelper; import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppUpdateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppUpdateCommand.java index b14f244aec..8bf3064f97 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppUpdateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppUpdateCommand.java @@ -24,7 +24,7 @@ import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.StringUtils; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.app.attr.cli.mixin.FoDAttributeUpdateOptions; import com.fortify.cli.fod.app.attr.helper.FoDAttributeDescriptor; import com.fortify.cli.fod.app.attr.helper.FoDAttributeHelper; 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 cf75daee5e..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 @@ -12,10 +12,11 @@ */ package com.fortify.cli.fod.dast_scan.cli.cmd; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; 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 706820456f..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,14 +12,18 @@ */ 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.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanSetupCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanAssessmentTypeDescriptor; import com.fortify.cli.fod._common.scan.helper.FoDScanHelper; 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 3decb4761d..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,8 +12,11 @@ */ 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.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanSetupCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanAssessmentTypeDescriptor; @@ -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 938008d5bc..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,8 +12,10 @@ */ 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.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanSetupCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanAssessmentTypeDescriptor; @@ -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/FoDDastLegacyScanGetConfigCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastLegacyScanGetConfigCommand.java index 2257d1f687..23b7491b9a 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastLegacyScanGetConfigCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastLegacyScanGetConfigCommand.java @@ -12,7 +12,7 @@ */ package com.fortify.cli.fod.dast_scan.cli.cmd; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanConfigGetCommand; import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastLegacyDescriptor; import com.fortify.cli.fod.dast_scan.helper.FoDScanConfigDastLegacyHelper; 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 7cf59e3b3f..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,14 +20,13 @@ 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; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin; import com.fortify.cli.common.util.FcliBuildPropertiesHelper; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanStartCommand; import com.fortify.cli.fod._common.scan.cli.mixin.FoDEntitlementFrequencyTypeMixins; import com.fortify.cli.fod._common.scan.cli.mixin.FoDInProgressScanActionTypeMixins; @@ -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/FoDDastScanCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanCommands.java index df5683ffc7..62d4656305 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanCommands.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanCommands.java @@ -37,6 +37,6 @@ FoDDastScanFileUploadCommand.class } ) -@DefaultVariablePropertyName("scanId") +@DefaultVariablePropertyName("releaseAndScanId") public class FoDDastScanCommands extends AbstractContainerCommand { } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanDownloadLatestCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanDownloadLatestCommand.java index 12abf12c5d..7d9f7b83c3 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanDownloadLatestCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastScanDownloadLatestCommand.java @@ -13,7 +13,7 @@ package com.fortify.cli.fod.dast_scan.cli.cmd; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanDownloadLatestFprCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanType; 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 c55df66372..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 @@ -12,10 +12,11 @@ */ package com.fortify.cli.fod.dast_scan.cli.cmd; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; 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/issue/cli/cmd/FoDIssueCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueCommands.java new file mode 100644 index 0000000000..df459bc3ae --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueCommands.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.issue.cli.cmd; + +import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; + +import picocli.CommandLine; + +@CommandLine.Command(name = "issue", + subcommands = { + FoDIssueListCommand.class, + } +) +//@DefaultVariablePropertyName("applicationId") +public class FoDIssueCommands extends AbstractContainerCommand { +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java new file mode 100644 index 0000000000..cd7fa22256 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.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.fod.issue.cli.cmd; + +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.rest.query.IServerSideQueryParamGeneratorSupplier; +import com.fortify.cli.common.rest.query.IServerSideQueryParamValueGenerator; +import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDBaseRequestOutputCommand; +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.issue.cli.mixin.FoDIssueEmbedMixin; +import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; + +import kong.unirest.HttpRequest; +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command(name = OutputHelperMixins.List.CMD_NAME) +public class FoDIssueListCommand extends AbstractFoDBaseRequestOutputCommand implements IServerSideQueryParamGeneratorSupplier { + @Getter @Mixin private OutputHelperMixins.List outputHelper; + @Mixin private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins + @Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver; + @Mixin private FoDFiltersParamMixin filterParamMixin; + @Mixin private FoDIssueEmbedMixin embedMixin; + @Getter private IServerSideQueryParamValueGenerator serverSideQueryParamGenerator = new FoDFiltersParamGenerator(); + // .add("id","applicationId") + // .add("name","applicationName") + // .add("criticality", "businessCriticalityType") + // .add("type", "applicationType"); + + @Override + public HttpRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities") + .routeParam("releaseId", releaseResolver.getReleaseId(unirest)); + } + + @Override + public boolean isSingular() { + return false; + } +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/mixin/FoDIssueEmbedMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/mixin/FoDIssueEmbedMixin.java new file mode 100644 index 0000000000..d40169eb7d --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/mixin/FoDIssueEmbedMixin.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * 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.issue.cli.mixin; + +import java.util.function.Supplier; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.util.DisableTest; +import com.fortify.cli.common.util.DisableTest.TestType; +import com.fortify.cli.fod._common.cli.mixin.AbstractFoDEmbedMixin; +import com.fortify.cli.fod._common.rest.embed.IFoDEntityEmbedder; +import com.fortify.cli.fod._common.rest.embed.IFoDEntityEmbedderSupplier; + +import kong.unirest.GetRequest; +import kong.unirest.UnirestInstance; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import picocli.CommandLine.Option; + +public class FoDIssueEmbedMixin extends AbstractFoDEmbedMixin { + @DisableTest(TestType.MULTI_OPT_PLURAL_NAME) + @Option(names = "--embed", required = false, split = ",", descriptionKey = "fcli.fod.issue.embed" ) + @Getter private FoDIssueEmbedderSupplier[] embedSuppliers; + + @RequiredArgsConstructor + public static enum FoDIssueEmbedderSupplier implements IFoDEntityEmbedderSupplier { + allData(FoDIssueAllDataEmbedder::new), + summary(FoDIssueSummaryEmbedder::new), + details(FoDIssueDetailsEmbedder::new), + recommendations(FoDIssueRecommendationsEmbedder::new), + history(FoDIssueHistoryEmbedder::new), + requestResponse(FoDIssueRequestResponseEmbedder::new), + headers(FoDIssueHeadersEmbedder::new), + parameters(FoDIssueParametersEmbedder::new), + traces(FoDIssueTracesEmbedder::new), + ; + + private final Supplier supplier; + + public IFoDEntityEmbedder createEntityEmbedder() { + return supplier.get(); + } + + private static abstract class AbstractFoDIssueEmbedder implements IFoDEntityEmbedder { + @Override + public void embed(UnirestInstance unirest, ObjectNode record) { + var releaseId = record.get("releaseId").asText(); + var vulnId = record.get("vulnId").asText(); + JsonNode response = getBaseRequest(unirest) + .routeParam("releaseId", releaseId) + .routeParam("vulnId", vulnId) + .queryString("limit", "-1") + .asObject(JsonNode.class) + .getBody(); + process(record, response); + } + + protected abstract GetRequest getBaseRequest(UnirestInstance unirest); + protected abstract void process(ObjectNode record, JsonNode response); + } + + private static final class FoDIssueAllDataEmbedder extends AbstractFoDIssueEmbedder { + @Override + protected GetRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities/{vulnId}/all-data"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("allData", response); + } + } + + private static final class FoDIssueSummaryEmbedder extends AbstractFoDIssueEmbedder { + @Override + protected GetRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities/{vulnId}/summary"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("summary", response); + } + } + + private static final class FoDIssueDetailsEmbedder extends AbstractFoDIssueEmbedder { + @Override + protected GetRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities/{vulnId}/details"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("details", response); + } + } + + private static final class FoDIssueRecommendationsEmbedder extends AbstractFoDIssueEmbedder { + @Override + protected GetRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities/{vulnId}/recommendations"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("recommendations", response); + } + } + + private static final class FoDIssueHistoryEmbedder extends AbstractFoDIssueEmbedder { + @Override + protected GetRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities/{vulnId}/history"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("history", response); + } + } + + private static final class FoDIssueRequestResponseEmbedder extends AbstractFoDIssueEmbedder { + @Override + protected GetRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities/{vulnId}/request-response"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("requestResponse", response); + } + } + + private static final class FoDIssueHeadersEmbedder extends AbstractFoDIssueEmbedder { + @Override + protected GetRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities/{vulnId}/headers"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("headers", response); + } + } + + private static final class FoDIssueParametersEmbedder extends AbstractFoDIssueEmbedder { + @Override + protected GetRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities/{vulnId}/parameters"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("parameters", response); + } + } + + private static final class FoDIssueTracesEmbedder extends AbstractFoDIssueEmbedder { + @Override + protected GetRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v3/releases/{releaseId}/vulnerabilities/{vulnId}/traces"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("traces", response); + } + } + + } +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanCommands.java index ef40c648cc..908de6d1c4 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanCommands.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanCommands.java @@ -32,6 +32,6 @@ FoDMastScanWaitForCommand.class, } ) -@DefaultVariablePropertyName("scanId") +@DefaultVariablePropertyName("releaseAndScanId") public class FoDMastScanCommands extends AbstractContainerCommand { } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanDownloadLatestCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanDownloadLatestCommand.java index c1f26fa3a2..724f48212d 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanDownloadLatestCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanDownloadLatestCommand.java @@ -13,7 +13,7 @@ package com.fortify.cli.fod.mast_scan.cli.cmd; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanDownloadLatestFprCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanType; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanGetConfigCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanGetConfigCommand.java index 5808a1267a..6021d75600 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanGetConfigCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanGetConfigCommand.java @@ -12,7 +12,7 @@ */ package com.fortify.cli.fod.mast_scan.cli.cmd; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanConfigGetCommand; import com.fortify.cli.fod.mast_scan.helper.FoDScanConfigMobileDescriptor; import com.fortify.cli.fod.mast_scan.helper.FoDScanConfigMobileHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceCreateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceCreateCommand.java index d8bbead91c..72586b1d67 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceCreateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceCreateCommand.java @@ -17,7 +17,7 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.app.helper.FoDAppDescriptor; import com.fortify.cli.fod.microservice.cli.mixin.FoDMicroserviceByQualifiedNameResolverMixin; import com.fortify.cli.fod.microservice.helper.FoDMicroserviceDescriptor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceDeleteCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceDeleteCommand.java index b09a9e7def..b5a479df43 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceDeleteCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceDeleteCommand.java @@ -17,7 +17,7 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.microservice.cli.mixin.FoDMicroserviceByQualifiedNameResolverMixin; import com.fortify.cli.fod.microservice.helper.FoDMicroserviceDescriptor; import com.fortify.cli.fod.microservice.helper.FoDMicroserviceHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceListCommand.java index fabb92f61e..20b32a5fd0 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceListCommand.java @@ -17,7 +17,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; 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.AbstractFoDBaseRequestOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDBaseRequestOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.app.cli.mixin.FoDAppResolverMixin; import com.fortify.cli.fod.app.helper.FoDAppDescriptor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceUpdateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceUpdateCommand.java index ad85557478..b02947395d 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceUpdateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/cli/cmd/FoDMicroserviceUpdateCommand.java @@ -17,7 +17,7 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.microservice.cli.mixin.FoDMicroserviceByQualifiedNameResolverMixin; import com.fortify.cli.fod.microservice.helper.FoDMicroserviceDescriptor; import com.fortify.cli.fod.microservice.helper.FoDMicroserviceHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanCommands.java index 1c8a9b5da3..c382394497 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanCommands.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanCommands.java @@ -32,6 +32,6 @@ FoDOssScanWaitForCommand.class, } ) -@DefaultVariablePropertyName("scanId") +@DefaultVariablePropertyName("releaseAndScanId") public class FoDOssScanCommands extends AbstractContainerCommand { } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanDownloadLatestCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanDownloadLatestCommand.java index d338464bfe..a4c6a8de64 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanDownloadLatestCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanDownloadLatestCommand.java @@ -12,7 +12,7 @@ */ package com.fortify.cli.fod.oss_scan.cli.cmd; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanDownloadLatestCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanDescriptor; import com.fortify.cli.fod._common.scan.helper.FoDScanType; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseAssessmentTypeListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseAssessmentTypeListCommand.java index 54f463b24c..acdac8dfe2 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseAssessmentTypeListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseAssessmentTypeListCommand.java @@ -24,7 +24,7 @@ import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.rest.helper.FoDInputTransformer; import com.fortify.cli.fod._common.scan.helper.FoDScanType; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseCreateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseCreateCommand.java index 07d434f45f..1e4ab326eb 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseCreateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseCreateCommand.java @@ -19,7 +19,7 @@ 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.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.app.cli.mixin.FoDSdlcStatusTypeOptions; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameResolverMixin; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseDeleteCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseDeleteCommand.java index 7c08c80512..b1c63fb360 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseDeleteCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseDeleteCommand.java @@ -18,7 +18,7 @@ import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseGetCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseGetCommand.java index c006cc84b1..479def8e74 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseGetCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseGetCommand.java @@ -17,7 +17,7 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IRecordTransformer; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import com.fortify.cli.fod.release.helper.FoDReleaseHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseListCommand.java index 379f5c06a4..d6c6514178 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseListCommand.java @@ -18,7 +18,7 @@ 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.AbstractFoDBaseRequestOutputCommand; +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; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseScanListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseScanListCommand.java index 7609e53927..9045eb61cc 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseScanListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseScanListCommand.java @@ -15,7 +15,7 @@ import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDBaseRequestOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDBaseRequestOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.scan.helper.FoDScanHelper; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseUpdateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseUpdateCommand.java index 339a990b96..6911e49ac3 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseUpdateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseUpdateCommand.java @@ -19,7 +19,7 @@ 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.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.app.cli.mixin.FoDSdlcStatusTypeOptions; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; 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 85fccc8d97..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,16 +16,16 @@ 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.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; import com.fortify.cli.fod.report.cli.mixin.FoDReportTemplateByNameOrIdResolverMixin; 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 0eba412ee8..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.AbstractFoDJsonNodeOutputCommand; +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 40e73fbac3..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.AbstractFoDJsonNodeOutputCommand; +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 296688d07f..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.AbstractFoDJsonNodeOutputCommand; +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 3ab84c3c0e..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.AbstractFoDBaseRequestOutputCommand; +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 76e5934ed7..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 @@ -15,11 +15,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.transform.IRecordTransformer; -import com.fortify.cli.fod._common.output.cli.AbstractFoDBaseRequestOutputCommand; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDBaseRequestOutputCommand; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; 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 8031891405..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,25 +12,26 @@ *******************************************************************************/ 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.output.mixin.FoDProductHelperStandardMixin; 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 FoDProductHelperStandardMixin productHelper; + @Getter @Mixin private FoDUnirestInstanceSupplierMixin unirestInstanceSupplier; @Mixin private FoDReportResolverMixin.PositionalParameterMulti reportResolver; @Option(names={"-s", "--any-state"}, required=true, split=",", defaultValue="Completed", completionCandidates = FoDReportStatusIterable.class) private Set states; 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/java/com/fortify/cli/fod/rest/cli/cmd/FoDRestCallCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/rest/cli/cmd/FoDRestCallCommand.java index fa535ad085..38df9f1006 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/rest/cli/cmd/FoDRestCallCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/rest/cli/cmd/FoDRestCallCommand.java @@ -12,15 +12,12 @@ *******************************************************************************/ package com.fortify.cli.fod.rest.cli.cmd; -import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.rest.cli.cmd.AbstractRestCallCommand; -import com.fortify.cli.common.rest.paging.INextPageUrlProducer; import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.DisableTest.TestType; -import com.fortify.cli.fod._common.output.mixin.FoDProductHelperBasicMixin; -import com.fortify.cli.fod._common.rest.helper.FoDInputTransformer; -import com.fortify.cli.fod._common.rest.helper.FoDPagingHelper; +import com.fortify.cli.fod._common.rest.helper.FoDProductHelper; +import com.fortify.cli.fod._common.session.cli.mixin.FoDUnirestInstanceSupplierMixin; import lombok.Getter; import picocli.CommandLine.Command; @@ -30,20 +27,6 @@ @DisableTest(TestType.CMD_DEFAULT_TABLE_OPTIONS_PRESENT) // Output columns depend on response contents public final class FoDRestCallCommand extends AbstractRestCallCommand { @Getter @Mixin private OutputHelperMixins.RestCall outputHelper; - @Getter @Mixin private FoDProductHelperBasicMixin productHelper; - - @Override - protected INextPageUrlProducer _getNextPageUrlProducer() { - return FoDPagingHelper.nextPageUrlProducer(); - } - - @Override - protected JsonNode _transformInput(JsonNode input) { - return FoDInputTransformer.getItems(input); - } - - @Override - protected JsonNode _transformRecord(JsonNode input) { - return input; - } + @Getter @Mixin private FoDUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final FoDProductHelper productHelper = FoDProductHelper.INSTANCE; } \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/rest/lookup/cli/cmd/FoDLookupCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/rest/lookup/cli/cmd/FoDLookupCommand.java index 634608963e..fed7b27e61 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/rest/lookup/cli/cmd/FoDLookupCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/rest/lookup/cli/cmd/FoDLookupCommand.java @@ -15,8 +15,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.cli.util.EnvSuffix; import com.fortify.cli.common.output.transform.IRecordTransformer; -import com.fortify.cli.fod._common.output.cli.AbstractFoDBaseRequestOutputCommand; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDBaseRequestOutputCommand; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod.rest.lookup.helper.FoDLookupHelper; import com.fortify.cli.fod.rest.lookup.helper.FoDLookupType; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanCommands.java index e50315da0b..13b720b6c4 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanCommands.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanCommands.java @@ -32,6 +32,6 @@ FoDSastScanWaitForCommand.class, } ) -@DefaultVariablePropertyName("scanId") +@DefaultVariablePropertyName("releaseAndScanId") public class FoDSastScanCommands extends AbstractContainerCommand { } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanDownloadLatestCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanDownloadLatestCommand.java index 46698d1970..b6954c6625 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanDownloadLatestCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanDownloadLatestCommand.java @@ -13,7 +13,7 @@ package com.fortify.cli.fod.sast_scan.cli.cmd; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanDownloadLatestFprCommand; import com.fortify.cli.fod._common.scan.helper.FoDScanType; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanGetConfigCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanGetConfigCommand.java index 14d7fafb38..8759d87a37 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanGetConfigCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanGetConfigCommand.java @@ -12,7 +12,7 @@ */ package com.fortify.cli.fod.sast_scan.cli.cmd; -import com.fortify.cli.fod._common.output.mixin.FoDOutputHelperMixins; +import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanConfigGetCommand; import com.fortify.cli.fod.sast_scan.helper.FoDScanConfigSastDescriptor; import com.fortify.cli.fod.sast_scan.helper.FoDScanConfigSastHelper; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanSetupCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanSetupCommand.java index a75305e3d7..3892889628 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanSetupCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanSetupCommand.java @@ -30,7 +30,7 @@ import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.DisableTest.TestType; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; -import com.fortify.cli.fod._common.output.cli.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.scan.cli.mixin.FoDEntitlementFrequencyTypeMixins; import com.fortify.cli.fod._common.scan.helper.FoDScanType; import com.fortify.cli.fod._common.scan.helper.sast.FoDScanSastHelper; 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..5265dc6c36 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/bitbucket-sast-report.yaml @@ -0,0 +1,116 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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-file + cliAliases: r + description: "Optional report output file name (or 'stdout' / 'stderr'). Default value: bb-fortify-report.json" + required: false + defaultValue: bb-fortify-report.json + - name: annotations-file + cliAliases: a + description: "Optional annotations output file name (or 'stdout' / 'stderr'). Default value: bb-fortify-annotations.json" + required: false + defaultValue: bb-fortify-annotations.json + - name: release + cliAliases: rel + 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: + - append: + - name: annotations + valueTemplate: annotations + - write: + - to: ${parameters['annotations-file']} + value: ${annotations?:{}} + - to: ${parameters['report-file']} + valueTemplate: report + - if: ${parameters['annotations-file']!='stdout' && parameters['report-file']!='stdout'} + to: stdout + value: | + Report written to ${parameters['report-file']} + Annotations written to ${parameters['annotations-file']} + +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/check-policy.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/check-policy.yaml new file mode 100644 index 0000000000..94f50a0a95 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/check-policy.yaml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +usage: + header: (SAMPLE) Check security policy. + description: | + This sample action demonstrates how to implement a security policy using + fcli actions, returning a non-zero exit code if any of the checks fail. + +defaults: + requestTarget: fod + +parameters: + - name: release + cliAliases: rel + description: "Required release id or :[:]" + type: release_single + +steps: + - check: + - displayName: FoD Security Policy must Pass + passIf: ${parameters.release.isPassed} + - check: + - displayName: FoD Star rating must be at least 2 stars + passIf: ${parameters.release.rating>=2} + 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..32d340f2da --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-pr-comment.yaml @@ -0,0 +1,161 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +# 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. + +author: Fortify +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: rel + description: "Required release id or :[:]" + type: release_single + - name: scan-type + cliAliases: t + description: "Scan type for which to list vulnerabilities. Default value: Static" + required: true + defaultValue: Static + - 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 + filters: scantype:${parameters['scan-type']} + 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: + - append: + - if: ${issue.status=='Fix Validated'} + name: removedIssues + valueTemplate: mdIssueListItem + - if: ${(issue.status=='New' || issue.status=='Reopen')} + name: newIssues + valueTemplate: mdIssueListItem + - if: ${(issue.status=='New' || issue.status=='Reopen') && issue.engineType=='Static'} + name: jsonSourceCodeComments + valueTemplate: jsonSourceCodeComment + + - progress: Generating GitHub request + - set: + - name: reviewBody + valueTemplate: reviewBody + - name: reviewRequestBody + valueTemplate: 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: | + ## Fortify vulnerability summary + + ### 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..585b9862b0 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-sast-report.yaml @@ -0,0 +1,170 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +# For now, github-sast-report and sarif-sast-report actions are exactly the same, apart from the +# following: +# - Different usage information +# - Different default Optional 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. + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). Default value: gh-fortify-sast.sarif" + required: false + defaultValue: gh-fortify-sast.sarif + - name: release + cliAliases: rel + 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: + - append: + - if: ${ruleCache==null || ruleCache[issue.checkId]==null} + name: rules + valueTemplate: rules + - name: ruleCache + property: ${issue.checkId} + value: true + - name: results + valueTemplate: results + - write: + - to: ${parameters.file} + valueTemplate: github-sast-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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?:{}} + properties: + copyright: ${#copyright()} + applicationName: ${parameters.release.applicationName} + applicationId: ${parameters.release.applicationId} + releaseName: ${parameters.release.releaseName} + releaseId: ${parameters.release.releaseId} + results: ${results?:{}} + + - name: rules + contents: + id: ${issue.checkId} + shortDescription: + text: ${issue.category} + fullDescription: + text: | + ## ${issue.category} + + ${#cleanRuleDescription(issue.details?.summary)} + help: + text: | + ${#cleanRuleDescription(issue.details?.explanation)?:'No explanation available'} + + ## Recommendations + + ${#cleanRuleDescription(issue.recommendations?.recommendations)?:'Not available'} + + ## Tips + + ${#cleanRuleDescription(issue.recommendations?.tips)?:'Not available'} + + ## References + + ${#numberedList(#cleanRuleDescription(issue.recommendations?.references)?.split('\n'))?:'Not available'} + + ${#copyright()} + + - name: results + contents: + ruleId: ${issue.checkId} + message: + text: ${#cleanIssueDescription(issue.details?.summary)} [More information](${#fod.issueBrowserUrl(issue)}) + 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..c3f469b9de --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-dast-report.yaml @@ -0,0 +1,160 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). Default value: gl-fortify-dast.json" + required: false + defaultValue: gl-fortify-dast.json + - name: release + cliAliases: rel + 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: + - debug: "Site tree unavailable: ${exception.getMessage()}" + - 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: + - append: + - name: vulnerabilities + valueTemplate: vulnerabilities + - write: + - to: ${parameters.file} + valueTemplate: gitlab-dast-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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: #fod.issueBrowserUrl(issue), + type: "issueInstanceId", + value: issue.instanceId + } + }} + links: + - name: Additional issue details, including analysis trace, in Fortify on Demand + url: ${#fod.issueBrowserUrl(issue)} + # 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..26b530ca86 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/gitlab-sast-report.yaml @@ -0,0 +1,111 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). Default value: gl-fortify-sast.json" + required: false + defaultValue: gl-fortify-sast.json + - name: release + cliAliases: rel + 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: + - append: + - name: vulnerabilities + valueTemplate: vulnerabilities + - write: + - to: ${parameters.file} + valueTemplate: gitlab-sast-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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: #fod.issueBrowserUrl(issue), + 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: ${#fod.issueBrowserUrl(issue)} + 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/release-summary.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/release-summary.yaml new file mode 100644 index 0000000000..1be7ae9e36 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/release-summary.yaml @@ -0,0 +1,62 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +usage: + header: (PREVIEW) Generate release summary. + description: | + This action generates a short summary listing issue counts and other statistics + for a given release. Based on user feedback on this initial version of this action, + parameters and output of this action may change in the next couple of fcli releases. + +defaults: + requestTarget: fod + +parameters: + - name: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). Default value: stdout" + required: false + defaultValue: stdout + - name: release + cliAliases: rel + description: "Required release id or :[:]" + type: release_single + +steps: + - set: + # Add short alias for release object, as we reference it a lot + - name: r + value: ${parameters.release} + # Define output date format + - name: dateFmt + value: YYYY-MM-dd HH:mm + - write: + - to: ${parameters.file} + valueTemplate: summary-md + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +valueTemplates: + - name: summary-md + contents: | + # Fortify on Demand Release Summary + + ## [${r.applicationName}${#isNotBlank(r.microserviceNae)?'- '+r.microserviceName:''} - ${r.releaseName}](${#fod.releaseBrowserUrl(r)}) + + Summary generated on: ${#formatDateTime(dateFmt)} + + ### Security Policy + **Rating:** ${#repeat("★", r.rating)}${#repeat("☆", 5-r.rating)} + **Status:** ${r.isPassed?'Pass':'Fail'} + + ### Issue Counts + | Type | Last Scan Date | Critical | High | Medium | Low | + | ----------- | ---------------- | -------- | -------- | -------- | -------- | + | **Static** | ${(#isBlank(r.staticScanDate)?#fmt('%-16s', 'N/A'):#formatDateTime(dateFmt, r.staticScanDate)) +' | '+#fmt('%8s', r.staticCritical) +' | '+#fmt('%8s', r.staticHigh) +' | '+#fmt('%8s', r.staticMedium) +' | '+#fmt('%8s', r.staticLow) +' |'} + | **Dynamic** | ${(#isBlank(r.dynamicScanDate)?#fmt('%-16s', 'N/A'):#formatDateTime(dateFmt, r.dynamicScanDate))+' | '+#fmt('%8s', r.dynamicCritical) +' | '+#fmt('%8s', r.dynamicHigh) +' | '+#fmt('%8s', r.dynamicMedium) +' | '+#fmt('%8s', r.dynamicLow) +' |'} + | **Mobile** | ${(#isBlank(r.mobileScanDate)?#fmt('%-16s', 'N/A'):#formatDateTime(dateFmt, r.mobileScanDate)) +' | '+#fmt('%8s', r.mobileCritical) +' | '+#fmt('%8s', r.mobileHigh) +' | '+#fmt('%8s', r.mobileMedium) +' | '+#fmt('%8s', r.mobileLow) +' |'} + | **Total** | | ${#fmt('%8s', r.staticCritical+r.dynamicCritical+r.mobileCritical)+' | '+#fmt('%8s', r.staticHigh+r.dynamicHigh+r.mobileHigh)+' | '+#fmt('%8s', r.staticMedium+r.dynamicMedium+r.mobileMedium)+' | '+#fmt('%8s', r.staticLow+r.dynamicLow+r.mobileLow)+' |'} + + \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sarif-sast-report.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sarif-sast-report.yaml new file mode 100644 index 0000000000..529340c79d --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sarif-sast-report.yaml @@ -0,0 +1,169 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +# For now, github-sast-report and sarif-sast-report actions are exactly the same, apart from the +# following: +# - Different usage information +# - Different default Optional 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. + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). Default value: fortify-sast.sarif" + required: false + defaultValue: fortify-sast.sarif + - name: release + cliAliases: rel + 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: + - append: + - if: ${ruleCache==null || ruleCache[issue.checkId]==null} + name: rules + valueTemplate: rules + - name: ruleCache + property: ${issue.checkId} + value: true + - name: results + valueTemplate: results + - write: + - to: ${parameters.file} + valueTemplate: github-sast-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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?:{}} + properties: + copyright: ${#copyright()} + applicationName: ${parameters.release.applicationName} + applicationId: ${parameters.release.applicationId} + releaseName: ${parameters.release.releaseName} + releaseId: ${parameters.release.releaseId} + results: ${results?:{}} + + - name: rules + contents: + id: ${issue.checkId} + shortDescription: + text: ${issue.category} + fullDescription: + text: | + ## ${issue.category} + + ${#cleanRuleDescription(issue.details?.summary)} + help: + text: | + ${#cleanRuleDescription(issue.details?.explanation)?:'No explanation available'} + + ## Recommendations + + ${#cleanRuleDescription(issue.recommendations?.recommendations)?:'Not available'} + + ## Tips + + ${#cleanRuleDescription(issue.recommendations?.tips)?:'Not available'} + + ## References + + ${#numberedList(#cleanRuleDescription(issue.recommendations?.references)?.split('\n'))?:'Not available'} + + ${#copyright()} + + - name: results + contents: + ruleId: ${issue.checkId} + message: + text: ${#cleanIssueDescription(issue.details?.summary)} [More information](${#fod.issueBrowserUrl(issue)}) + 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..8d6201c279 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/sonarqube-sast-report.yaml @@ -0,0 +1,71 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). 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: rel + 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: + - append: + - name: sq_issues + valueTemplate: sq_issues + + - write: + - to: ${parameters.file} + valueTemplate: sq-sast-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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..5727294e73 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 @@ -1,5 +1,6 @@ # Used to 'productize' some descriptions defined in FortifyCLIMessages.properties product=FoD +module=fod # Make sure none of the commands inherit usage header or description of the top-level fcli command # Note that we define these as usage.* whereas our parent bundle defines fcli.usage.* (with fcli prefix). @@ -151,8 +152,82 @@ 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 +# Apart from the top-level usage header, which includes a FoD reference, all headers +# and descriptions are the same as for other action modules like SSC. When updating here, +# the same updates should be made in other modules. +fcli.fod.action.usage.header = Manage FoD actions: data export, integration, automation & more. +fcli.fod.action.get.usage.header = Get action contents. +fcli.fod.action.help.usage.header = Show action usage help. +fcli.fod.action.import.usage.header = Import custom actions. +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.sign.usage.header = Sign action. +fcli.fod.action.sign.confirm = Confirm overwriting existing output file. +fcli.fod.action.sign.confirmPrompt = Do you want overwrite existing output file %s? + +fcli.action.source.single = The action can be specified as either a simple name or a local or \ + remote action YAML file location. If specified as a simple name, the action will be loaded from \ + the list of built-in and imported custom actions unless the `--from-zip` option is specified, \ + in which case the action will be loaded from the given local or remote zip file. The `--from-zip` \ + option will only be used if action is specified as a simple name, it will be ignored if the action \ + is specified as a local or remote action YAML file location. +fcli.fod.action.usage.description = Fcli supports workflow-style actions defined in YAML files. Many \ + built-in actions are provided, focusing on data export and CI/CD integration. Users can also develop \ + their own custom actions, either from scratch or by customizing built-in actions. If you require any \ + assistance with developing custom actions, please consult with Fortify Professional Services. \ + %n%nNote that the ability to load and run custom actions is currently considered PREVIEW \ + functionality; custom actions developed for this fcli version may fail on other fcli versions, \ + even between minor fcli releases. Based on user feedback, we will stabilize action syntax over \ + the next couple of fcli releases, after which any breaking action syntax changes will be considered \ + a major fcli version change. \ + %n%nThis fcli version supports the following action schema versions: ${fcli.action.supportedSchemaVersions:-See fcli help output}. \ + %n%nActions can potentially perform dangerous operations like deleting data or posting data to 3rd-party \ + systems, so it is recommended to only run trusted actions. Action authors can sign their actions using \ + the `action sign` command; actions without a (valid) signature will require confirmation when trying to \ + run them. Trusted public keys can be configured through the `fcli config public-key` commands, \ + or passed directly using the `--pubkey` option on various action-related commands. +fcli.fod.action.get.usage.description = This command allows for listing the YAML contents of built-in or \ + custom actions. This allows for reviewing the operations performed by an action, or for using the action \ + contents as a basis for developing custom actions. \ + %n%n${fcli.action.source.single} +fcli.fod.action.help.usage.description = This command allows for showing the help information for the given \ + built-in or custom action. \ + ${fcli.action.source.single} +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%n${fcli.action.source.single} \ + If only `--from-zip` is specified, all actions from that zip-file will be imported. +fcli.fod.action.list.usage.description = By default, this command lists available built-in and \ + previously imported custom actions. If the `--from-zip` option is specified, this command \ + lists available actions from the given local or remote zip file instead. +fcli.fod.action.reset.usage.description = Remove all previously imported custom actions, restoring \ + fcli configuration to provide the default built-in actions only. +fcli.fod.action.run.usage.description = This command allows for running built-in or custom actions. As actions \ + may perform potentially dangerous operations like deleting data or posting data to 3rd-party systems, you should \ + only run trusted actions. For this reason, fcli requires confirmation when attempting to run an action without \ + a (valid) signature. \ + %n%n${fcli.action.source.single} +fcli.fod.action.sign.usage.description = This command allows for signing custom actions, allowing those actions \ + to be run without confirmation if the corresponding public key has been imported through the \ + `fcli config public-key import` command or passed in the `--pubkey` option on various action \ + commands. The action to be signed must be a local file. \ + %n%nThis command can use an existing private key for signing, or generate a new key pair if the \ + private key file as specified through the `--with` option doesn't yet exist and `--pubout` is \ + specified to output the corresponding public key. \ + %n%nPrivate keys may also be generated using OpenSSL or similar tools, but note that only RSA keys in PEM format are supported, \ + and only a small set of encryption schemes are supported for encrypted private keys. It is recommended to use AES \ + encryption, which is supported by both native fcli executables and the .jar version of fcli. The latter requires \ + Java 19 or above though to handle AES-encrypted private keys. Following is a sample OpenSSL command for generating an \ + encrypted private key that's supported by fcli for signing: %n openssl genpkey -algorithm rsa -out private-key.pem -aes256 \ + %n%nFor convenience, when using a pre-existing private key, the `--pubout` option allows for outputting the corresponding \ + public key for use by the `fcli config public-key import` command. Note that public keys will not be automatically added \ + to the fcli trusted public key store; even if this command generates a key pair on the fly, you'll still need to import \ + the generated public key using the `fcli config public-key import` command. + +# fcli fod access-control fcli.fod.access-control.usage.header = Manage FoD users & groups. fcli.fod.access-control.list-roles.usage.header = List user roles. @@ -196,7 +271,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 +319,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 +339,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 +373,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 +389,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 +420,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 +490,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 +653,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 +711,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 +746,18 @@ 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 issue +fcli.fod.issue.usage.header = Manage FoD issues (vulnerabilities) and related entities. +fcli.fod.issue.list.usage.header = List vulnerabilities. +# TODO --embed option yet to be added. +fcli.fod.issue.embed = Embed extra issue data. Due to FoD rate limits, this may significantly \ + affect performance. Allowed values: ${COMPLETION-CANDIDATES}. \ + Using the --output option, this extra data can be included in the output. Using the --query option, \ + this extra data can be queried upon. To get an understanding of the structure and contents of the \ + embedded data, use the --output json or --output yaml options. + +# 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 +812,9 @@ 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,author,origin,status,signatureStatus,usageHeader +fcli.fod.action.import.output.table.options = name,author,status,signatureStatus,usageHeader +fcli.ssc.action.sign.output.table.options = in,out,publicKeyFingerprint 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 @@ -745,7 +832,9 @@ fcli.fod.*-scan-config.output.table.options = applicationName,microserviceName,r fcli.fod.*-scan-setup.output.table.options = scanType,setupType,fileId,filename,applicationName,microserviceName,releaseName,entitlementId fcli.fod.*-scan-start.output.table.options = scanId,scanType,analysisStatusType,applicationName,microserviceName,releaseName fcli.fod.*-scan-upload-file.output.table.options = fileId,fileType,filename,applicationName,microserviceName,releaseName +fcli.fod.*-scan-wait-for.output.table.options = scanId,analysisStatusType,scanType fcli.fod.session.output.table.options = name,type,url,created,expires,expired fcli.fod.rest.lookup.output.table.options = group,text,value fcli.fod.report.output.table.options = reportId,reportName,reportStatusType,reportType fcli.fod.report.report-template.output.table.options = value,text,group +fcli.fod.issue.list.output.table.options = id,primaryLocation,lineNumber,category diff --git a/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/_common/action/AbstractActionTest.java b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/_common/action/AbstractActionTest.java new file mode 100644 index 0000000000..e814c73042 --- /dev/null +++ b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/_common/action/AbstractActionTest.java @@ -0,0 +1,66 @@ +/** + * 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.fod._common.action; + +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fortify.cli.common.action.helper.ActionLoaderHelper; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionSource; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler.ActionInvalidSchemaVersionHandler; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler.ActionInvalidSignatureHandler; +import com.fortify.cli.common.action.model.Action.ActionMetadata; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignedTextDescriptor; + +// TODO Move this class to a common test utility module; currently +// exact copies of this class are available in every module +// that performs action tests. +@TestInstance(Lifecycle.PER_CLASS) +public abstract class AbstractActionTest { + @ParameterizedTest + @MethodSource("getActions") + public void testLoadAction(String name) { + try { + var actionValidationHandler = ActionValidationHandler.builder() + .onSignatureStatusDefault(invalidSignatureHandler()) + .onUnsupportedSchemaVersion(ActionInvalidSchemaVersionHandler.fail) + .build(); + ActionLoaderHelper + .load(ActionSource.builtinActionSources(getType()), name, actionValidationHandler) + .getAction(); + } catch ( Exception e ) { + System.err.println(String.format("Error loading %s action %s:\n%s", getType(), name, e)); + Assertions.fail(String.format("Error loading %s action %s", getType(), name), e); + } + } + + public final String[] getActions() { + return ActionLoaderHelper.streamAsJson(ActionSource.builtinActionSources(getType()), ActionValidationHandler.IGNORE) + .map(a->a.get("name").asText()) + .toArray(String[]::new); + } + + private static final BiConsumer invalidSignatureHandler() { + return "true".equalsIgnoreCase((System.getProperty("test.action.requireValidSignature"))) + ? ActionInvalidSignatureHandler.fail + : ActionInvalidSignatureHandler.warn; + } + + protected abstract String getType(); +} \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/_common/action/FoDActionTest.java b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/_common/action/FoDActionTest.java new file mode 100644 index 0000000000..80b1a9e674 --- /dev/null +++ b/fcli-core/fcli-fod/src/test/java/com/fortify/cli/fod/_common/action/FoDActionTest.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * 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._common.action; + +import lombok.Getter; + +public class FoDActionTest extends AbstractActionTest { + @Getter private final String type = "FoD"; +} diff --git a/fcli-core/fcli-license/src/main/java/com/fortify/cli/license/msp_report/collector/MspReportAppScanCollector.java b/fcli-core/fcli-license/src/main/java/com/fortify/cli/license/msp_report/collector/MspReportAppScanCollector.java index 47b9bab0f6..98624c2e41 100644 --- a/fcli-core/fcli-license/src/main/java/com/fortify/cli/license/msp_report/collector/MspReportAppScanCollector.java +++ b/fcli-core/fcli-license/src/main/java/com/fortify/cli/license/msp_report/collector/MspReportAppScanCollector.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.rest.unirest.config.IUrlConfig; +import com.fortify.cli.common.util.Break; import com.fortify.cli.license.msp_report.config.MspReportConfig; import com.fortify.cli.license.msp_report.generator.ssc.MspReportSSCAppDescriptor; import com.fortify.cli.license.msp_report.generator.ssc.MspReportSSCAppSummaryDescriptor; @@ -62,12 +63,12 @@ public MspReportSSCAppSummaryDescriptor summary() { /** * Report an SSC application version scan. - * @return {@link MspReportScanCollectorState#DONE} is we're done processing - * this application version, {@link MspReportScanCollectorState#DONE} + * @return {@link Break#TRUE} is we're done processing + * this application version, {@link Break#TRUE} * otherwise. */ @SneakyThrows - public MspReportScanCollectorState report(MspReportSSCScanDescriptor scanDescriptor) { + public Break report(MspReportSSCScanDescriptor scanDescriptor) { addScan(scanDescriptor); return continueOrDone(scanDescriptor); } @@ -99,7 +100,7 @@ public MspReportScanCollectorState report(MspReportSSCScanDescriptor scanDescrip *
  • Both uploadDate and lastScanDate within reporting period
  • * */ - private MspReportScanCollectorState continueOrDone(MspReportSSCScanDescriptor scanDescriptor) { + private Break continueOrDone(MspReportSSCScanDescriptor scanDescriptor) { /* var licenseType = appDescriptor.getMspLicenseType(); var uploadDateTime = scanDescriptor.getArtifactUploadDate(); @@ -110,7 +111,7 @@ else if ( licenseType!=MspReportLicenseType.Application && isBeforeReportingStar return MspReportScanCollectorState.DONE; } */ - return MspReportScanCollectorState.CONTINUE; + return Break.FALSE; } private void addScan(MspReportSSCScanDescriptor scanDescriptor) { @@ -267,10 +268,6 @@ private MspReportSSCScanDescriptor getPreviousScan(MspReportSSCScanDescriptor de return previousScan; } - public static enum MspReportScanCollectorState { - CONTINUE, DONE - } - @RequiredArgsConstructor public static enum MspReportArtifactEntitlementConsumedReason { beforeContractStartDate(MspReportArtifactEntitlementConsumed.none, true), diff --git a/fcli-core/fcli-license/src/main/java/com/fortify/cli/license/msp_report/generator/ssc/MspReportSSCResultsGenerator.java b/fcli-core/fcli-license/src/main/java/com/fortify/cli/license/msp_report/generator/ssc/MspReportSSCResultsGenerator.java index 0d2e4f0e41..473c6b11cc 100644 --- a/fcli-core/fcli-license/src/main/java/com/fortify/cli/license/msp_report/generator/ssc/MspReportSSCResultsGenerator.java +++ b/fcli-core/fcli-license/src/main/java/com/fortify/cli/license/msp_report/generator/ssc/MspReportSSCResultsGenerator.java @@ -19,9 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.util.Break; import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.license.msp_report.collector.MspReportAppScanCollector; -import com.fortify.cli.license.msp_report.collector.MspReportAppScanCollector.MspReportScanCollectorState; import com.fortify.cli.license.msp_report.collector.MspReportResultsCollector; import com.fortify.cli.license.msp_report.config.MspReportSSCSourceConfig; import com.fortify.cli.license.msp_report.generator.AbstractMspReportUnirestResultsGenerator; @@ -154,7 +154,7 @@ private void processArtifactPage(JsonNode body, MspReportSSCAppVersionDescriptor .peek(d->resultsCollector().artifactCollector().report(sourceConfig(), versionDescriptor, d)) .flatMap(MspReportSSCScanDescriptor::from) .map(scanCollector::report) - .filter(MspReportScanCollectorState.DONE::equals) + .filter(Break.TRUE::equals) .findFirst() .isPresent(); continueNextPageSupplier.setLoadNextPage(!done); diff --git a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/cmd/AbstractSCDastOutputCommand.java b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/cmd/AbstractSCDastOutputCommand.java index 7385dc0987..069f763e35 100644 --- a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/cmd/AbstractSCDastOutputCommand.java +++ b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/cmd/AbstractSCDastOutputCommand.java @@ -15,7 +15,8 @@ import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; import com.fortify.cli.common.output.product.IProductHelperSupplier; import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; -import com.fortify.cli.sc_dast._common.output.cli.mixin.SCDastProductHelperStandardMixin; +import com.fortify.cli.sc_dast._common.rest.helper.SCDastProductHelper; +import com.fortify.cli.sc_dast._common.session.cli.mixin.SCDastUnirestInstanceSupplierMixin; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -24,9 +25,10 @@ public abstract class AbstractSCDastOutputCommand extends AbstractOutputCommand implements IProductHelperSupplier, IUnirestInstanceSupplier { - @Getter @Mixin SCDastProductHelperStandardMixin productHelper; + @Mixin private SCDastUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final SCDastProductHelper productHelper = SCDastProductHelper.INSTANCE; public final UnirestInstance getUnirestInstance() { - return productHelper.getUnirestInstance(); + return unirestInstanceSupplier.getUnirestInstance(); } } diff --git a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/mixin/SCDastProductHelperStandardMixin.java b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/rest/helper/SCDastProductHelper.java similarity index 79% rename from fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/mixin/SCDastProductHelperStandardMixin.java rename to fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/rest/helper/SCDastProductHelper.java index e9df93934f..7f962b6367 100644 --- a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/mixin/SCDastProductHelperStandardMixin.java +++ b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/rest/helper/SCDastProductHelper.java @@ -10,20 +10,20 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.sc_dast._common.output.cli.mixin; +package com.fortify.cli.sc_dast._common.rest.helper; 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 com.fortify.cli.sc_dast._common.rest.helper.SCDastInputTransformer; -import com.fortify.cli.sc_dast._common.rest.helper.SCDastPagingHelper; // IMPORTANT: When updating/adding any methods in this class, SCDastControllerRestCallCommand // also likely needs to be updated -public class SCDastProductHelperStandardMixin extends SCDastProductHelperBasicMixin - implements IInputTransformer, INextPageUrlProducerSupplier +public class SCDastProductHelper implements IProductHelper, IInputTransformer, INextPageUrlProducerSupplier { + public static final SCDastProductHelper INSTANCE = new SCDastProductHelper(); + private SCDastProductHelper() {} @Override public INextPageUrlProducer getNextPageUrlProducer() { return SCDastPagingHelper.nextPageUrlProducer(); diff --git a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/mixin/SCDastProductHelperBasicMixin.java b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/session/cli/mixin/SCDastUnirestInstanceSupplierMixin.java similarity index 89% rename from fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/mixin/SCDastProductHelperBasicMixin.java rename to fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/session/cli/mixin/SCDastUnirestInstanceSupplierMixin.java index 5b3d5bf6fa..e602eb5236 100644 --- a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/output/cli/mixin/SCDastProductHelperBasicMixin.java +++ b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/session/cli/mixin/SCDastUnirestInstanceSupplierMixin.java @@ -10,10 +10,9 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.sc_dast._common.output.cli.mixin; +package com.fortify.cli.sc_dast._common.session.cli.mixin; import com.fortify.cli.common.http.proxy.helper.ProxyHelper; -import com.fortify.cli.common.output.product.IProductHelper; import com.fortify.cli.common.rest.unirest.config.UnirestJsonHeaderConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUnexpectedHttpResponseConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUrlConfigConfigurer; @@ -23,9 +22,7 @@ import kong.unirest.UnirestInstance; -public class SCDastProductHelperBasicMixin extends AbstractSessionUnirestInstanceSupplierMixin - implements IProductHelper -{ +public final class SCDastUnirestInstanceSupplierMixin extends AbstractSessionUnirestInstanceSupplierMixin { @Override protected final void configure(UnirestInstance unirest, SCDastSessionDescriptor sessionDescriptor) { UnirestUnexpectedHttpResponseConfigurer.configure(unirest); diff --git a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/rest/cli/cmd/SCDastRestCallCommand.java b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/rest/cli/cmd/SCDastRestCallCommand.java index 91ab3b0456..30587fa062 100644 --- a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/rest/cli/cmd/SCDastRestCallCommand.java +++ b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/rest/cli/cmd/SCDastRestCallCommand.java @@ -12,15 +12,12 @@ *******************************************************************************/ package com.fortify.cli.sc_dast.rest.cli.cmd; -import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.rest.cli.cmd.AbstractRestCallCommand; -import com.fortify.cli.common.rest.paging.INextPageUrlProducer; import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.DisableTest.TestType; -import com.fortify.cli.sc_dast._common.output.cli.mixin.SCDastProductHelperBasicMixin; -import com.fortify.cli.sc_dast._common.rest.helper.SCDastInputTransformer; -import com.fortify.cli.sc_dast._common.rest.helper.SCDastPagingHelper; +import com.fortify.cli.sc_dast._common.rest.helper.SCDastProductHelper; +import com.fortify.cli.sc_dast._common.session.cli.mixin.SCDastUnirestInstanceSupplierMixin; import lombok.Getter; import picocli.CommandLine.Command; @@ -30,20 +27,6 @@ @DisableTest(TestType.CMD_DEFAULT_TABLE_OPTIONS_PRESENT) // Output columns depend on response contents public final class SCDastRestCallCommand extends AbstractRestCallCommand { @Getter @Mixin private OutputHelperMixins.RestCall outputHelper; - @Getter @Mixin SCDastProductHelperBasicMixin productHelper; - - @Override - protected INextPageUrlProducer _getNextPageUrlProducer() { - return SCDastPagingHelper.nextPageUrlProducer(); - } - - @Override - protected JsonNode _transformInput(JsonNode input) { - return SCDastInputTransformer.getItems(input); - } - - @Override - protected JsonNode _transformRecord(JsonNode input) { - return input; - } + @Getter @Mixin SCDastUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final SCDastProductHelper productHelper = SCDastProductHelper.INSTANCE; } \ No newline at end of file diff --git a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/scan/cli/cmd/SCDastScanWaitForCommand.java b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/scan/cli/cmd/SCDastScanWaitForCommand.java index 0d830affa9..5aaa6a2a92 100644 --- a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/scan/cli/cmd/SCDastScanWaitForCommand.java +++ b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/scan/cli/cmd/SCDastScanWaitForCommand.java @@ -17,7 +17,7 @@ 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.sc_dast._common.output.cli.mixin.SCDastProductHelperStandardMixin; +import com.fortify.cli.sc_dast._common.session.cli.mixin.SCDastUnirestInstanceSupplierMixin; import com.fortify.cli.sc_dast.scan.cli.mixin.SCDastScanResolverMixin; import com.fortify.cli.sc_dast.scan.helper.SCDastScanStatus; import com.fortify.cli.sc_dast.scan.helper.SCDastScanStatus.SCDastScanStatusIterable; @@ -30,7 +30,7 @@ @Command(name = OutputHelperMixins.WaitFor.CMD_NAME) public class SCDastScanWaitForCommand extends AbstractWaitForCommand { - @Getter @Mixin SCDastProductHelperStandardMixin productHelper; + @Getter @Mixin SCDastUnirestInstanceSupplierMixin unirestInstanceSupplier; @Mixin private SCDastScanResolverMixin.PositionalParameterMulti scansResolver; @Option(names={"-s", "--any-state"}, required=true, split=",", defaultValue="Complete", completionCandidates = SCDastScanStatusIterable.class) private Set states; diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/cmd/AbstractSCSastControllerOutputCommand.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/cmd/AbstractSCSastControllerOutputCommand.java index 3b02b53395..7029422fd2 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/cmd/AbstractSCSastControllerOutputCommand.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/cmd/AbstractSCSastControllerOutputCommand.java @@ -15,7 +15,8 @@ import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; import com.fortify.cli.common.output.product.IProductHelperSupplier; import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; -import com.fortify.cli.sc_sast._common.output.cli.mixin.SCSastControllerProductHelperStandardMixin; +import com.fortify.cli.sc_sast._common.rest.helper.SCSastControllerProductHelper; +import com.fortify.cli.sc_sast._common.session.cli.mixin.SCSastUnirestInstanceSupplierMixin; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -24,13 +25,14 @@ public abstract class AbstractSCSastControllerOutputCommand extends AbstractOutputCommand implements IProductHelperSupplier, IUnirestInstanceSupplier { - @Getter @Mixin SCSastControllerProductHelperStandardMixin productHelper; + @Getter @Mixin private SCSastUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final SCSastControllerProductHelper productHelper = SCSastControllerProductHelper.INSTANCE; public final UnirestInstance getUnirestInstance() { - return productHelper.getUnirestInstance(); + return unirestInstanceSupplier.getControllerUnirestInstance(); } protected final UnirestInstance getSscUnirestInstance() { - return productHelper.getSscUnirestInstance(); + return unirestInstanceSupplier.getSscUnirestInstance(); } } diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/cmd/AbstractSCSastSSCOutputCommand.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/cmd/AbstractSCSastSSCOutputCommand.java index 995602d297..a3558b37a8 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/cmd/AbstractSCSastSSCOutputCommand.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/cmd/AbstractSCSastSSCOutputCommand.java @@ -15,7 +15,8 @@ import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; import com.fortify.cli.common.output.product.IProductHelperSupplier; import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; -import com.fortify.cli.sc_sast._common.output.cli.mixin.SCSastSSCProductHelperStandardMixin; +import com.fortify.cli.sc_sast._common.rest.helper.SCSastSSCProductHelper; +import com.fortify.cli.sc_sast._common.session.cli.mixin.SCSastUnirestInstanceSupplierMixin; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -24,13 +25,14 @@ public abstract class AbstractSCSastSSCOutputCommand extends AbstractOutputCommand implements IProductHelperSupplier, IUnirestInstanceSupplier { - @Getter @Mixin SCSastSSCProductHelperStandardMixin productHelper; + @Getter @Mixin private SCSastUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final SCSastSSCProductHelper productHelper = SCSastSSCProductHelper.INSTANCE; public final UnirestInstance getUnirestInstance() { - return productHelper.getUnirestInstance(); + return unirestInstanceSupplier.getSscUnirestInstance(); } protected final UnirestInstance getControllerUnirestInstance() { - return productHelper.getControllerUnirestInstance(); + return unirestInstanceSupplier.getControllerUnirestInstance(); } } diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastControllerProductHelperStandardMixin.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/rest/helper/SCSastControllerProductHelper.java similarity index 75% rename from fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastControllerProductHelperStandardMixin.java rename to fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/rest/helper/SCSastControllerProductHelper.java index 8d8aa8810e..8174ff67e1 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastControllerProductHelperStandardMixin.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/rest/helper/SCSastControllerProductHelper.java @@ -10,17 +10,18 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.sc_sast._common.output.cli.mixin; +package com.fortify.cli.sc_sast._common.rest.helper; 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.sc_sast._common.rest.helper.SCSastInputTransformer; // IMPORTANT: When updating/adding any methods in this class, SCSastControllerRestCallCommand // also likely needs to be updated -public class SCSastControllerProductHelperStandardMixin extends SCSastControllerProductHelperBasicMixin - implements IInputTransformer +public class SCSastControllerProductHelper implements IProductHelper, IInputTransformer { + public static final SCSastControllerProductHelper INSTANCE = new SCSastControllerProductHelper(); + private SCSastControllerProductHelper() {} @Override public JsonNode transformInput(JsonNode input) { return SCSastInputTransformer.getItems(input); diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastSSCProductHelperStandardMixin.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/rest/helper/SCSastSSCProductHelper.java similarity index 78% rename from fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastSSCProductHelperStandardMixin.java rename to fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/rest/helper/SCSastSSCProductHelper.java index 48ce682e11..7f1404251c 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/output/cli/mixin/SCSastSSCProductHelperStandardMixin.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/rest/helper/SCSastSSCProductHelper.java @@ -10,18 +10,21 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.sc_sast._common.output.cli.mixin; +package com.fortify.cli.sc_sast._common.rest.helper; 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 com.fortify.cli.ssc._common.rest.helper.SSCInputTransformer; import com.fortify.cli.ssc._common.rest.helper.SSCPagingHelper; -public class SCSastSSCProductHelperStandardMixin extends SCSastSSCProductHelperBasicMixin - implements IInputTransformer, INextPageUrlProducerSupplier +public class SCSastSSCProductHelper implements IProductHelper, IInputTransformer, INextPageUrlProducerSupplier { + public static final SCSastSSCProductHelper INSTANCE = new SCSastSSCProductHelper(); + private SCSastSSCProductHelper() {} + @Override public JsonNode transformInput(JsonNode input) { return SSCInputTransformer.getDataOrSelf(input); diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/SCSastControllerUnirestInstanceSupplierMixin.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/SCSastControllerUnirestInstanceSupplierMixin.java new file mode 100644 index 0000000000..f731b58212 --- /dev/null +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/SCSastControllerUnirestInstanceSupplierMixin.java @@ -0,0 +1,24 @@ +/** + * 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.sc_sast._common.session.cli.mixin; + +import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; + +import kong.unirest.UnirestInstance; + +public class SCSastControllerUnirestInstanceSupplierMixin extends SCSastUnirestInstanceSupplierMixin implements IUnirestInstanceSupplier { + @Override + public UnirestInstance getUnirestInstance() { + return getControllerUnirestInstance(); + } +} diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/SCSastSSCUnirestInstanceSupplierMixin.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/SCSastSSCUnirestInstanceSupplierMixin.java new file mode 100644 index 0000000000..8902111dec --- /dev/null +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/SCSastSSCUnirestInstanceSupplierMixin.java @@ -0,0 +1,24 @@ +/** + * 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.sc_sast._common.session.cli.mixin; + +import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; + +import kong.unirest.UnirestInstance; + +public class SCSastSSCUnirestInstanceSupplierMixin extends SCSastUnirestInstanceSupplierMixin implements IUnirestInstanceSupplier { + @Override + public UnirestInstance getUnirestInstance() { + return getSscUnirestInstance(); + } +} diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/AbstractSCSastUnirestInstanceSupplierMixin.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/SCSastUnirestInstanceSupplierMixin.java similarity index 93% rename from fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/AbstractSCSastUnirestInstanceSupplierMixin.java rename to fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/SCSastUnirestInstanceSupplierMixin.java index a6baef5651..7bfbc7a613 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/AbstractSCSastUnirestInstanceSupplierMixin.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/_common/session/cli/mixin/SCSastUnirestInstanceSupplierMixin.java @@ -20,7 +20,7 @@ import kong.unirest.UnirestInstance; -public abstract class AbstractSCSastUnirestInstanceSupplierMixin extends AbstractSessionDescriptorSupplierMixin { +public class SCSastUnirestInstanceSupplierMixin extends AbstractSessionDescriptorSupplierMixin { @Override protected final SCSastSessionDescriptor getSessionDescriptor(String sessionName) { return SCSastSessionHelper.instance().get(sessionName, true); diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/rest/cli/cmd/SCSastControllerRestCallCommand.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/rest/cli/cmd/SCSastControllerRestCallCommand.java index 207dd9ac81..bb9a18feea 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/rest/cli/cmd/SCSastControllerRestCallCommand.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/rest/cli/cmd/SCSastControllerRestCallCommand.java @@ -12,14 +12,12 @@ *******************************************************************************/ package com.fortify.cli.sc_sast.rest.cli.cmd; -import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.rest.cli.cmd.AbstractRestCallCommand; -import com.fortify.cli.common.rest.paging.INextPageUrlProducer; import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.DisableTest.TestType; -import com.fortify.cli.sc_sast._common.output.cli.mixin.SCSastControllerProductHelperBasicMixin; -import com.fortify.cli.sc_sast._common.rest.helper.SCSastInputTransformer; +import com.fortify.cli.sc_sast._common.rest.helper.SCSastControllerProductHelper; +import com.fortify.cli.sc_sast._common.session.cli.mixin.SCSastControllerUnirestInstanceSupplierMixin; import lombok.Getter; import picocli.CommandLine.Command; @@ -29,20 +27,6 @@ @DisableTest(TestType.CMD_DEFAULT_TABLE_OPTIONS_PRESENT) // Output columns depend on response contents public final class SCSastControllerRestCallCommand extends AbstractRestCallCommand { @Getter @Mixin private OutputHelperMixins.RestCall outputHelper; - @Getter @Mixin private SCSastControllerProductHelperBasicMixin productHelper; - - @Override - protected INextPageUrlProducer _getNextPageUrlProducer() { - return null; - } - - @Override - protected JsonNode _transformInput(JsonNode input) { - return SCSastInputTransformer.getItems(input); - } - - @Override - protected JsonNode _transformRecord(JsonNode input) { - return input; - } + @Getter @Mixin private SCSastControllerUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final SCSastControllerProductHelper productHelper = SCSastControllerProductHelper.INSTANCE; } \ No newline at end of file diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanStartCommand.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanStartCommand.java index 8fb896a763..76ab68b697 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanStartCommand.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanStartCommand.java @@ -110,7 +110,7 @@ private String getUploadToken() { // passed through --ssc-ci-token, and arbitrary token passed through --ssc-token on the // login command; we should only reuse a token passed through the --ssc-ci-token login // option. - char[] ciTokenFromSession = getProductHelper().getSessionDescriptor().getPredefinedSscToken(); + char[] ciTokenFromSession = getUnirestInstanceSupplier().getSessionDescriptor().getPredefinedSscToken(); uploadToken = ciTokenFromSession==null ? null : SSCTokenConverter.toApplicationToken(String.valueOf(ciTokenFromSession)); } if ( StringUtils.isBlank(uploadToken) ) { throw new IllegalArgumentException("--ssc-ci-token is required if --publish-to is specified and --ssc-ci-token was not passed to the 'sc-sast session login' command"); } diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanWaitForCommand.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanWaitForCommand.java index f9d1cb3ca1..c9999d4fa4 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanWaitForCommand.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanWaitForCommand.java @@ -18,7 +18,7 @@ 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.sc_sast._common.output.cli.mixin.SCSastControllerProductHelperStandardMixin; +import com.fortify.cli.sc_sast._common.session.cli.mixin.SCSastControllerUnirestInstanceSupplierMixin; import com.fortify.cli.sc_sast.scan.cli.mixin.SCSastScanJobResolverMixin; import com.fortify.cli.sc_sast.scan.helper.SCSastControllerScanJobArtifactState; import com.fortify.cli.sc_sast.scan.helper.SCSastControllerScanJobArtifactState.SCSastControllerScanJobArtifactStateIterable; @@ -36,7 +36,7 @@ @Command(name = OutputHelperMixins.WaitFor.CMD_NAME) public class SCSastControllerScanWaitForCommand extends AbstractWaitForCommand { - @Getter @Mixin SCSastControllerProductHelperStandardMixin productHelper; + @Getter @Mixin private SCSastControllerUnirestInstanceSupplierMixin unirestInstanceSupplier; @Mixin private SCSastScanJobResolverMixin.PositionalParameterMulti scanJobsResolver; @ArgGroup(exclusive = true, multiplicity = "0..1") private WaitOptions waitOptions; private static final class WaitOptions { 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..fa779bbf00 100644 --- a/fcli-core/fcli-ssc/build.gradle +++ b/fcli-core/fcli-ssc/build.gradle @@ -1 +1,12 @@ +task zipResources_actions(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") { + // TODO We should also sign file; how do we invoke a sign operation from Gradle? + filter(line->project.version.startsWith('0.') + ? line + : line.replaceAll("https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json", "https://fortify.github.io/fcli/schemas/action/fcli-action-schema-${fcliActionSchemaVersion}.json")) + } + 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/output/cli/cmd/AbstractSSCCheckCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/cmd/AbstractSSCCheckCommand.java new file mode 100644 index 0000000000..73bbf6aec1 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/cmd/AbstractSSCCheckCommand.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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._common.output.cli.cmd; + +import com.fortify.cli.common.output.cli.cmd.AbstractCheckCommand; +import com.fortify.cli.common.output.product.IProductHelperSupplier; +import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; +import com.fortify.cli.ssc._common.rest.helper.SSCProductHelper; +import com.fortify.cli.ssc._common.session.cli.mixin.SSCUnirestInstanceSupplierMixin; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Mixin; + +public abstract class AbstractSSCCheckCommand extends AbstractCheckCommand + implements IProductHelperSupplier, IUnirestInstanceSupplier +{ + @Mixin private SSCUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final SSCProductHelper productHelper = SSCProductHelper.INSTANCE; + + public UnirestInstance getUnirestInstance() { + return unirestInstanceSupplier.getUnirestInstance(); + } + + @Override + protected final boolean isPass() { + return isPass(getUnirestInstance()); + } + + protected abstract boolean isPass(UnirestInstance unirest); +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/cmd/AbstractSSCOutputCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/cmd/AbstractSSCOutputCommand.java index 8249a72136..5de5b85388 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/cmd/AbstractSSCOutputCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/cmd/AbstractSSCOutputCommand.java @@ -15,7 +15,8 @@ import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; import com.fortify.cli.common.output.product.IProductHelperSupplier; import com.fortify.cli.common.rest.unirest.IUnirestInstanceSupplier; -import com.fortify.cli.ssc._common.output.cli.mixin.SSCProductHelperStandardMixin; +import com.fortify.cli.ssc._common.rest.helper.SSCProductHelper; +import com.fortify.cli.ssc._common.session.cli.mixin.SSCUnirestInstanceSupplierMixin; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -24,9 +25,10 @@ public abstract class AbstractSSCOutputCommand extends AbstractOutputCommand implements IProductHelperSupplier, IUnirestInstanceSupplier { - @Getter @Mixin SSCProductHelperStandardMixin productHelper; + @Mixin private SSCUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final SSCProductHelper productHelper = SSCProductHelper.INSTANCE; public UnirestInstance getUnirestInstance() { - return productHelper.getUnirestInstance(); + return unirestInstanceSupplier.getUnirestInstance(); } } 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/_common/output/cli/mixin/SSCProductHelperStandardMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/helper/SSCProductHelper.java similarity index 80% rename from fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/mixin/SSCProductHelperStandardMixin.java rename to fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/helper/SSCProductHelper.java index 63331caab2..f3e0ba3b1a 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/mixin/SSCProductHelperStandardMixin.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/helper/SSCProductHelper.java @@ -10,20 +10,20 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.ssc._common.output.cli.mixin; +package com.fortify.cli.ssc._common.rest.helper; 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 com.fortify.cli.ssc._common.rest.helper.SSCInputTransformer; -import com.fortify.cli.ssc._common.rest.helper.SSCPagingHelper; //IMPORTANT: When updating/adding any methods in this class, SSCRestCallCommand //also likely needs to be updated -public class SSCProductHelperStandardMixin extends SSCProductHelperBasicMixin - implements IInputTransformer, INextPageUrlProducerSupplier +public class SSCProductHelper implements IProductHelper, IInputTransformer, INextPageUrlProducerSupplier { + public static final SSCProductHelper INSTANCE = new SSCProductHelper(); + private SSCProductHelper() {} @Override public INextPageUrlProducer getNextPageUrlProducer() { return SSCPagingHelper.nextPageUrlProducer(); diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/bulk/package-info.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/package-info.java similarity index 94% rename from fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/bulk/package-info.java rename to fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/package-info.java index e043a03af7..f298092772 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/bulk/package-info.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/package-info.java @@ -13,4 +13,4 @@ /** * This package contains generic helper classes for working with the SSC REST API. */ -package com.fortify.cli.ssc._common.rest.bulk; \ No newline at end of file +package com.fortify.cli.ssc._common.rest; \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/transfer/SSCFileTransferHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/transfer/SSCFileTransferHelper.java index 86d27797fb..4c04900c11 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/transfer/SSCFileTransferHelper.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/rest/transfer/SSCFileTransferHelper.java @@ -119,7 +119,7 @@ public static enum SSCFileTransferTokenType { REPORT_FILE } - private static final class SSCFileTransferTokenSupplier implements AutoCloseable, Supplier { + public static final class SSCFileTransferTokenSupplier implements AutoCloseable, Supplier { private final UnirestInstance unirest; private final String token; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/session/cli/mixin/SSCSessionLoginOptions.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/session/cli/mixin/SSCSessionLoginOptions.java index 5dc610b5e4..4404fbd110 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/session/cli/mixin/SSCSessionLoginOptions.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/session/cli/mixin/SSCSessionLoginOptions.java @@ -24,6 +24,7 @@ import lombok.Getter; import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; import picocli.CommandLine.Help.Visibility; import picocli.CommandLine.Option; @@ -79,4 +80,12 @@ public static class SSCCITokenCredentialOptions { @Option(names = {"--ci-token"}, interactive = true, echo = false, arity = "0..1", required = true) @Getter private char[] token; } + + @Command + public static final class SSCUrlConfigOptions extends UrlConfigOptions { + @Override + protected int getDefaultSocketTimeoutInMillis() { + return 600000; + } + } } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/mixin/SSCProductHelperBasicMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/session/cli/mixin/SSCUnirestInstanceSupplierMixin.java similarity index 84% rename from fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/mixin/SSCProductHelperBasicMixin.java rename to fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/session/cli/mixin/SSCUnirestInstanceSupplierMixin.java index 912b2245bf..2196bf31f2 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/output/cli/mixin/SSCProductHelperBasicMixin.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/_common/session/cli/mixin/SSCUnirestInstanceSupplierMixin.java @@ -10,9 +10,8 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.ssc._common.output.cli.mixin; +package com.fortify.cli.ssc._common.session.cli.mixin; -import com.fortify.cli.common.output.product.IProductHelper; import com.fortify.cli.common.session.cli.mixin.AbstractSessionUnirestInstanceSupplierMixin; import com.fortify.cli.ssc._common.session.helper.SSCSessionDescriptor; import com.fortify.cli.ssc._common.session.helper.SSCSessionHelper; @@ -20,9 +19,7 @@ import kong.unirest.UnirestInstance; -public class SSCProductHelperBasicMixin extends AbstractSessionUnirestInstanceSupplierMixin - implements IProductHelper -{ +public final class SSCUnirestInstanceSupplierMixin extends AbstractSessionUnirestInstanceSupplierMixin { @Override public final SSCSessionDescriptor getSessionDescriptor(String sessionName) { return SSCSessionHelper.instance().get(sessionName, true); 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..aacfdb5c95 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionCommands.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * 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, + SSCActionSignCommand.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..0554be6e8e --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionRunCommand.java @@ -0,0 +1,281 @@ +/******************************************************************************* + * 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.io.InputStream; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.action.cli.cmd.AbstractActionRunCommand; +import com.fortify.cli.common.action.model.ActionValidationException; +import com.fortify.cli.common.action.model.ActionStepForEach.IActionStepForEachProcessor; +import com.fortify.cli.common.action.runner.ActionRunner; +import com.fortify.cli.common.action.runner.ActionSpelFunctions; +import com.fortify.cli.common.action.runner.ActionRunner.ParameterTypeConverterArgs; +import com.fortify.cli.common.action.runner.ActionRunner.IActionRequestHelper.BasicActionRequestHelper; +import com.fortify.cli.common.json.JsonHelper; +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.SSCUrls; +import com.fortify.cli.ssc._common.rest.bulk.SSCBulkRequestBuilder; +import com.fortify.cli.ssc._common.rest.helper.SSCProductHelper; +import com.fortify.cli.ssc._common.rest.transfer.SSCFileTransferHelper.SSCFileTransferTokenSupplier; +import com.fortify.cli.ssc._common.rest.transfer.SSCFileTransferHelper.SSCFileTransferTokenType; +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 kong.unirest.RawResponse; +import kong.unirest.UnirestInstance; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +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 SSCActionRequestHelper(unirestInstanceSupplier::getUnirestInstance, SSCProductHelper.INSTANCE)); + context.setVariable("ssc", new SSCSpelFunctions(templateRunner)); + } + + @RequiredArgsConstructor @Reflectable + public final class SSCSpelFunctions { + private final ActionRunner templateRunner; + public IActionStepForEachProcessor ruleDescriptionsProcessor(String appVersionId) { + var unirest = templateRunner.getRequestHelper("ssc").getUnirestInstance(); + return new SSCFPRRuleDescriptionProcessor(unirest, appVersionId)::process; + } + 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 SSCActionRequestHelper extends BasicActionRequestHelper { + public SSCActionRequestHelper(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); + } + } + + @RequiredArgsConstructor + private final class SSCFPRRuleDescriptionProcessor { + private final UnirestInstance unirest; + private final String appVersionId; + + @SneakyThrows + public final void process(Function consumer) { + try ( SSCFileTransferTokenSupplier tokenSupplier = new SSCFileTransferTokenSupplier(unirest, SSCFileTransferTokenType.DOWNLOAD); ) { + unirest.get(SSCUrls.DOWNLOAD_CURRENT_FPR(appVersionId, false)) + .routeParam("downloadToken", tokenSupplier.get()).asObject(r->processFpr(r, consumer)).getBody(); + } + } + + @SneakyThrows + private final String processFpr(RawResponse r, Function consumer) { + try ( var zis = new ZipInputStream(r.getContent()) ) { + ZipEntry entry; + while ( (entry = zis.getNextEntry())!=null ) { + if ( "audit.fvdl".equals(entry.getName()) ) { + processAuditFvdl(zis, consumer); break; + } + } + } + return null; + } + + private final void processAuditFvdl(InputStream is, Function consumer) throws XMLStreamException { + var factory = XMLInputFactory.newInstance(); + var reader = factory.createXMLStreamReader(is); + while(reader.hasNext()) { + int eventType = reader.next(); + if ( eventType==XMLStreamReader.START_ELEMENT && "Description".equals(reader.getLocalName()) ) { + if (!processDescription(reader, consumer)) { break; } + } + } + } + + private boolean processDescription(XMLStreamReader reader, Function consumer) throws XMLStreamException { + var ruleId = reader.getAttributeValue(null, "classID"); + var entry = JsonHelper.getObjectMapper().createObjectNode() + .put("id", ruleId); + var tips = JsonHelper.getObjectMapper().createArrayNode(); + var references = JsonHelper.getObjectMapper().createArrayNode(); + entry.set("tips", tips); + entry.set("references", references); + processElement(reader, name->{ + switch ( name ) { + case "Abstract": entry.put("abstract", ActionSpelFunctions.cleanRuleDescription(readString(reader))); break; + case "Explanation": entry.put("explanation", ActionSpelFunctions.cleanRuleDescription(readString(reader))); break; + case "Recommendations": entry.put("recommendations", ActionSpelFunctions.cleanRuleDescription(readString(reader))); break; + case "Tips": addTips(reader, tips); break; + case "References": addReferences(reader, references); break; + } + }); + return consumer.apply(entry); + } + + @SneakyThrows + private void addTips(XMLStreamReader reader, ArrayNode tips) { + processElement(reader, name->{ + switch ( name ) { + case "Tip": tips.add(ActionSpelFunctions.cleanRuleDescription(readString(reader))); + } + }); + } + + @SneakyThrows + private void addReferences(XMLStreamReader reader, ArrayNode references) { + processElement(reader, name->{ + switch ( name ) { + case "Reference": references.add(readReference(reader)); + } + }); + } + + @SneakyThrows + private ObjectNode readReference(XMLStreamReader reader) { + var reference = JsonHelper.getObjectMapper().createObjectNode(); + processElement(reader, name->{ + switch ( name ) { + case "Title": reference.put("title", readString(reader)); break; + case "Publisher": reference.put("publisher", readString(reader)); break; + case "Author": reference.put("author", readString(reader)); break; + case "Source": reference.put("source", readString(reader)); break; + } + }); + return reference; + } + + private void processElement(XMLStreamReader reader, Consumer consumer) throws XMLStreamException { + int level = 1; + while(level > 0 && reader.hasNext()) { + int eventType = reader.next(); + switch ( eventType ) { + case XMLStreamReader.START_ELEMENT: + level++; + consumer.accept(reader.getLocalName()); + case XMLStreamReader.END_ELEMENT: + level--; break; + } + } + } + + @SneakyThrows + private String readString(XMLStreamReader reader) { + StringBuilder result = new StringBuilder(); + while (reader.hasNext()) { + int eventType = reader.next(); + switch (eventType) { + case XMLStreamReader.CHARACTERS: + case XMLStreamReader.CDATA: + result.append(reader.getText()); + break; + case XMLStreamReader.END_ELEMENT: + return result.toString(); + } + } + throw new XMLStreamException("Premature end of file"); + } + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionSignCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionSignCommand.java new file mode 100644 index 0000000000..628de49efc --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/cli/cmd/SSCActionSignCommand.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * 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.AbstractActionSignCommand; + +import picocli.CommandLine.Command; + +@Command(name = "sign") +public class SSCActionSignCommand extends AbstractActionSignCommand {} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/cmd/SSCAppVersionCopyStateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/cmd/SSCAppVersionCopyStateCommand.java index 8c63262cb3..fa8e63fc75 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/cmd/SSCAppVersionCopyStateCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/cmd/SSCAppVersionCopyStateCommand.java @@ -26,9 +26,9 @@ import com.fortify.cli.ssc.appversion.cli.mixin.SSCDelimiterMixin; import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor; import com.fortify.cli.ssc.appversion.helper.SSCAppVersionHelper; - import com.fortify.cli.ssc.system_state.helper.SSCJobDescriptor; import com.fortify.cli.ssc.system_state.helper.SSCJobHelper; + import kong.unirest.UnirestInstance; import lombok.Getter; import picocli.CommandLine.Command; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/cmd/SSCAppVersionCreateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/cmd/SSCAppVersionCreateCommand.java index cf704bfb0f..1f32fc54e2 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/cmd/SSCAppVersionCreateCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/cmd/SSCAppVersionCreateCommand.java @@ -40,9 +40,9 @@ import com.fortify.cli.ssc.attribute.helper.SSCAttributeUpdateBuilder; import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueTemplateResolverMixin; import com.fortify.cli.ssc.issue.helper.SSCIssueTemplateDescriptor; - import com.fortify.cli.ssc.system_state.helper.SSCJobDescriptor; import com.fortify.cli.ssc.system_state.helper.SSCJobHelper; + import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import lombok.Getter; @@ -78,9 +78,9 @@ public JsonNode getJsonNode(UnirestInstance unirest) { SSCAttributeUpdateBuilder attrUpdateBuilder = getAttrUpdateBuilder(unirest); SSCAppVersionUserUpdateBuilder authUpdateBuilder = getAuthUpdateBuilder(unirest); - SSCAppVersionDescriptor descriptor = createUncommittedAppVersion(unirest); - SSCAppVersionCreateCopyFromBuilder copyFromBuilder = getCopyFromBuilder(unirest); + + SSCAppVersionDescriptor descriptor = createUncommittedAppVersion(unirest); SSCBulkResponse bulkResponse = new SSCBulkRequestBuilder() .request("attrUpdate", attrUpdateBuilder.buildRequest(descriptor.getVersionId())) diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCAppVersionCopyFromMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCAppVersionCopyFromMixin.java index 9c5a5e3a51..90f9036241 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCAppVersionCopyFromMixin.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCAppVersionCopyFromMixin.java @@ -14,9 +14,9 @@ import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.DisableTest.TestType; +import com.fortify.cli.ssc.appversion.helper.SSCAppVersionCopyType; import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor; import com.fortify.cli.ssc.appversion.helper.SSCAppVersionHelper; -import com.fortify.cli.ssc.appversion.helper.SSCAppVersionCopyType; import kong.unirest.UnirestInstance; import lombok.Getter; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCAppVersionRefreshOptions.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCAppVersionRefreshOptions.java index 88fa380b84..b5e371e3bd 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCAppVersionRefreshOptions.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCAppVersionRefreshOptions.java @@ -12,9 +12,7 @@ *******************************************************************************/ package com.fortify.cli.ssc.appversion.cli.mixin; -import com.fortify.cli.common.cli.mixin.CommonOptionMixins; import lombok.Getter; -import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; @Getter diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactWaitForCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactWaitForCommand.java index cf692e9695..376754dfaf 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactWaitForCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactWaitForCommand.java @@ -17,7 +17,7 @@ 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.ssc._common.output.cli.mixin.SSCProductHelperStandardMixin; +import com.fortify.cli.ssc._common.session.cli.mixin.SSCUnirestInstanceSupplierMixin; import com.fortify.cli.ssc.artifact.cli.mixin.SSCArtifactResolverMixin; import com.fortify.cli.ssc.artifact.helper.SSCArtifactHelper; import com.fortify.cli.ssc.artifact.helper.SSCArtifactStatus; @@ -31,7 +31,7 @@ @Command(name = OutputHelperMixins.WaitFor.CMD_NAME) public class SSCArtifactWaitForCommand extends AbstractWaitForCommand { - @Getter @Mixin SSCProductHelperStandardMixin productHelper; + @Getter @Mixin private SSCUnirestInstanceSupplierMixin unirestInstanceSupplier; @Mixin private SSCArtifactResolverMixin.PositionalParameterMulti artifactsResolver; @Option(names={"-s", "--any-state"}, required=true, split=",", defaultValue="PROCESS_COMPLETE", completionCandidates = SSCArtifactStatusIterable.class) private Set states; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/attribute/cli/cmd/SSCAttributeDefinitionListCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/attribute/cli/cmd/SSCAttributeDefinitionListCommand.java index d1c556e3a1..5c5c60cf35 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/attribute/cli/cmd/SSCAttributeDefinitionListCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/attribute/cli/cmd/SSCAttributeDefinitionListCommand.java @@ -41,7 +41,7 @@ public class SSCAttributeDefinitionListCommand extends AbstractSSCBaseRequestOut @Override public HttpRequest getBaseRequest(UnirestInstance unirest) { - return unirest.get("/api/v1/attributeDefinitions?limit=-1&orderby=category,name"); + return unirest.get("/api/v1/attributeDefinitions?limit=100&orderby=category,name"); } @Override diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCheckCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCheckCommand.java new file mode 100644 index 0000000000..5514ff690b --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCheckCommand.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * 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.issue.cli.cmd; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCCheckCommand; +import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin; +import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueFilterSetResolverMixin; + +import kong.unirest.GetRequest; +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +// TODO Work in progress, maybe we'll instead use an action-based or other yaml-based approach +@Command(name = OutputHelperMixins.Check.CMD_NAME, hidden=true) +public class SSCIssueCheckCommand extends AbstractSSCCheckCommand { + @Getter @Mixin private OutputHelperMixins.Check outputHelper; + @Mixin private SSCAppVersionResolverMixin.RequiredOption parentResolver; + @Mixin private SSCIssueFilterSetResolverMixin.FilterSetOption filterSetResolver; + + @Override + protected boolean isPass(UnirestInstance unirest) { + String appVersionId = parentResolver.getAppVersionId(unirest); + String filterSetId = filterSetResolver.getFilterSetId(unirest, appVersionId); + GetRequest request = getBaseRequest(unirest, appVersionId, filterSetId) + .queryString("limit", "1"); + var count = request.asObject(ObjectNode.class).getBody() + .get("count").asInt(); + return count==0; + } + + private GetRequest getBaseRequest(UnirestInstance unirest, String appVersionId, String filterSetId) { + return unirest.get("/api/v1/projectVersions/{id}/issues?&qm=issues") + .routeParam("id", appVersionId) + .queryString("filterset", filterSetId); + + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCommands.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCommands.java index ef5d2dc086..d5ce0498a9 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCommands.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueCommands.java @@ -31,7 +31,9 @@ SSCIssueFiltersListCommand.class, SSCIssueGroupGetCommand.class, SSCIssueGroupListCommand.class, + SSCIssueCheckCommand.class, SSCIssueCountCommand.class, + SSCIssueListCommand.class, } ) public class SSCIssueCommands extends AbstractContainerCommand { diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueListCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueListCommand.java new file mode 100644 index 0000000000..1aa690e062 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/cmd/SSCIssueListCommand.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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.issue.cli.cmd; + +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.rest.query.IServerSideQueryParamGeneratorSupplier; +import com.fortify.cli.common.rest.query.IServerSideQueryParamValueGenerator; +import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCBaseRequestOutputCommand; +import com.fortify.cli.ssc._common.rest.query.SSCQParamGenerator; +import com.fortify.cli.ssc._common.rest.query.SSCQParamValueGenerators; +import com.fortify.cli.ssc._common.rest.query.cli.mixin.SSCQParamMixin; +import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionResolverMixin; +import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueBulkEmbedMixin; +import com.fortify.cli.ssc.issue.cli.mixin.SSCIssueFilterSetResolverMixin; +import com.fortify.cli.ssc.issue.helper.SSCIssueFilterHelper; +import com.fortify.cli.ssc.issue.helper.SSCIssueFilterSetDescriptor; + +import kong.unirest.GetRequest; +import kong.unirest.HttpRequest; +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command(name = OutputHelperMixins.List.CMD_NAME) +public class SSCIssueListCommand extends AbstractSSCBaseRequestOutputCommand implements IServerSideQueryParamGeneratorSupplier { + @Getter @Mixin private OutputHelperMixins.List outputHelper; + @Mixin private SSCAppVersionResolverMixin.RequiredOption parentResolver; + @Mixin private SSCIssueFilterSetResolverMixin.FilterSetOption filterSetResolver; + @Mixin private SSCQParamMixin qParamMixin; + @Mixin private SSCIssueBulkEmbedMixin bulkEmbedMixin; + @Option(names="--filter", required=false) private String filter; + + // For some reason, SSC q param doesn't use same property names as returned by SSC, + // so we list the proper mappings below. TODO Any other useful server-side queries? + @Getter private IServerSideQueryParamValueGenerator serverSideQueryParamGenerator = new SSCQParamGenerator() + .add("issueName", "category", SSCQParamValueGenerators::wrapInQuotes) + .add("fullFileName", "file", SSCQParamValueGenerators::wrapInQuotes); + + @Override + public HttpRequest getBaseRequest(UnirestInstance unirest) { + String appVersionId = parentResolver.getAppVersionId(unirest); + SSCIssueFilterSetDescriptor filterSetDescriptor = filterSetResolver.getFilterSetDescriptor(unirest, appVersionId); + GetRequest request = unirest.get("/api/v1/projectVersions/{id}/issues?limit=100&qm=issues") + .routeParam("id", appVersionId); + if ( filterSetDescriptor!=null ) { + request.queryString("filterset", filterSetDescriptor.getGuid()); + } + if ( filter!=null ) { + request.queryString("filter", new SSCIssueFilterHelper(unirest, appVersionId).getFilter(filter)); + } + return request; + } + + @Override + public boolean isSingular() { + return false; + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueBulkEmbedMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueBulkEmbedMixin.java new file mode 100644 index 0000000000..ded6677bf8 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueBulkEmbedMixin.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * 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.issue.cli.mixin; + +import com.fortify.cli.common.util.DisableTest; +import com.fortify.cli.common.util.DisableTest.TestType; +import com.fortify.cli.ssc._common.output.cli.mixin.AbstractSSCBulkEmbedMixin; +import com.fortify.cli.ssc.issue.helper.SSCIssueEmbedderSupplier; + +import lombok.Getter; +import picocli.CommandLine.Option; + +public class SSCIssueBulkEmbedMixin extends AbstractSSCBulkEmbedMixin { + @DisableTest(TestType.MULTI_OPT_PLURAL_NAME) + @Option(names = "--embed", required = false, split = ",", descriptionKey = "fcli.ssc.issue.embed" ) + @Getter private SSCIssueEmbedderSupplier[] embedSuppliers; +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueFilterSetResolverMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueFilterSetResolverMixin.java index 675c701995..d75e121a99 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueFilterSetResolverMixin.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueFilterSetResolverMixin.java @@ -28,10 +28,14 @@ private static abstract class AbstractSSCFilterSetResolverMixin { public SSCIssueFilterSetDescriptor getFilterSetDescriptor(UnirestInstance unirest, String appVersionId) { return new SSCIssueFilterSetHelper(unirest, appVersionId).getDescriptorByTitleOrId(getFilterSetTitleOrId(), true); } + + public String getFilterSetId(UnirestInstance unirest, String appVersionId) { + return getFilterSetDescriptor(unirest, appVersionId).getGuid(); + } } public static class FilterSetOption extends AbstractSSCFilterSetResolverMixin { - @Option(names="--filterset", descriptionKey = "fcli.ssc.issue.filterset.resolver.titleOrId") + @Option(names={"--filterset", "--fs"}, descriptionKey = "fcli.ssc.issue.filterset.resolver.titleOrId") @Getter private String filterSetTitleOrId; } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueEmbedderSupplier.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueEmbedderSupplier.java new file mode 100644 index 0000000000..4d10104ee2 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueEmbedderSupplier.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.ssc.issue.helper; + +import java.util.function.Supplier; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.ssc._common.rest.bulk.ISSCEntityEmbedder; +import com.fortify.cli.ssc._common.rest.bulk.ISSCEntityEmbedderSupplier; +import com.fortify.cli.ssc._common.rest.bulk.SSCBulkRequestBuilder; +import com.fortify.cli.ssc._common.rest.helper.SSCInputTransformer; + +import kong.unirest.HttpRequest; +import kong.unirest.UnirestInstance; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum SSCIssueEmbedderSupplier implements ISSCEntityEmbedderSupplier { + details(SSCIssueDetailsEmbedder::new), + comments(SSCIssueCommentsEmbedder::new), + auditHistory(SSCIssueAuditHistoryEmbedder::new), + ; + + private final Supplier supplier; + + public ISSCEntityEmbedder createEntityEmbedder() { + return supplier.get(); + } + + private static abstract class AbstractSSCIssueEmbedder implements ISSCEntityEmbedder { + @Override + public void addEmbedRequests(SSCBulkRequestBuilder builder, UnirestInstance unirest, JsonNode record) { + var id = record.get("id").asText(); + builder.request(getBaseRequest(unirest) + .routeParam("id", id) + .queryString("limit", "-1"), + response->process((ObjectNode)record, SSCInputTransformer.getDataOrSelf(response))); + } + + protected abstract HttpRequest getBaseRequest(UnirestInstance unirest); + protected abstract void process(ObjectNode record, JsonNode response); + } + + private static final class SSCIssueDetailsEmbedder extends AbstractSSCIssueEmbedder { + @Override + protected HttpRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v1/issueDetails/{id}"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("details", response); + } + } + + private static final class SSCIssueAuditHistoryEmbedder extends AbstractSSCIssueEmbedder { + @Override + protected HttpRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v1/issues/{id}/auditHistory?limit=-1"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("auditHistory", response); + } + } + + private static final class SSCIssueCommentsEmbedder extends AbstractSSCIssueEmbedder { + @Override + protected HttpRequest getBaseRequest(UnirestInstance unirest) { + return unirest.get("/api/v1/issues/{id}/comments?limit=-1"); + } + @Override + protected void process(ObjectNode record, JsonNode response) { + record.set("comments", response); + } + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/plugin/cli/cmd/SSCPluginListCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/plugin/cli/cmd/SSCPluginListCommand.java index 62cf275372..71f786266a 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/plugin/cli/cmd/SSCPluginListCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/plugin/cli/cmd/SSCPluginListCommand.java @@ -28,7 +28,7 @@ public class SSCPluginListCommand extends AbstractSSCBaseRequestOutputCommand { @Override public HttpRequest getBaseRequest(UnirestInstance unirest) { - return unirest.get("/api/v1/plugins?orderBy=pluginType,pluginName,pluginVersion&limit=-1"); + return unirest.get("/api/v1/plugins?orderBy=pluginType,pluginName,pluginVersion&limit=100"); } @Override diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/cli/cmd/SSCReportWaitForCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/cli/cmd/SSCReportWaitForCommand.java index 195f378926..3601cf0ace 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/cli/cmd/SSCReportWaitForCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/cli/cmd/SSCReportWaitForCommand.java @@ -17,7 +17,7 @@ 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.ssc._common.output.cli.mixin.SSCProductHelperStandardMixin; +import com.fortify.cli.ssc._common.session.cli.mixin.SSCUnirestInstanceSupplierMixin; import com.fortify.cli.ssc.report.cli.mixin.SSCReportResolverMixin; import com.fortify.cli.ssc.report.helper.SSCReportStatus; import com.fortify.cli.ssc.report.helper.SSCReportStatus.SSCReportStatusIterable; @@ -30,7 +30,7 @@ @Command(name = OutputHelperMixins.WaitFor.CMD_NAME) public class SSCReportWaitForCommand extends AbstractWaitForCommand { - @Getter @Mixin SSCProductHelperStandardMixin productHelper; + @Getter @Mixin private SSCUnirestInstanceSupplierMixin unirestInstanceSupplier; @Mixin private SSCReportResolverMixin.PositionalParameterMulti reportsResolver; @Option(names={"-s", "--any-state"}, required=true, split=",", defaultValue="PROCESS_COMPLETE", completionCandidates = SSCReportStatusIterable.class) private Set states; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/helper/SSCReportFormat.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/helper/SSCReportFormat.java index b88d7d7cd9..0bb34df7e5 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/helper/SSCReportFormat.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/helper/SSCReportFormat.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/helper/SSCReportParameterType.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/helper/SSCReportParameterType.java index b3aedc7bef..7a7880fe7d 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/helper/SSCReportParameterType.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/report/helper/SSCReportParameterType.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/rest/cli/cmd/SSCRestCallCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/rest/cli/cmd/SSCRestCallCommand.java index 4692271e55..6523d7035b 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/rest/cli/cmd/SSCRestCallCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/rest/cli/cmd/SSCRestCallCommand.java @@ -12,15 +12,12 @@ *******************************************************************************/ package com.fortify.cli.ssc.rest.cli.cmd; -import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.rest.cli.cmd.AbstractRestCallCommand; -import com.fortify.cli.common.rest.paging.INextPageUrlProducer; import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.DisableTest.TestType; -import com.fortify.cli.ssc._common.output.cli.mixin.SSCProductHelperBasicMixin; -import com.fortify.cli.ssc._common.rest.helper.SSCInputTransformer; -import com.fortify.cli.ssc._common.rest.helper.SSCPagingHelper; +import com.fortify.cli.ssc._common.rest.helper.SSCProductHelper; +import com.fortify.cli.ssc._common.session.cli.mixin.SSCUnirestInstanceSupplierMixin; import lombok.Getter; import picocli.CommandLine.Command; @@ -30,20 +27,6 @@ @DisableTest(TestType.CMD_DEFAULT_TABLE_OPTIONS_PRESENT) // Output columns depend on response contents public final class SSCRestCallCommand extends AbstractRestCallCommand { @Getter @Mixin private OutputHelperMixins.RestCall outputHelper; - @Getter @Mixin private SSCProductHelperBasicMixin productHelper; - - @Override - protected INextPageUrlProducer _getNextPageUrlProducer() { - return SSCPagingHelper.nextPageUrlProducer(); - } - - @Override - protected JsonNode _transformInput(JsonNode input) { - return SSCInputTransformer.getDataOrSelf(input); - } - - @Override - protected JsonNode _transformRecord(JsonNode input) { - return input; - } + @Getter @Mixin private SSCUnirestInstanceSupplierMixin unirestInstanceSupplier; + @Getter private final SSCProductHelper productHelper = SSCProductHelper.INSTANCE; } \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/system_state/cli/cmd/SSCStateJobWaitForCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/system_state/cli/cmd/SSCStateJobWaitForCommand.java index 8dadf3066a..5db16fadee 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/system_state/cli/cmd/SSCStateJobWaitForCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/system_state/cli/cmd/SSCStateJobWaitForCommand.java @@ -17,7 +17,7 @@ import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.rest.cli.cmd.AbstractWaitForCommand; import com.fortify.cli.common.rest.wait.WaitHelper.WaitHelperBuilder; -import com.fortify.cli.ssc._common.output.cli.mixin.SSCProductHelperStandardMixin; +import com.fortify.cli.ssc._common.session.cli.mixin.SSCUnirestInstanceSupplierMixin; import com.fortify.cli.ssc.system_state.cli.mixin.SSCJobResolverMixin; import com.fortify.cli.ssc.system_state.helper.SSCJobStatus; import com.fortify.cli.ssc.system_state.helper.SSCJobStatus.SSCJobStatusIterable; @@ -30,7 +30,7 @@ @Command(name = "wait-for-job") @CommandGroup("job") public class SSCStateJobWaitForCommand extends AbstractWaitForCommand { - @Getter @Mixin SSCProductHelperStandardMixin productHelper; + @Getter @Mixin private SSCUnirestInstanceSupplierMixin unirestInstanceSupplier; @Mixin private SSCJobResolverMixin.PositionalParameterMulti jobsResolver; @Option(names={"-s", "--any-state"}, required=true, split=",", defaultValue="FINISHED", completionCandidates = SSCJobStatusIterable.class) private Set states; diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/appversion-summary.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/appversion-summary.yaml new file mode 100644 index 0000000000..93d5914025 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/appversion-summary.yaml @@ -0,0 +1,244 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +usage: + header: (PREVIEW) Generate application version summary. + description: | + This action generates a short summary listing issue counts and other statistics + for a given application version. Based on user feedback on this initial version + of this action, parameters and output of this action may change in the next + couple of fcli releases. + +defaults: + requestTarget: ssc + +parameters: + - name: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). Default value: stdout" + required: false + defaultValue: stdout + - name: appversion + cliAliases: av + description: "Required application version id or :" + type: appversion_single + - name: filtersets + cliAliases: fs + description: "Comma-separated list of filter set names, guid's or 'default' to display in the summary. If not specified, all filter sets will be included." + required: false + +steps: + # Set output date format and convert filtersets parameter to array + - set: + - name: dateFmt + value: YYYY-MM-dd HH:mm + - name: filtersetsArray + value: ${parameters.filtersets?.split(',')} + - name: issueStateNames + value: ${{'NEW', 'REINTRODUCED', 'REMOVED', 'UPDATED'}} + # Load SSC issue selector filter by sets + - progress: Loading issue selector sets + - requests: + - name: issueSelectorSet + uri: /api/v1/projectVersions/${parameters.appversion.id}/issueSelectorSet?fields=filterBySet + - set: + - name: analysisTypes + value: ${issueSelectorSet.filterBySet.^[displayName=='Analysis Type'].selectorOptions} + - requests: + - if: ${analysisTypes!=null && analysisTypes.size()>0} + name: artifactsResponse + uri: /api/v1/projectVersions/${parameters.appversion.id}/artifacts + type: paged + query: + embed: scans + forEach: + name: artifact + breakIf: ${analysizedScans?.![type]?.containsAll(analysisTypes.![displayName])?:false} + do: + - forEach: + values: ${artifact._embed.scans} + name: scan + do: + - append: + - name: lastScans + if: ${lastScans==null || lastScans[scan.type]==null} + property: ${analysisTypes?.^[guid==#root.scan.type].displayName} + value: ${scan} + - name: analyzedScans + value: ${scan} + + + # Collect SSC filter set data, together with issue counts by analysis type & folder + - requests: + - name: filterSetsResponse + uri: /api/v1/projectVersions/${parameters.appversion.id}/filterSets + type: paged + forEach: + # Process each filter set if included by filtersets parameter value + name: filterset + if: ${filtersetsArray==null || filtersetsArray.contains(filterset.title) || filtersetsArray.contains(filterset.guid) || (filtersetsArray.contains('default') && filterset.defaultFilterSet)} + do: + - progress: Loading ${filterset.title} filter set data + # Collect filter sets + - append: + - name: filtersets + value: ${filterset} + # Collect issue counts for current filter set and each analysis type + - forEach: + name: analysisType + values: ${analysisTypes} + do: + # Load SSC issue counts by folder for current filter set and analysis type + - requests: + - name: issueGroupsByFolder + uri: /api/v1/projectVersions/${parameters.appversion.id}/issueGroups + query: + qm: issues + groupingtype: FOLDER + filter: ISSUE[11111111-1111-1111-1111-111111111151]:${analysisType.guid} + filterset: ${filterset.guid} + type: paged + forEach: + name: issueGroupFolder + do: + # Collect issue count by filter set, analysis type & folder + - append: + - name: issueCountsByFolder + property: ${filterset.title+':'+analysisType.displayName+':'+issueGroupFolder.id} + value: ${issueGroupFolder.visibleCount} + - name: issueGroupsByState + uri: /api/v1/projectVersions/${parameters.appversion.id}/issueGroups + query: + qm: issues + groupingtype: 11111111-1111-1111-1111-111111111167 + filter: ISSUE[11111111-1111-1111-1111-111111111151]:${analysisType.guid} + filterset: ${filterset.guid} + showremoved: true + type: paged + forEach: + name: issueGroupState + do: + # Collect issue count by filter set, analysis type & issue state + - append: + - name: issueCountsByState + property: ${filterset.title+':'+analysisType.displayName+':'+issueGroupState.id} + value: ${issueGroupState.visibleCount} + + - progress: Generating output data + + # For each filter set, generate the issue counts table + - forEach: + name: filterset + values: ${filtersets} + do: + # Clear variables for each filter set being processed + - unset: + - name: folderNames + - name: issueCountByFolderRows + - name: issueCountByStateRows + - set: + - name: folderTotals + value: ${{:}} + - name: stateTotals + value: ${{:}} + # Collect folder names from current filter set + - forEach: + name: folderName + values: ${filterset.folders.![name]} + do: + - append: + - name: folderNames + value: ${folderName} + # For current filter set, generate an issue count table row for each analysis type + - forEach: + name: analysisTypeName + if: ${analysisTypes!=null} + values: ${analysisTypes.![displayName]} + do: + # Clear counts for each analysis type being processed + - unset: + - name: issueCountByFolderRowValues + # For each folder, collect issue counts for current filter set & analysis type + - forEach: + name: folderName + values: ${filterset.folders.![name]} + do: + - set: + - name: folderIssueCount + value: ${issueCountsByFolder[filterset.title+':'+analysisTypeName+':'+folderName]?:0} + - append: + - name: issueCountByFolderRowValues + value: ${folderIssueCount} + - name: folderTotals + property: ${folderName} + value: ${folderIssueCount + (folderTotals[folderName]?:0)} + # Generate issue count row for current filter set and analysis type, listing + # issue counts as collected above + - append: + - name: issueCountByFolderRows + value: "| ${#fmt('%-22s', '**'+analysisTypeName+'**')} | ${#formatDateTime(dateFmt, lastScans[analysisTypeName].uploadDate)} | ${#join(' | ', issueCountByFolderRowValues.![#fmt('%10s', #this)])} |" + # For current filter set, generate an issue count table row for each issue state + - forEach: + name: analysisTypeName + if: ${analysisTypes!=null} + values: ${analysisTypes.![displayName]} + do: + # Clear counts for each analysis type being processed + - unset: + - name: issueCountByStateRowValues + # For each issue state, collect issue counts for current filter set & analysis type + - forEach: + name: stateName + values: ${issueStateNames} + do: + - set: + - name: stateIssueCount + value: ${issueCountsByState[filterset.title+':'+analysisTypeName+':'+stateName]?:0} + - append: + - name: issueCountByStateRowValues + value: ${stateIssueCount} + - name: stateTotals + property: ${stateName} + value: ${stateIssueCount + (stateTotals[stateName]?:0)} + # Generate issue count row for current filter set and analysis type, listing + # issue counts as collected above + - append: + - name: issueCountByStateRows + value: "| ${#fmt('%-22s', '**'+analysisTypeName+'**')} | ${#formatDateTime(dateFmt, lastScans[analysisTypeName].uploadDate)} | ${#join(' | ', issueCountByStateRowValues.![#fmt('%13s', #this)])} |" + # Combine the output of the steps above to generate full issue counts table for current filter set + - append: + - name: issueCountsOutput + value: | + #### ${filterset.title} ${filterset.defaultFilterSet?'(default)':''} + | Analysis Type | Last Scan Date | ${#join(' | ', folderNames.![#fmt('%10s', #this)])} | + | ---------------------- | ---------------- | ${#join(' | ', folderNames.!['----------'])} | + ${#join('\n', issueCountByFolderRows)} + | **Total** | | ${#join(' | ', folderNames.![#fmt('%10s', #root.folderTotals?.getRealNode()?.get(#this)?:0)])} | + + | Analysis Type | Last Scan Date | New | Re-introduced | Removed | Updated | + | ---------------------- | ---------------- | ------------- | ------------- | ------------- | ------------- | + ${#join('\n', issueCountByStateRows)} + | **Total** | | ${#join(' | ', issueStateNames.![#fmt('%13s', #root.stateTotals?.getRealNode()?.get(#this)?:0)])} | + + # Write output based on data collected above, and value template defined below + - write: + - to: ${parameters.file} + valueTemplate: summary-md + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +valueTemplates: + - name: summary-md + contents: | + # SSC Application Version Summary + + ## [${parameters.appversion.project.name} - ${parameters.appversion.name}](${#ssc.appversionBrowserUrl(parameters.appversion)}) + + Summary generated on: ${#formatDateTime(dateFmt)} + + ### Issue Counts + + ${#join('\n', issueCountsOutput)} + 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..20695d15dc --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bitbucket-sast-report.yaml @@ -0,0 +1,155 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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-file + cliAliases: r + description: "Optional report output file name (or 'stdout' / 'stderr'). Default value: bb-fortify-report.json" + required: false + defaultValue: bb-fortify-report.json + - name: annotations-file + cliAliases: a + description: "Optional annotations output file name (or 'stdout' / 'stderr'). 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 + - name: page-size + description: "Number of vulnerabilities to retrieve at a time. Higher numbers may reduce time required to build the report, at the cost of increased memory usage (on both fcli and SSC), and could potentially negatively affect overall SSC performance or result in read time-outs (see --socket-timeout option on fcli ssc session login command). Default value: 100" + required: false + defaultValue: "100" + +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 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + filterset: ${parameters.filterset.guid} + limit: ${parameters['page-size']} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - append: + - name: annotations + valueTemplate: annotations + - write: + - to: ${parameters['annotations-file']} + value: ${annotations?:{}} + - to: ${parameters['report-file']} + valueTemplate: report + - if: ${parameters['annotations-file']!='stdout' && parameters['report-file']!='stdout'} + to: stdout + value: | + Report written to ${parameters['report-file']} + Annotations written to ${parameters['annotations-file']} + +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/check-policy.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/check-policy.yaml new file mode 100644 index 0000000000..1ff8df421c --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/check-policy.yaml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +usage: + header: (SAMPLE) Check security policy. + description: | + This sample action demonstrates how to implement a security policy using + fcli actions, returning a non-zero exit code if any of the checks fail. + +defaults: + requestTarget: ssc + +parameters: + - name: appversion + cliAliases: av + description: "Required application version id or :" + type: appversion_single + +steps: + - fcli: + - name: countsByFolder + args: ssc issue count --av ${parameters.appversion.id} --by FOLDER + - fcli: + - name: countsByNewIssue + args: ssc issue count --av ${parameters.appversion.id} --by "New Issue" + - check: + - displayName: No critical issues allowed + failIf: ${countsByFolder.^[cleanName=='Critical']?.visibleCount>0} + - displayName: No new issues allowed + failIf: ${countsByNewIssue.^[cleanName=='NEW']?.visibleCount>0} + - fcli: + - args: ssc issue ls --av ${parameters.appversion.id} + forEach: + name: issue + do: + - check: + - displayName: No new critical issues allowed + failIf: ${issue.scanStatus=='NEW' && issue.friority=='Critical'} + ifSkipped: PASS # If no issues + 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..c4d9d9ed60 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml @@ -0,0 +1,164 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +# 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. + +author: Fortify +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: analysis-type + cliAliases: t + description: "Analysis type for which to list vulnerabilities. Default value: SCA" + required: true + defaultValue: SCA + - 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=200 + query: + showremoved: true + filter: ISSUE[11111111-1111-1111-1111-111111111151]:${parameters['analysis-type']} + 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: + - append: + - if: ${issue.scanStatus=='REMOVED'} + name: removedIssues + valueTemplate: mdIssueListItem + - if: ${(issue.scanStatus=='NEW' || issue.scanStatus=='REINTRODUCED')} + name: newIssues + valueTemplate: mdIssueListItem + - if: ${(issue.scanStatus=='NEW' || issue.scanStatus=='REINTRODUCED') && issue.engineType=='SCA'} + name: jsonSourceCodeComments + valueTemplate: jsonSourceCodeComment + + - progress: Generating GitHub request + - set: + - name: reviewBody + valueTemplate: reviewBody + - name: reviewRequestBody + valueTemplate: 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: | + ## Fortify vulnerability summary + + ### 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..b4eec36cf1 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml.bak @@ -0,0 +1,162 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-1.0.json + +# 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. + +author: Fortify +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=200 + 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: + - append: + - if: ${scanDates.contains(issue.removedDate)} + name: removedIssues + valueTemplate: removedIssues + - if: ${scanDates.contains(issue.foundDate) && issue.engineType=='SCA'} + name: newStaticIssues + valueTemplate: newStaticIssues + - if: ${scanDates.contains(issue.foundDate) && issue.engineType!='SCA'} + name: newOtherIssues + valueTemplate: newOtherIssues + - progress: Generating GitHub request + - set: + - name: reviewBody + valueTemplate: reviewBody + - name: reviewRequestBody + valueTemplate: 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..4bbec2055e --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-sast-report.yaml @@ -0,0 +1,194 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +# For now, github-sast-report and sarif-sast-report actions are exactly the same, apart from the +# following: +# - Different usage information +# - Different default Optional 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. + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). 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 + - name: page-size + description: "Number of vulnerabilities to retrieve at a time. Higher numbers may reduce time required to build the report, at the cost of increased memory usage (on both fcli and SSC), and could potentially negatively affect overall SSC performance or result in read time-outs (see --socket-timeout option on fcli ssc session login command). Default value: 100" + required: false + defaultValue: "100" + +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 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + filterset: ${parameters.filterset.guid} + limit: ${parameters['page-size']} + 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: + - append: + - name: ruleCategories + property: ${issue.primaryRuleGuid} + value: ${issue.issueName} + - name: results + valueTemplate: results + - progress: Processing rule data + - forEach: + processor: ${#ssc.ruleDescriptionsProcessor(parameters.appversion.id)} + name: rule + do: + - append: + - if: ${#isNotBlank(ruleCategories[rule.id])} + name: rules + valueTemplate: rules + - write: + - to: ${parameters.file} + valueTemplate: github-sast-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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?:{}} + properties: + copyright: ${#copyright()} + applicationName: ${parameters.appversion.project.name} + applicationId: ${parameters.appversion.project.id} + versionName: ${parameters.appversion.name} + versionId: ${parameters.appversion.id} + results: ${results?:{}} + + - name: rules + contents: + id: ${rule.id} + shortDescription: + text: ${ruleCategories[rule.id]} + fullDescription: + text: | + ## ${ruleCategories[rule.id]} + + ${rule.abstract} + help: + text: | + ${rule.explanation?:'No explanation available'} + + ## Recommendations + + ${rule.recommendations?:'Not available'} + + ## Tips + + ${#join('\n\n', rule.tips)?:'Not available'} + + ## References + + ${#numberedList(rule.references.![title + +(#isNotBlank(publisher)?", "+publisher:"") + +(#isNotBlank(author)?", "+author:"") + +(#isNotBlank(source)?", "+source:"")])?:'Not available'} + + ${#copyright()} + + - name: results + contents: + ruleId: ${issue.primaryRuleGuid} + message: + text: ${issue.details?.brief} [More information](${#ssc.issueBrowserUrl(issue,parameters.filterset)}) + level: ${(issue.friority matches "(Critical|High)") ? "warning":"note" } + partialFingerprints: + issueInstanceId: ${issue.issueInstanceId} + locations: + - physicalLocation: + artifactLocation: + uri: ${issue.fullFileName} + region: + startLine: ${issue.lineNumber==0||issue.lineNumber==null?1:issue.lineNumber} + endLine: ${issue.lineNumber==0||issue.lineNumber==null?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||line==null?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..634fb77bff --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-dast-report.yaml @@ -0,0 +1,169 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). 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 + - name: page-size + description: "Number of vulnerabilities to retrieve at a time. Higher numbers may reduce time required to build the report, at the cost of increased memory usage (on both fcli and SSC), and could potentially negatively affect overall SSC performance or result in read time-outs (see --socket-timeout option on fcli ssc session login command). Default value: 100" + required: false + defaultValue: "100" + +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 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:WEBINSPECT + filterset: ${parameters.filterset.guid} + limit: ${parameters['page-size']} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - append: + - name: vulnerabilities + valueTemplate: vulnerabilities + - write: + - to: ${parameters.file} + valueTemplate: gitlab-dast-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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..2735b6048b --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-debricked-report.yaml @@ -0,0 +1,130 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). 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 + - name: page-size + description: "Number of vulnerabilities to retrieve at a time. Higher numbers may reduce time required to build the report, at the cost of increased memory usage (on both fcli and SSC), and could potentially negatively affect overall SSC performance or result in read time-outs (see --socket-timeout option on fcli ssc session login command). Default value: 100" + required: false + defaultValue: "100" + +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 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:DEBRICKED + filterset: ${parameters.filterset.guid} + limit: ${parameters['page-size']} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - append: + - name: vulnerabilities + valueTemplate: vulnerabilities + - write: + - to: ${parameters.file} + valueTemplate: gitlab-debricked-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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/Debricked + 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..a90796e02f --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sast-report.yaml @@ -0,0 +1,127 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). 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 + - name: page-size + description: "Number of vulnerabilities to retrieve at a time. Higher numbers may reduce time required to build the report, at the cost of increased memory usage (on both fcli and SSC), and could potentially negatively affect overall SSC performance or result in read time-outs (see --socket-timeout option on fcli ssc session login command). Default value: 100" + required: false + defaultValue: "100" + +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 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + filterset: ${parameters.filterset.guid} + limit: ${parameters['page-size']} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - append: + - name: vulnerabilities + valueTemplate: vulnerabilities + - write: + - to: ${parameters.file} + valueTemplate: gitlab-sast-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + + +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..942aa5a605 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/gitlab-sonatype-report.yaml @@ -0,0 +1,128 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). 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 + - name: page-size + description: "Number of vulnerabilities to retrieve at a time. Higher numbers may reduce time required to build the report, at the cost of increased memory usage (on both fcli and SSC), and could potentially negatively affect overall SSC performance or result in read time-outs (see --socket-timeout option on fcli ssc session login command). Default value: 100" + required: false + defaultValue: "100" + +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 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SONATYPE + filterset: ${parameters.filterset.guid} + limit: ${parameters['page-size']} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - append: + - name: vulnerabilities + valueTemplate: vulnerabilities + - write: + - to: ${parameters.file} + valueTemplate: gitlab-sonatype-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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-sast-report.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sarif-sast-report.yaml new file mode 100644 index 0000000000..bf60642c70 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sarif-sast-report.yaml @@ -0,0 +1,193 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +# For now, github-sast-report and sarif-sast-report actions are exactly the same, apart from the +# following: +# - Different usage information +# - Different default Optional 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. + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). 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 + - name: page-size + description: "Number of vulnerabilities to retrieve at a time. Higher numbers may reduce time required to build the report, at the cost of increased memory usage (on both fcli and SSC), and could potentially negatively affect overall SSC performance or result in read time-outs (see --socket-timeout option on fcli ssc session login command). Default value: 100" + required: false + defaultValue: "100" + +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 + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + filterset: ${parameters.filterset.guid} + limit: ${parameters['page-size']} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + embed: + - name: details + uri: /api/v1/issueDetails/${issue.id} + do: + - append: + - name: ruleCategories + property: ${issue.primaryRuleGuid} + value: ${issue.issueName} + - name: results + valueTemplate: results + - progress: Processing rule data + - forEach: + processor: ${#ssc.ruleDescriptionsProcessor(parameters.appversion.id)} + name: rule + do: + - append: + - if: ${#isNotBlank(ruleCategories[rule.id])} + name: rules + valueTemplate: rules + - write: + - to: ${parameters.file} + valueTemplate: github-sast-report + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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?:{}} + properties: + copyright: ${#copyright()} + applicationName: ${parameters.appversion.project.name} + applicationId: ${parameters.appversion.project.id} + versionName: ${parameters.appversion.name} + versionId: ${parameters.appversion.id} + results: ${results?:{}} + + - name: rules + contents: + id: ${rule.id} + shortDescription: + text: ${ruleCategories[rule.id]} + fullDescription: + text: | + ## ${ruleCategories[rule.id]} + + ${rule.abstract} + help: + text: | + ${rule.explanation?:'No explanation available'} + + ## Recommendations + + ${rule.recommendations?:'Not available'} + + ## Tips + + ${#join('\n\n', rule.tips)?:'Not available'} + + ## References + + ${#numberedList(rule.references.![title + +(#isNotBlank(publisher)?", "+publisher:"") + +(#isNotBlank(author)?", "+author:"") + +(#isNotBlank(source)?", "+source:"")])?:'Not available'} + + ${#copyright()} + + - name: results + contents: + ruleId: ${issue.primaryRuleGuid} + message: + text: ${issue.details?.brief} [More information](${#ssc.issueBrowserUrl(issue,parameters.filterset)}) + level: ${(issue.friority matches "(Critical|High)") ? "warning":"note" } + partialFingerprints: + issueInstanceId: ${issue.issueInstanceId} + locations: + - physicalLocation: + artifactLocation: + uri: ${issue.fullFileName} + region: + startLine: ${issue.lineNumber==0||issue.lineNumber==null?1:issue.lineNumber} + endLine: ${issue.lineNumber==0||issue.lineNumber==null?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||line==null?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..9cfbf4e878 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/sonarqube-sast-report.yaml @@ -0,0 +1,79 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json + +author: Fortify +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: file + cliAliases: f + description: "Optional output file name (or 'stdout' / 'stderr'). 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 + - name: page-size + description: "Number of vulnerabilities to retrieve at a time. Higher numbers may reduce time required to build the report, at the cost of increased memory usage (on both fcli and SSC), and could potentially negatively affect overall SSC performance or result in read time-outs (see --socket-timeout option on fcli ssc session login command). Default value: 100" + required: false + defaultValue: "100" + +steps: + - progress: Processing issue data + - requests: + - name: issues + uri: /api/v1/projectVersions/${parameters.appversion.id}/issues + query: + filter: ISSUE[11111111-1111-1111-1111-111111111151]:SCA + filterset: ${parameters.filterset.guid} + limit: ${parameters['page-size']} + pagingProgress: + postPageProcess: Processed ${totalIssueCount?:0} of ${issues_raw.count} issues + forEach: + name: issue + do: + - append: + - name: sq_issues + valueTemplate: sq_issues + - write: + - to: ${parameters.file} + valueTemplate: sq_output + - if: ${parameters.file!='stdout'} + to: stdout + value: | + Output written to ${parameters.file} + +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..e97d36e8a9 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 @@ -1,7 +1,8 @@ # Used to 'productize' some descriptions defined in FortifyCLIMessages.properties product=SSC +module=ssc default-connect-timeout = 10 seconds -default-socket-timeout = 60 seconds +default-socket-timeout = 5 minutes # Make sure none of the commands inherit usage header or description of the top-level fcli command # Note that we define these as usage.* whereas our parent bundle defines fcli.usage.* (with fcli prefix). @@ -127,7 +128,81 @@ 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 +# Apart from the top-level usage header, which includes an SSC reference, all headers +# and descriptions are the same as for other action modules like FoD. When updating here, +# the same updates should be made in other modules. +fcli.ssc.action.usage.header = Manage SSC actions: data export, integration, automation & more. +fcli.ssc.action.get.usage.header = Get action contents. +fcli.ssc.action.help.usage.header = Show action usage help. +fcli.ssc.action.import.usage.header = Import custom actions. +fcli.ssc.action.list.usage.header = List built-in and imported actions. +fcli.ssc.action.reset.usage.header = Remove all custom actions. +fcli.ssc.action.run.usage.header = Run an action. +fcli.ssc.action.sign.usage.header = Sign action. +fcli.ssc.action.sign.confirm = Confirm overwriting existing output file. +fcli.ssc.action.sign.confirmPrompt = Do you want overwrite existing output file %s? +fcli.action.source.single = The action can be specified as either a simple name or a local or \ + remote action YAML file location. If specified as a simple name, the action will be loaded from \ + the list of built-in and imported custom actions unless the `--from-zip` option is specified, \ + in which case the action will be loaded from the given local or remote zip file. The `--from-zip` \ + option will only be used if action is specified as a simple name, it will be ignored if the action \ + is specified as a local or remote action YAML file location. +fcli.ssc.action.usage.description = Fcli supports workflow-style actions defined in YAML files. Many \ + built-in actions are provided, focusing on data export and CI/CD integration. Users can also develop \ + their own custom actions, either from scratch or by customizing built-in actions. If you require any \ + assistance with developing custom actions, please consult with Fortify Professional Services. \ + %n%nNote that the ability to load and run custom actions is currently considered PREVIEW \ + functionality; custom actions developed for this fcli version may fail on other fcli versions, \ + even between minor fcli releases. Based on user feedback, we will stabilize action syntax over \ + the next couple of fcli releases, after which any breaking action syntax changes will be considered \ + a major fcli version change. \ + %n%nThis fcli version supports the following action schema versions: ${fcli.action.supportedSchemaVersions:-See fcli help output}. \ + %n%nActions can potentially perform dangerous operations like deleting data or posting data to 3rd-party \ + systems, so it is recommended to only run trusted actions. Action authors can sign their actions using \ + the `action sign` command; actions without a (valid) signature will require confirmation when trying to \ + run them. Trusted public keys can be configured through the `fcli config public-key` commands, \ + or passed directly using the `--pubkey` option on various action-related commands. +fcli.ssc.action.get.usage.description = This command allows for listing the YAML contents of built-in or \ + custom actions. This allows for reviewing the operations performed by an action, or for using the action \ + contents as a basis for developing custom actions. \ + %n%n${fcli.action.source.single} +fcli.ssc.action.help.usage.description = This command allows for showing the help information for the given \ + built-in or custom action. \ + ${fcli.action.source.single} +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%n${fcli.action.source.single} \ + If only `--from-zip` is specified, all actions from that zip-file will be imported. +fcli.ssc.action.list.usage.description = By default, this command lists available built-in and \ + previously imported custom actions. If the `--from-zip` option is specified, this command \ + lists available actions from the given local or remote zip file instead. +fcli.ssc.action.reset.usage.description = Remove all previously imported custom actions, restoring \ + fcli configuration to provide the default built-in actions only. +fcli.ssc.action.run.usage.description = This command allows for running built-in or custom actions. As actions \ + may perform potentially dangerous operations like deleting data or posting data to 3rd-party systems, you should \ + only run trusted actions. For this reason, fcli requires confirmation when attempting to run an action without \ + a (valid) signature. \ + %n%n${fcli.action.source.single} +fcli.ssc.action.sign.usage.description = This command allows for signing custom actions, allowing those actions \ + to be run without confirmation if the corresponding public key has been imported through the \ + `fcli config public-key import` command or passed in the `--pubkey` option on various action \ + commands. The action to be signed must be a local file. \ + %n%nThis command can use an existing private key for signing, or generate a new key pair if the \ + private key file as specified through the `--with` option doesn't yet exist and `--pubout` is \ + specified to output the corresponding public key. \ + %n%nPrivate keys may also be generated using OpenSSL or similar tools, but note that only RSA keys in PEM format are supported, \ + and only a small set of encryption schemes are supported for encrypted private keys. It is recommended to use AES \ + encryption, which is supported by both native fcli executables and the .jar version of fcli. The latter requires \ + Java 19 or above though to handle AES-encrypted private keys. Following is a sample OpenSSL command for generating an \ + encrypted private key that's supported by fcli for signing: %n openssl genpkey -algorithm rsa -out private-key.pem -aes256 \ + %n%nFor convenience, when using a pre-existing private key, the `--pubout` option allows for outputting the corresponding \ + public key for use by the `fcli config public-key import` command. Note that public keys will not be automatically added \ + to the fcli trusted public key store; even if this command generates a key pair on the fly, you'll still need to import \ + the generated public key using the `fcli config public-key import` command. + # 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. @@ -340,6 +415,13 @@ fcli.ssc.issue.count.by = Vulnerability grouping type. See 'fcli ssc issue list- allowed values. Default value: ${DEFAULT-VALUE}. fcli.ssc.issue.count.filter = Filter issue counts using the given (friendly or technical) filter. \ See 'fcli ssc issue list-filters' for allowed values. +fcli.ssc.issue.list.usage.header = List application version vulnerabilities. +fcli.ssc.issue.list.filter = Filter issues using the given (friendly or technical) filter. \ + See 'fcli ssc issue list-filters' for allowed values. +fcli.ssc.issue.list.embed = Embed extra application version data. Allowed values: ${COMPLETION-CANDIDATES}. \ + Using the --output option, this extra data can be included in the output. Using the --query option, \ + this extra data can be queried upon. To get an understanding of the structure and contents of the \ + embedded data, use the --output json or --output yaml options. fcli.ssc.issue.get-filter.usage.header = Get issue filter details. fcli.ssc.issue.filter = Technical or friendly filter as returned by the 'fcli ssc issue list-filters' command. fcli.ssc.issue.list-filters.usage.header = List application version issue filters. @@ -365,6 +447,7 @@ fcli.ssc.issue.update-template.name = Update issue template name. fcli.ssc.issue.update-template.description = Update issue template description. fcli.ssc.issue.update-template.set-as-default = Set this issue template as the default issue template. fcli.ssc.issue.template.resolver.nameOrId = Issue template name or id. +fcli.ssc.issue.check.usage.header = (PREVIEW) Check issue count. # fcli ssc performance-indicator fcli.ssc.performance-indicator.usage.header = (PREVIEW) Manage SSC performance indicators & definitions. @@ -466,6 +549,9 @@ 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,author,origin,status,signatureStatus,usageHeader +fcli.ssc.action.import.output.table.options = name,author,status,signatureStatus,usageHeader +fcli.ssc.action.sign.output.table.options = in,out,publicKeyFingerprint 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 @@ -485,10 +571,12 @@ fcli.ssc.artifact.output.table.options = id,scanTypes,lastScanDate,uploadDate,st fcli.ssc.attribute.output.table.options = id,category,guid,name,valueString fcli.ssc.attribute.definition.output.table.options = id,category,guid,name,type,required fcli.ssc.issue.count.output.table.options = cleanName,totalCount,auditedCount +fcli.ssc.issue.list.output.table.options = id,primaryLocation,lineNumber,issueName fcli.ssc.issue.filter-set.output.table.options = guid,title,defaultFilterSet fcli.ssc.issue.group.output.table.options = guid,displayName,entityType fcli.ssc.issue.filter.output.table.options = entityType,friendlyFilter,technicalFilter fcli.ssc.issue.template.output.table.options = id,name,inUse,defaultTemplate,publishVersion,originalFileName +fcli.ssc.issue.check.output.table.options = result fcli.ssc.performance-indicator.definition.output.table.options = id,guid,name,equation,range fcli.ssc.performance-indicator.output.table.options = id,name,timestamp,valueString fcli.ssc.variable.definition.output.table.options = id,guid,name,searchString diff --git a/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/_common/action/AbstractActionTest.java b/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/_common/action/AbstractActionTest.java new file mode 100644 index 0000000000..26c7ade76e --- /dev/null +++ b/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/_common/action/AbstractActionTest.java @@ -0,0 +1,66 @@ +/** + * 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.ssc._common.action; + +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fortify.cli.common.action.helper.ActionLoaderHelper; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionSource; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler.ActionInvalidSchemaVersionHandler; +import com.fortify.cli.common.action.helper.ActionLoaderHelper.ActionValidationHandler.ActionInvalidSignatureHandler; +import com.fortify.cli.common.action.model.Action.ActionMetadata; +import com.fortify.cli.common.crypto.helper.SignatureHelper.SignedTextDescriptor; + +// TODO Move this class to a common test utility module; currently +// exact copies of this class are available in every module +// that performs action tests. +@TestInstance(Lifecycle.PER_CLASS) +public abstract class AbstractActionTest { + @ParameterizedTest + @MethodSource("getActions") + public void testLoadAction(String name) { + try { + var actionValidationHandler = ActionValidationHandler.builder() + .onSignatureStatusDefault(invalidSignatureHandler()) + .onUnsupportedSchemaVersion(ActionInvalidSchemaVersionHandler.fail) + .build(); + ActionLoaderHelper + .load(ActionSource.builtinActionSources(getType()), name, actionValidationHandler) + .getAction(); + } catch ( Exception e ) { + System.err.println(String.format("Error loading %s action %s:\n%s", getType(), name, e)); + Assertions.fail(String.format("Error loading %s action %s", getType(), name), e); + } + } + + public final String[] getActions() { + return ActionLoaderHelper.streamAsJson(ActionSource.builtinActionSources(getType()), ActionValidationHandler.IGNORE) + .map(a->a.get("name").asText()) + .toArray(String[]::new); + } + + private static final BiConsumer invalidSignatureHandler() { + return "true".equalsIgnoreCase((System.getProperty("test.action.requireValidSignature"))) + ? ActionInvalidSignatureHandler.fail + : ActionInvalidSignatureHandler.warn; + } + + protected abstract String getType(); +} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/_common/action/SSCActionTest.java b/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/_common/action/SSCActionTest.java new file mode 100644 index 0000000000..99b02d099e --- /dev/null +++ b/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/_common/action/SSCActionTest.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * 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._common.action; + +import lombok.Getter; + +public class SSCActionTest extends AbstractActionTest { + @Getter private final String type = "SSC"; +} diff --git a/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/rest/query/SSCQParamGeneratorTest.java b/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/_common/rest/query/SSCQParamGeneratorTest.java similarity index 92% rename from fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/rest/query/SSCQParamGeneratorTest.java rename to fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/_common/rest/query/SSCQParamGeneratorTest.java index 5644420202..26599153bb 100644 --- a/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/rest/query/SSCQParamGeneratorTest.java +++ b/fcli-core/fcli-ssc/src/test/java/com/fortify/cli/ssc/_common/rest/query/SSCQParamGeneratorTest.java @@ -10,7 +10,7 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.ssc.rest.query; +package com.fortify.cli.ssc._common.rest.query; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; @@ -19,9 +19,6 @@ import org.springframework.expression.spel.SpelParseException; import org.springframework.expression.spel.standard.SpelExpressionParser; -import com.fortify.cli.ssc._common.rest.query.SSCQParamGenerator; -import com.fortify.cli.ssc._common.rest.query.SSCQParamValueGenerators; - public class SSCQParamGeneratorTest { private SSCQParamGenerator generator = new SSCQParamGenerator() .add("plain", SSCQParamValueGenerators::plain) diff --git a/fcli-core/fcli-tool/build.gradle b/fcli-core/fcli-tool/build.gradle index a94c467c3a..df6b2e59b2 100644 --- a/fcli-core/fcli-tool/build.gradle +++ b/fcli-core/fcli-tool/build.gradle @@ -1,5 +1,5 @@ plugins { - id "de.undercouch.download" version "5.5.0" + id "de.undercouch.download" version "5.6.0" } apply from: "${sharedGradleScriptsDir}/fcli-module.gradle" diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolListPlatformsCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolListPlatformsCommand.java index 59b3a74333..6f0310c16f 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolListPlatformsCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/cli/cmd/AbstractToolListPlatformsCommand.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/SignatureHelper.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/SignatureHelper.java deleted file mode 100644 index 388d13924c..0000000000 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/SignatureHelper.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.fortify.cli.tool._common.helper; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; - -import org.apache.commons.codec.binary.Base64; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -public class SignatureHelper { - private static final Logger LOG = LoggerFactory.getLogger(SignatureHelper.class); - private static String pubKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArij9U9yJVNc53oEMFWYp" - + "NrXUG1UoRZseDh/p34q1uywD70RGKKWZvXIcUAZZwbZtCu4i0UzsrKRJeUwqanbc" - + "woJvYanp6lc3DccXUN1w1Y0WOHOaBxiiK3B1TtEIH1cK/X+ZzazPG5nX7TSGh8Tp" - + "/uxQzUFli2mDVLqaP62/fB9uJ2joX9Gtw8sZfuPGNMRoc8IdhjagbFkhFT7WCZnk" - + "FH/4Co007lmXLAe12lQQqR/pOTeHJv1sfda1xaHtj4/Tcrq04Kx0ZmGAd5D9lA92" - + "8pdBbzoe/mI5/Sk+nIY3AHkLXB9YAaKJf//Wb1yiP1/hchtVkfXyIaGM+cVyn7AN" - + "VQIDAQAB"; - - public static final void verifyFileSignature(File destFile, String expectedSignature, boolean throwOnFailure) { - try { - Signature signature = createSignature(); - updateSignature(signature, destFile); - verifySignature(signature, expectedSignature); - } catch (Exception e) { - handleSignatureException(e, throwOnFailure); - } - } - - private static Signature createSignature() throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { - X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decodeBase64(pubKey)); - KeyFactory kf = KeyFactory.getInstance("RSA"); - PublicKey pub = kf.generatePublic(spec); - Signature signature = Signature.getInstance("SHA256withRSA"); - signature.initVerify(pub); - return signature; - } - - private static void updateSignature(Signature signature, File destFile) - throws IOException, SignatureException, FileNotFoundException { - try ( var is = new FileInputStream(destFile); ) { - byte[] buffer = new byte[4096]; - int read = 0; - while ( (read = is.read(buffer)) > 0 ) { - signature.update(buffer, 0, read); - } - } - } - - private static void verifySignature(Signature signature, String expectedSignature) - throws SignatureException, SignatureMismatchException { - if(!signature.verify(Base64.decodeBase64(expectedSignature))) { - String msg = "Signature mismatch" - +"\n Expected: "+expectedSignature - +"\n Actual: "+signature.hashCode(); - throw new SignatureMismatchException(msg); - } - } - - private static void handleSignatureException(Exception e, boolean throwOnFailure) { - if(!throwOnFailure) { - LOG.warn("Signature verification failed", e); - } else { - if ( e instanceof RuntimeException ) { - throw (RuntimeException)e; - } else { - throw new IllegalStateException("Signature verification failed", e); - } - } - } - - public static final class SignatureMismatchException extends IllegalStateException { - private static final long serialVersionUID = 1L; - public SignatureMismatchException(String msg) { - super(msg); - } - } - - -} diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationHelper.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationHelper.java index 6db7e2efb2..c584ccd59d 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationHelper.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstallationHelper.java @@ -16,7 +16,7 @@ import java.util.Set; import com.fortify.cli.common.util.FcliDataHelper; -import com.fortify.cli.common.util.SemVerHelper; +import com.fortify.cli.common.util.SemVer; import com.fortify.cli.tool.definitions.helper.ToolDefinitionVersionDescriptor; public final class ToolInstallationHelper { @@ -42,10 +42,11 @@ public static final Path getToolsStatePath() { */ public static final boolean isCandidateForUninstall(String toolName, Set versionsToUninstall, ToolDefinitionVersionDescriptor versionDescriptor) { var version = versionDescriptor.getVersion(); + var semver = new SemVer(version); return (versionsToUninstall.contains("all") || containsCandidateForUninstall(versionsToUninstall, version) - || containsCandidateForUninstall(versionsToUninstall, SemVerHelper.getMajor(version).orElse("N/A")) - || containsCandidateForUninstall(versionsToUninstall, SemVerHelper.getMajorMinor(version).orElse("N/A"))) + || containsCandidateForUninstall(versionsToUninstall, semver.getMajor().orElse("N/A")) + || containsCandidateForUninstall(versionsToUninstall, semver.getMajorMinor().orElse("N/A"))) && ToolInstallationDescriptor.load(toolName, versionDescriptor)!=null; } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstaller.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstaller.java index 932f9fd528..8e102bacb0 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstaller.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstaller.java @@ -30,6 +30,7 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import com.fortify.cli.common.crypto.helper.SignatureHelper; import com.fortify.cli.common.progress.helper.IProgressWriterI18n; import com.fortify.cli.common.rest.unirest.UnirestHelper; import com.fortify.cli.common.util.FileUtils; @@ -216,7 +217,9 @@ private void downloadAndExtract(ToolDefinitionArtifactDescriptor artifactDescrip progressWriter.writeProgress("Downloading tool binaries"); File downloadedFile = download(artifactDescriptor); progressWriter.writeProgress("Verifying signature"); - SignatureHelper.verifyFileSignature(downloadedFile, artifactDescriptor.getRsa_sha256(), onDigestMismatch == DigestMismatchAction.fail); + SignatureHelper.fortifySignatureVerifier() + .verify(downloadedFile, artifactDescriptor.getRsa_sha256()) + .throwIfNotValid(onDigestMismatch == DigestMismatchAction.fail); progressWriter.writeProgress("Installing tool binaries"); copyOrExtract(artifactDescriptor, downloadedFile); } diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolUninstaller.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolUninstaller.java index 5da340aeb5..25754e8f12 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolUninstaller.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolUninstaller.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 @@ -23,7 +23,7 @@ import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.util.FcliDataHelper; import com.fortify.cli.common.util.FileUtils; -import com.fortify.cli.common.util.SemVerHelper; +import com.fortify.cli.common.util.SemVer; import com.fortify.cli.tool.definitions.helper.ToolDefinitionVersionDescriptor; import lombok.Data; @@ -44,7 +44,7 @@ public final ToolInstallationOutputDescriptor uninstall(ToolDefinitionVersionDes var action = "UNINSTALLED"; if ( !FileUtils.isDirPathInUse(installPath) ) { FileUtils.deleteRecursive(installPath); - } else if (replacementVersion==null || SemVerHelper.compare(replacementVersion.getVersion(), "2.2.0")<0 ) { + } else if (replacementVersion==null || new SemVer(replacementVersion.getVersion()).compareTo("2.2.0")<0 ) { action = "MANUAL_DELETE_REQUIRED"; } else { action = "PENDING_FCLI_RESTART"; diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/bugtracker_utility/cli/cmd/ToolBugTrackerUtilityListPlatformsCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/bugtracker_utility/cli/cmd/ToolBugTrackerUtilityListPlatformsCommand.java index d17a2fba68..d68c2bf077 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/bugtracker_utility/cli/cmd/ToolBugTrackerUtilityListPlatformsCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/bugtracker_utility/cli/cmd/ToolBugTrackerUtilityListPlatformsCommand.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/debricked_cli/cli/cmd/ToolDebrickedCliListPlatformsCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/debricked_cli/cli/cmd/ToolDebrickedCliListPlatformsCommand.java index 44908842e4..4ea7a4f8e1 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/debricked_cli/cli/cmd/ToolDebrickedCliListPlatformsCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/debricked_cli/cli/cmd/ToolDebrickedCliListPlatformsCommand.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsOutputDescriptor.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsOutputDescriptor.java index cd21901111..99c6141e17 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsOutputDescriptor.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsOutputDescriptor.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsStateDescriptor.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsStateDescriptor.java index 27e1b96ec7..abc2e60792 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsStateDescriptor.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionsStateDescriptor.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/fcli/cli/cmd/ToolFcliListPlatformsCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/fcli/cli/cmd/ToolFcliListPlatformsCommand.java index 94e4da6530..f3dedeec0c 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/fcli/cli/cmd/ToolFcliListPlatformsCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/fcli/cli/cmd/ToolFcliListPlatformsCommand.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/fod_uploader/cli/cmd/ToolFoDUploaderListPlatformsCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/fod_uploader/cli/cmd/ToolFoDUploaderListPlatformsCommand.java index aa773fcf5f..e1cbfd1189 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/fod_uploader/cli/cmd/ToolFoDUploaderListPlatformsCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/fod_uploader/cli/cmd/ToolFoDUploaderListPlatformsCommand.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sc_client/cli/cmd/ToolSCClientListPlatformsCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sc_client/cli/cmd/ToolSCClientListPlatformsCommand.java index 0caca76012..619087b99a 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sc_client/cli/cmd/ToolSCClientListPlatformsCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sc_client/cli/cmd/ToolSCClientListPlatformsCommand.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/vuln_exporter/cli/cmd/ToolVulnExporterListPlatformsCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/vuln_exporter/cli/cmd/ToolVulnExporterListPlatformsCommand.java index 191af0b013..74362b0f67 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/vuln_exporter/cli/cmd/ToolVulnExporterListPlatformsCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/vuln_exporter/cli/cmd/ToolVulnExporterListPlatformsCommand.java @@ -2,7 +2,7 @@ * Copyright 2023 Open Text. * * The only warranties for products and services of Open Text - * and its affiliates and licensors (“Open Text”) are as may + * 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 diff --git a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/all_commands/cli/cmd/AllCommandsUsageCommand.java b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/all_commands/cli/cmd/AllCommandsUsageCommand.java index 16250767f5..75aea44724 100644 --- a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/all_commands/cli/cmd/AllCommandsUsageCommand.java +++ b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/all_commands/cli/cmd/AllCommandsUsageCommand.java @@ -21,14 +21,15 @@ import picocli.CommandLine.Model.CommandSpec; @Command(name = "usage") -public final class AllCommandsUsageCommand extends AbstractRunnableCommand implements Runnable { +public final class AllCommandsUsageCommand extends AbstractRunnableCommand { @Mixin private AllCommandsCommandSelectorMixin selectorMixin; @Override - public final void run() { + public final Integer call() { initMixins(); selectorMixin.getSelectedCommands().getSpecs() .forEach(this::printHelp); + return 0; } private void printHelp(CommandSpec spec) { diff --git a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/autocomplete/cli/cmd/AutoCompleteGenerationCommand.java b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/autocomplete/cli/cmd/AutoCompleteGenerationCommand.java index 206238d21d..386794a88a 100644 --- a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/autocomplete/cli/cmd/AutoCompleteGenerationCommand.java +++ b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/autocomplete/cli/cmd/AutoCompleteGenerationCommand.java @@ -27,15 +27,16 @@ * */ @Command(name = "generate") -public final class AutoCompleteGenerationCommand extends AbstractRunnableCommand implements Runnable { +public final class AutoCompleteGenerationCommand extends AbstractRunnableCommand { @Spec CommandSpec spec; - public void run() { + public Integer call() { String script = AutoComplete.bash(spec.root().name(), spec.root().commandLine()); // not PrintWriter.println: scripts with Windows line separators fail in strange // ways! spec.commandLine().getOut().print(script); spec.commandLine().getOut().print('\n'); spec.commandLine().getOut().flush(); + return 0; } } diff --git a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/AbstractCryptoCommand.java b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/AbstractCryptoCommand.java index b7f72bf26d..9815a6be71 100644 --- a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/AbstractCryptoCommand.java +++ b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/AbstractCryptoCommand.java @@ -20,11 +20,11 @@ import lombok.SneakyThrows; import picocli.CommandLine.Mixin; -public abstract class AbstractCryptoCommand extends AbstractRunnableCommand implements Runnable { +public abstract class AbstractCryptoCommand extends AbstractRunnableCommand { @Mixin private CommandHelperMixin commandHelper; @Override @SneakyThrows - public final void run() { + public final Integer call() { initMixins(); String prompt = commandHelper.getMessageResolver().getMessageString("prompt")+" "; String value; @@ -37,6 +37,7 @@ public final void run() { } } System.out.println(process(value)); + return 0; } protected abstract String process(String value); diff --git a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/CryptoDecryptCommand.java b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/CryptoDecryptCommand.java index 431bcf9e8a..4956399707 100644 --- a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/CryptoDecryptCommand.java +++ b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/CryptoDecryptCommand.java @@ -12,7 +12,7 @@ *******************************************************************************/ package com.fortify.cli.util.crypto.cli.cmd; -import com.fortify.cli.common.util.EncryptionHelper; +import com.fortify.cli.common.crypto.helper.EncryptionHelper; import picocli.CommandLine.Command; diff --git a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/CryptoEncryptCommand.java b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/CryptoEncryptCommand.java index 18a7efa732..360a82c7c1 100644 --- a/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/CryptoEncryptCommand.java +++ b/fcli-core/fcli-util/src/main/java/com/fortify/cli/util/crypto/cli/cmd/CryptoEncryptCommand.java @@ -12,7 +12,7 @@ *******************************************************************************/ package com.fortify.cli.util.crypto.cli.cmd; -import com.fortify.cli.common.util.EncryptionHelper; +import com.fortify.cli.common.crypto.helper.EncryptionHelper; import picocli.CommandLine.Command; diff --git a/fcli-other/fcli-bom/build.gradle b/fcli-other/fcli-bom/build.gradle index 236a6cc0ed..1ecd2d114e 100644 --- a/fcli-other/fcli-bom/build.gradle +++ b/fcli-other/fcli-bom/build.gradle @@ -9,25 +9,25 @@ javaPlatform { } dependencies { - api platform('com.fasterxml.jackson:jackson-bom:2.15.2') - api platform('org.springframework:spring-framework-bom:6.0.11') + api platform('com.fasterxml.jackson:jackson-bom:2.17.0') + api platform('org.springframework:spring-framework-bom:6.1.5') constraints { // Picocli - api("info.picocli:picocli:4.7.4") - api("info.picocli:picocli-codegen:4.7.4") + api("info.picocli:picocli:4.7.5") + api("info.picocli:picocli-codegen:4.7.5") // Annotation-based reflect-config.json generation api('com.formkiq:graalvm-annotations:1.2.0') - api('com.formkiq:graalvm-annotations-processor:1.4.1') + api('com.formkiq:graalvm-annotations-processor:1.4.2') // ANSI support - api("org.fusesource.jansi:jansi:2.4.0") + api("org.fusesource.jansi:jansi:2.4.1") // Logging - api('org.slf4j:slf4j-api:2.0.7') - api('org.slf4j:jcl-over-slf4j:2.0.7') - api("ch.qos.logback:logback-classic:1.4.8") + api('org.slf4j:slf4j-api:2.0.12') + api('org.slf4j:jcl-over-slf4j:2.0.12') + api("ch.qos.logback:logback-classic:1.5.3") // REST client api('com.konghq:unirest-java:3.14.5') @@ -39,7 +39,7 @@ dependencies { //api('org.springframework.integration:spring-integration-core:6.1.1') // Output formatting - api('hu.webarticum:tree-printer:3.1.0') + api('hu.webarticum:tree-printer:3.2.0') api('com.github.freva:ascii-table:1.8.0') // Remove annotation processor warning. @@ -49,11 +49,14 @@ dependencies { api('org.jasypt:jasypt:1.9.3:lite') // Test dependencies - api 'org.junit.jupiter:junit-jupiter-api:5.9.3' - api 'org.junit.jupiter:junit-jupiter-params:5.9.3' - api 'org.junit.jupiter:junit-jupiter-engine:5.9.3' + api 'org.junit.jupiter:junit-jupiter-api:5.10.2' + api 'org.junit.jupiter:junit-jupiter-params:5.10.2' + api 'org.junit.jupiter:junit-jupiter-engine:5.10.2' - //required for unpacking tar.gz (debricked cli) - api('org.apache.commons:commons-compress:1.25.0') + // Required for unpacking tar.gz (debricked cli) + api('org.apache.commons:commons-compress:1.26.1') + + // 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/build.gradle b/fcli-other/fcli-doc/build.gradle index daff809f79..41434c1075 100644 --- a/fcli-other/fcli-doc/build.gradle +++ b/fcli-other/fcli-doc/build.gradle @@ -2,8 +2,11 @@ apply plugin: 'java' apply plugin: 'org.asciidoctor.jvm.convert' dependencies { - runtimeOnly project("${fcliAppRef}") + implementation project("${fcliCommonRef}") + implementation project("${fcliAppRef}") runtimeOnly 'info.picocli:picocli-codegen' + implementation 'com.github.victools:jsonschema-generator:4.35.0' + implementation 'com.github.victools:jsonschema-module-jackson:4.35.0' } ext { @@ -17,9 +20,19 @@ ext { htmlAsciiDocDir = "${docsBuildDir}/html/asciidoc" htmlOutputDir = "${docsBuildDir}/html/output" ghPagesStaticOutputDir = "${docsBuildDir}/gh-pages/static/output" + schemaOutputDir = "${ghPagesStaticOutputDir}/schemas" + actionSchemaOutputDir = "${schemaOutputDir}/action" ghPagesVersionedOutputDir = "${docsBuildDir}/gh-pages/versioned/output" } +task generateActionSchema(type: JavaExec) { + dependsOn('build') + classpath = sourceSets.main.runtimeClasspath + main 'com.fortify.cli.common.action.schema.generator.GenerateActionSchema' + // Pass whether this is an (fcli) development release, schema version and output dir + args project.version.startsWith("0."), fcliActionSchemaVersion, actionSchemaOutputDir +} + task generateManpageAsciiDoc(type: JavaExec) { group = "Documentation" description = "Generate AsciiDoc manpage" @@ -30,7 +43,6 @@ task generateManpageAsciiDoc(type: JavaExec) { task generateManpageOutput(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { dependsOn(generateManpageAsciiDoc) - inProcess = JAVA_EXEC forkOptions { jvmArgs("--add-opens","java.base/sun.nio.ch=ALL-UNNAMED","--add-opens","java.base/java.io=ALL-UNNAMED") } @@ -57,8 +69,7 @@ task prepareAsciiDocForVersionedHtml(type: Copy) { // Generate HTML documentation from AsciiDoc prepared by prepareAsciiDocForVersionedHtml task asciiDoctorVersionedHtml(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { dependsOn(prepareAsciiDocForVersionedHtml) - inProcess = JAVA_EXEC - forkOptions { + forkOptions { jvmArgs("--add-opens","java.base/sun.nio.ch=ALL-UNNAMED","--add-opens","java.base/java.io=ALL-UNNAMED") } sourceDir = file("${htmlAsciiDocDir}") @@ -68,12 +79,14 @@ task asciiDoctorVersionedHtml(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) backends = ['html5'] } attributes = [ - 'toc' : 'left', - 'sectanchors' : 'true', - 'docinfo' : 'shared', - 'jekyll' : false, - 'bannertitle' : 'FCLI: The Universal Fortify CLI', - 'docversion' : "${project.version}" + 'toc' : 'left', + 'sectanchors' : 'true', + 'docinfo' : 'shared', + 'jekyll' : false, + 'bannertitle' : 'FCLI: The Universal Fortify CLI', + 'docversion' : "${project.version}", + 'actionSchemaVersion' : "${fcliActionSchemaVersion}", + 'actionSchemaUrl' : "${fcliActionSchemaUrl}" ] options = [ 'template_dirs': [new File("${asciiDocTemplatesSrcDir}").absolutePath] @@ -84,7 +97,6 @@ task asciiDoctorVersionedHtml(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) // to be deployed to a version-specific directory on the fcli gh-pages site task asciiDoctorVersionedJekyll(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { dependsOn(prepareAsciiDocForVersionedHtml) - inProcess = JAVA_EXEC forkOptions { jvmArgs("--add-opens","java.base/sun.nio.ch=ALL-UNNAMED","--add-opens","java.base/java.io=ALL-UNNAMED") } @@ -95,13 +107,15 @@ task asciiDoctorVersionedJekyll(type: org.asciidoctor.gradle.jvm.AsciidoctorTask backends = ['html5'] } attributes = [ - 'toc' : 'left', - 'sectanchors' : 'true', - 'docinfo' : 'shared', - 'jekyll' : true, - 'stylesheet' : false, - 'bannertitle' : 'FCLI: The Universal Fortify CLI', - 'docversion' : "${project.version}" + 'toc' : 'left', + 'sectanchors' : 'true', + 'docinfo' : 'shared', + 'jekyll' : true, + 'stylesheet' : false, + 'bannertitle' : 'FCLI: The Universal Fortify CLI', + 'docversion' : "${project.version}", + 'actionSchemaVersion' : "${fcliActionSchemaVersion}", + 'actionSchemaUrl' : "${fcliActionSchemaUrl}" ] options = [ 'template_dirs': [new File("${asciiDocTemplatesSrcDir}").absolutePath] @@ -111,7 +125,6 @@ task asciiDoctorVersionedJekyll(type: org.asciidoctor.gradle.jvm.AsciidoctorTask // Generate Jekyll HTML documentation from AsciiDoc files in ${staticAsciiDocSrcDir} // to be deployed to the root directory on the fcli gh-pages site task asciiDoctorStaticJekyll(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { - inProcess = JAVA_EXEC forkOptions { jvmArgs("--add-opens","java.base/sun.nio.ch=ALL-UNNAMED","--add-opens","java.base/java.io=ALL-UNNAMED") } @@ -162,7 +175,7 @@ task distDocsVersionedJekyll(type: Zip) { // Zip the output of the asciiDoctorStaticJekyl task into ${releaseAssetsDir} task distDocsStaticJekyll(type: Zip) { - dependsOn 'asciiDoctorStaticJekyll' + dependsOn 'asciiDoctorStaticJekyll', 'generateActionSchema' archiveFileName = "docs-gh-pages.zip" destinationDirectory = file("$distDir") from layout.buildDirectory.dir("${ghPagesStaticOutputDir}") diff --git a/fcli-other/fcli-doc/src/docs/asciidoc/static/dev-info.adoc b/fcli-other/fcli-doc/src/docs/asciidoc/static/dev-info.adoc index 8418167e21..54e84ad67b 100644 --- a/fcli-other/fcli-doc/src/docs/asciidoc/static/dev-info.adoc +++ b/fcli-other/fcli-doc/src/docs/asciidoc/static/dev-info.adoc @@ -1,10 +1,24 @@ = Fortify CLI (fcli) Developer Information -The following sections provide information that may be useful for developers of this utility. +The following sections provide information that may be useful for developers of this utility. Note that this documentation is not always properly updated, so some details may be out of date and no longer apply. However, general concepts won't change very often, so even with some outdated details, it's still useful to review this documentation if you're contributing to fcli. If you notice anything that's incorrect or outdated, please let us know or update yourself. + +== Branches + +The `develop` branch is our primary branch for active fcli development. In general, new features/fixes/... should be developed in a dedicated branch or fcli fork, and eventually merged into the `develop` branch by raising a Pull Request. + +Once we are ready for releasing a new fcli version, the `develop` branch will be merged into the `main` branch, which, based on conventional commit messages (see next section) will result in a release PR to be automatically raised; merging this PR will automatically build the fcli release version and publish the various release artifacts. Once release, the `main` branch needs to be merged back into the `develop` branch to apply changelog and version updates. + +In some cases, the process above may require a rewrite of the commit history on the `develop` branch, as we may have a need to rewrite some commit messages to generate proper release notes. So, after an fcli release, you should always update/replace any branches/forks/local copies to apply the new commit history to avoid issues when merging new changes. + +In general, the `main` branch shouldn't be touched (other than as described above), except if we need to apply some (relatively urgent) bug fix at a time when the `develop` branch is not yet ready for release. In general, this is only applicable if we've already merged some features to the `develop` branch that still need more work before final release. + +Apart from the branches listed above, there's also a `gh-pages` branch that is used to publish documentation to https://fortify.github.io/fcli. Most of the files in this branch are either static or automatically generated during fcli builds, so other than changing documentation structure or performing cleanup (like removing documentation for outdated fcli versions), in general this branch shouldn't be touched. == Conventional Commits & Versioning -Versioning is handled automatically by https://github.com/google-github-actions/release-please-action[release-please-action] based on https://www.conventionalcommits.org/[Conventional Commits]. Every commit to the `+main+` branch should follow the Conventional Commits convention. Following are some examples; these can be combined in a single commit message (separated by empty lines), or you can have commit messages describing just a single fix or feature. +=== `main` & `develop` branches + +Versioning is handled automatically by https://github.com/google-github-actions/release-please-action[release-please-action] based on https://www.conventionalcommits.org/[Conventional Commits]. Every commit to the `+main+` or `+develop+` branch should follow the Conventional Commits convention. Following are some examples; these can be combined in a single commit message (separated by empty lines), or you can have commit messages describing just a single fix or feature. .... chore: Won't show up in changelog @@ -23,10 +37,17 @@ feat: Some feature BREAKING-CHANGE: No longer supports xyz .... -See the output of `+git log+` to view some sample commit messages. +See the output of `+git log+` to view some sample commit messages. Note that such commit messages should only describe changes since the last fcli release, not changes within the current development release. For example, suppose you've developed a new feature that hasn't been released yet, and then implement some fix for this unreleased feature, you shouldn't use a `fix:` commit message but instead use a `chore:` or similar message. Otherwise, the changelog would contain entries for both the new feature and fixes to that feature, which doesn't make sense. `+release-please-action+` invoked from the GitHub CI workflow generates pull requests containing updated `+CHANGELOG.md+` and `+version.txt+` files based on these commit messages. Merging the pull request will result in a new release version being published by creating a GitHub release describing the changes. +=== Feature & other branches + +In general, you can use the same commit messages as described above. However, especially when implementing a larger feature that may undergo some changes before being released, it maybe be better to use `proposed-feat:` instead, to avoid having to fix commit messages if the initial feature description no longer matches the feature that's eventually being released. + +As an example, we've had several occasions where we initially wrote a commit message like `feat: Add 'fcli ssc new-entity' commands to ...`, but before release decided to rename this entity or combine with another entity, causing the previous commit message to be outdated as instead we now should have a commit message `feat: Add 'fcli ssc renamed-entity' commands to ...`. + + == Technologies & Frameworks Following is a list of the main frameworks and technologies used by fcli: @@ -46,7 +67,7 @@ This project uses the following frameworks that may require some special setup i * Lombok: Please see https://projectlombok.org/setup/overview for more information on how to add Lombok support to your IDE * Annotation processors: This project requires various annotation processors to be run, like the picocli annotation processor. Please see your IDE documentation on how to enable annotation processing -=== Incremental Compilation +=== Incremental Compilation (Java) Incremental compilation (for example in IDE or when doing a `+gradle build+` without `+clean+`) may leave behind old AOT artifacts causing exceptions when trying to run `+fcli+`. This is especially the case when renaming or moving classes, which may result in exceptions like the below: @@ -59,6 +80,10 @@ Caused by: java.lang.NoClassDefFoundError: com/fortify/cli/rest/unirest/Abstract In this example, the AbstractUnirestRunner class was moved to a different package, but the now obsolete AOT-generated classes were still present. So, if at any time you see unexpected exceptions, please try to do a full clean/rebuild and run `+fcli+` again. +=== Incremental Compilation (Resources) + +Incremental compilation may sometimes result in incorrect resource file handling. For example, action zip files are being updated with updated action YAML files, rather than being fully replaced. This can sometimes cause incorrect behavior, like removed actions still being available, or updated actions not being properly replaced. + === Reflection GraalVM Native Image needs to know ahead-of-time the reflectively accessed program elements; see https://www.graalvm.org/reference-manual/native-image/Reflection/[GraalVM Native Image & Reflection] for details. @@ -105,7 +130,6 @@ User documentation is generated automatically by the `+buildRoot/app/fcli-doc+` Documentation can be generated using the Gradle `distDocs` task, ending up in the `dist` directory of the aforementioned project. The global `dist` task includes the `distDocs` task. - The GitHub Actions workflow defined in `+.github/workflows/ci.yml+` is responsible for publishing the documentation: * The `+build+` job builds the documentation artifacts and archives them as artifacts @@ -298,3 +322,29 @@ Ideally, all commands, options and positional parameters should have a proper de Where applicable, option and positional parameter descriptions should include references to other related fcli commands, in particular when these related commands are in a separate command tree. For example, available attribute names and values that can be specified on the `fcli ssc appversion-attribute set` command can be found through the `fcli ssc attribute-definition *` commands. Being in a separate command tree, this may not be obvious to users and as such should be documented on the `fcli ssc appversion-attribute set` command. Comparing this to the 'application name or id' to be passed to the `fcli ssc app get` command; available application id's can be found through the `fcli ssc app list` command under the same `app` parent command, so this doesn't need to be documented as it should be obvious to users. + +== Fcli action-related development + +=== Implementing fcli built-in actions + +We have fairly extensive user documentation regarding fcli action development, including an action schema for IDE code assistance, and many existing actions that may be used as an example. Virtually all of this documentation also applies to internal actions, except for one exception: built-in actions should always use the link:https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev.json[dev schema]. + +With every GitHub workflow run (for any branch), the dev schema will be automatically updated to reflect any changes in the action model (syntax), allowing IDE code assistance to take these changes into account (you may need to restart your IDE to apply any schema changes). This allows for utilizing any new/updated action syntax while developing built-in actions. + +Upon fcli release, the dev schema referenced from built-in actions will be automatically updated to reflect current action schema release version. + +=== Action schema + +Fcli action schemas are versioned independently from fcli; as not every fcli release will have schema changes, it doesn't make much sense to republish an already existing schema with a new (fcli) version number. Having schema's versioned independently allows for easily identifying when there have been any schema changes, and avoids users having to update the schema version used by their custom actions upon every fcli release. + +The schema is automatically generated from the `...action.model.Action` class structure located in `fcli-common`, and current action schema version is defined in the `fcliActionSchemaVersion` property in `gradle.properties` in the fcli root project. Upon each fcli release, the configured schema version will be published if it hasn't been published yet upon earlier fcli releases. + +If any changes are made to the action model source code, the `fcliActionSchemaVersion` property must be updated accordingly, taking into account proper major, minor, or patch version increase based on the type of changes; see comments in `gradle.properties` for details. If a given `fcliActionSchemaVersion` has been previously released, and any changes in action model are detected, the Gradle `generateActionSchema` task will fail as we won't allow updating any previously released schema. + +If the currently configured `fcliActionSchemaVersion` hasn't been published yet, GitHub workflows will produce a warning stating that a new schema version will be released upon the next fcli release. This is to try to avoid situations like the following: + +1. Someone updates a property description and increases `fcliActionSchemaVersion` to the next patch release to avoid the `generateActionSchema` task from failing. +2. Later on, someone makes a structural change to the action model, but forgets to update the `fcliActionSchemaVersion` property. +3. Upon fcli release, we now release a schema patch version, whereas it should have been a minor or major version change. + +Any suggestions on better ways to avoid such situations are welcome. 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..294f7c1996 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: @@ -57,6 +58,8 @@ Each release comes with a list of assets: found, please try with the Java version as it may be an issue specific to the native binaries. See the link:#_troubleshooting[Troubleshooting] section for details. * `+fcli-thirdparty.zip+`: Third-party licenses and sources for license purposes; usually no need to download * `+LICENSE.TXT+` & `+README.md+`: Some generic information and license for fcli +* `+*.sha256+`: SHA256 checksum for each asset +* `+*.rsa_sha256+`: RSA SHA256 signature for each asset, to be verified using this link:https://raw.githubusercontent.com/fortify/tool-definitions/main/id_rsa.pub[public key] Please note that when publishing a new release, it may take up to 30-60 minutes before release assets are posted. If the latest release doesn’t show any of the assets listed above, please check again in 30-60 minutes. If you encounter a release without these assets after waiting for 60 minutes, please consider submitting an issue on the https://github.com/fortify/fcli/issues[fcli issue tracker]. @@ -64,7 +67,27 @@ Please note that when publishing a new release, it may take up to 30-60 minutes To install fcli, start by visiting the fcli https://github.com/fortify/fcli/releases[Releases] page as described in the previous section, and downloading the `fcli.jar` file or appropriate native binary archive for your platform. Native binary archives will need to be extracted to a temporary directory, the `fcli.jar` file can be used as-is. -Once downloaded and extracted (if applicable), you can either perform a manual or managed installation. Managed installation is recommended, but only available on internet-connected systems. Please see the sections below for more information. +As a best practice, you should verify the integrity of the downloaded file. Release assets include `+*.sha256+` and `+*.rsa_sha256+` for this purpose, with the latter being preferred for better security. For example, after downloading both `+fcli-linux.tgz+` and `+fcli-linux.tgz.rsa_sha256+`, you can verify integrity by running the following OpenSSL command: + +---- + openssl dgst -sha256 -verify <(echo "-----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArij9U9yJVNc53oEMFWYp + NrXUG1UoRZseDh/p34q1uywD70RGKKWZvXIcUAZZwbZtCu4i0UzsrKRJeUwqanbc + woJvYanp6lc3DccXUN1w1Y0WOHOaBxiiK3B1TtEIH1cK/X+ZzazPG5nX7TSGh8Tp + /uxQzUFli2mDVLqaP62/fB9uJ2joX9Gtw8sZfuPGNMRoc8IdhjagbFkhFT7WCZnk + FH/4Co007lmXLAe12lQQqR/pOTeHJv1sfda1xaHtj4/Tcrq04Kx0ZmGAd5D9lA92 + 8pdBbzoe/mI5/Sk+nIY3AHkLXB9YAaKJf//Wb1yiP1/hchtVkfXyIaGM+cVyn7AN + VQIDAQAB + -----END PUBLIC KEY-----") -signature "fcli-linux.tgz.rsa_sha256" "fcli-linux.tgz" +---- + +If your shell doesn't support `+<(...)+` syntax, you'll need to save the link:https://raw.githubusercontent.com/fortify/tool-definitions/main/id_rsa.pub[public key] to a file and pass the file name to the `-verify` command line option, for example: + +---- + openssl dgst -sha256 -verify pubkey.pem -signature "fcli-linux.tgz.rsa_sha256" "fcli-linux.tgz" +---- + +Once downloaded, verified and extracted (if applicable), you can either perform a manual or managed installation. Managed installation is recommended, but only available on internet-connected systems. Please see the sections below for more information. === Managed Installation @@ -445,6 +468,117 @@ 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 + +Fcli actions are a powerful feature that allows for rich integration and automation by running a customizable set of instructions defined in YAML files. These instructions allow for processing data collected from Fortify or third-party products, updating data in those products, and writing output to files or console. + +=== Built-in Actions + +Fci comes with various built-in actions, currently focused on providing the following functionality: + +* Vulnerability export to various third-party formats like SARIF or GitHub, GitLab, BitBucket, and SonarQube reports, as a replacement for link:https://github.com/fortify/FortifyVulnerabilityExporter[FortifyVulnerabilityExporter]. +* Generating GitHub Pull Request comments, listing (re-)introduced and removed vulnerabilities. +* Generating application version/release summaries in Markdown format, for example for use as pipeline summaries. +* Evaluating security policy criteria, for example allowing to break a build if one or more checks are failing. + +Future fcli versions may introduce new built-in actions, enhance existing built-in actions, or provide enhanced action syntax, for example to allow for running pipeline-style actions that run a set of commands to package source code, submit a scan request, wait for scan completion, and perform post-scan activities like data export or policy checks. + +Action support is centralized into the `action` entity of the various product modules. For now, fcli supports `fcli fod action *` and `fcli ssc action *` commands. Based on user feedback, we may consider adding action support on other modules like `sc-sast` or `sc-dast` as well. The following commands may be used to manage and run built-in actions: + +* `fcli * action list`: List available built-in actions (and imported custom actions, see next section). +* `fcli * action help `: Display action usage information like description and list of action-specific command-line options. +* `fcli * action get `: Display action YAML contents. +* `fcli * action run `: Run a given action. + +=== Custom Actions + +Apart from built-in actions, users can also develop and run custom actions, which could be customized versions of built-in actions or completely new actions. Note that the ability to run custom actions is currently considered PREVIEW functionality; as fcli actions are a new functionality, we are collecting user feedback regarding action syntax, which could potentially result in breaking action syntax changes across minor fcli releases (although we'll try to avoid this as much as possible). + +As such, custom actions that run fine on the current fcli version may fail to run on any other fcli version. Fcli does perform action version checks based on action schema version (see below), but supported schema versions may change between minor or patch releases of fcli. Once we are comfortable with moving custom actions out of PREVIEW status, breaking schema/action syntax changes will be allowed only on major fcli releases, for example when moving from fcli 2.x to fcli 3.x. + +Custom actions can be loaded from various sources, like a local or remote YAML file, optionally embedded in a zip-file that contains multiple actions. Each of the fcli commands listed in the previous section also support custom actions: + +* `fcli * action list --from-zip `: List available actions from the given zip-file. +* `fcli * action help|get|run `: Load the action from the given file or URL. +* `fcli * action help|get|run --from-zip `: Load the given action from the given zip-file. + +To allow for easy access, custom actions can also be imported into fcli using the `fcli * action import` command, which allows for importing either a single action YAML file or all action YAML files from a local or remote zip-file. Once imported, these actions can be accessed in the same way as built-in actions. Note that imported custom actions will override built-in actions if they have the same name. You can use the `fcli * action reset` command to remove all previously imported custom actions. + +=== Custom Action Development + +There are several resources available to help you developing custom actions. To start with, you can use the `fcli * action get ` command to view the contents of any built-in action as a basis for developing your own custom actions. Fcli provides a built-in `+__sample__+` action that explains various concepts and lists many of the supported action YAML elements together with a description. To view the sample action, use the `+fcli ssc action get __sample__+` or `+fcli fod action get __sample__+` command. + +Fcli also provides an action schema, which allows YAML editors and IDEs to provide code completion, documentation and validation for fcli action YAML documents. You may need to install a plugin to enable proper YAML editing and schema support. Following are some commonly used IDE plugins that are known to work with one of the schema associations listed below: + +* Visual Studio Code: link:https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml[Red Hat YAML plugin] +* Eclipse: link:https://marketplace.eclipse.org/content/wild-web-developer-html-css-javascript-typescript-nodejs-angular-json-yaml-kubernetes-xml[Wild Web Developer] + +For optimal compatibility with various IDEs and YAML editors, fcli allows the schema to be declared in two ways: + +* YAML comment, supported by IDEs like Visual Studio Code, Eclipse and IntelliJ: ++ +[subs="attributes"] +---- +# yaml-language-server: $schema={actionSchemaUrl} +---- +* YAML property, supported by Eclipse and some other YAML editors: ++ +[subs="attributes"] +---- +$schema: {actionSchemaUrl} +---- + +Fcli requires at least one of these statements to be provided in action YAML files. You may also provide both, in which case the schema URL must be identical in both statements. Once you've added one of these statements to your action YAML file, your IDE or YAML editor may automatically provide support for editing and validating action YAML files. If not, try closing and re-opening the YAML file, or consult the documentation of your IDE or YAML editor. + +As shown in the sample schema statements above, the current fcli version {docversion} supports schema version link:{actionSchemaUrl}[{actionSchemaVersion}]. Other fcli versions may support different schema versions. In general, fcli also provides backward compatibility for earlier schema versions with the same schema major version range, and forward compatibility for later schema patch versions within the same schema minor version range. + +=== Security Considerations + +As actions can potentially perform dangerous operations, like sending confidential data to third-party systems, or updating or deleting data in Fortify or third-party systems, you should only run trusted actions. If you wish to run any actions provided by a third party, you could potentially review action contents, and potentially in the future we may provide functionality for performing a security analysis on action contents (either as a new fcli command or through Fortify rules). + +However, this won't be sufficient for actions that are dynamically loaded from a remote location, as action contents could change at any time. For example, a legitimate action can easily be replaced by some malicious action. + +For this reason, fcli by default requires custom actions to be signed. Organizations, teams or individuals can sign actions with their own private key using the `fcli * action sign` command, the corresponding public key can be imported into fcli using the `fcli config public-key import` command. Most `action` commands also allow for explicitly passing a public key through the `--pubkey` option. + +Public keys can be loaded from a local file, URL, string or environment variable, see command help or next section for details. Obviously, you should only use trusted public keys; loading a public key from a third-party URL may be dangerous for the same reasons as explained above. + +=== Actions in CI/CD pipelines + +Fcli is commonly used in CI/CD pipelines to perform all sorts of Fortify-related operations, and fcli actions allow for even more advanced use cases. This section provides some hints as to how to integrate actions into CI/CD pipelines, considering deployment and security aspects. + +*Publishing* + +Fcli actions can be loaded from any URL, but for CI/CD integration you could consider hosting fcli actions in a shared source code repository like `https://some-scm.our.org/repositories/shared-ci-tools/fcli/actions`. This allows for easy sharing and maintainance of fcli actions that are used across many CI/CD pipelines. This even allows for automatically signing those action using CI/CD pipelines on this shared repository. + +*Public & private key management* + +Of course, those actions should be signed using an organization-specific private key. For example, this could be either an existing private key that's used throughout the organization, or a private key that's managed by the CI/CD team. There are several ways to pass the corresponding public key to fcli from within CI/CD pipelines: + +* Explicitly run `fcli config public-key import` command from each pipeline before running any actions. +* Explicitly pass the `--pubkey` option on any `fcli * action run` commands. +* Set the `FCLI_DEFAULT_PUBKEY` environment variable to configure a default value for the `--pubkey` option. + +With each of these approaches, the public key can be be loaded from: + +* Local file, for example public key stored in current source code repository. +* URL, for example pointing to public key stored in same shared source code repository as the actions themselves. +* Environment variable, for example defined as system environment variable on self-hosted CI/CD nodes, or configured through (global) CI/CD secrets/variables. +* Plain string, for example having the environment variable `FCLI_DEFAULT_PUBKEY` set to `string:`. You can use CI/CD secrets/variables to set the `FCLI_DEFAULT_PUBKEY` to a value like this. For testing, you can use a `bash` command like the following: `export FCLI_DEFAULT_PUBKEY=string:$(cat my-public.key)`. + +*Design considerations* + +Given that actions can define and process arbitrary parameters, it may be tempting to implement generic, highly configurable actions. For example, you could implement a highly configurable action for checking all kinds of security policy criteria, with the actual criteria to be checked being passed as action parameters. + +However, from a CI/CD perspective, this means that every pipeline must properly set these action parameters. Pipeline authors may copy existing pipelines or use pipeline templates to have these action parameters set to predefined values, but what if those parameters need to updated globally? Potentially, this means that you'd need to update thousands of pipelines to adjust action parameters. + +This is exactly the reason why we don't provide a highly configurable `check-policy` action, but instead just provide an example that can be customized according to organization requirements. Instead of passing fine-grained pass/fail criteria as action parameters, those criteria are hard-coded into the action. If there is a need to apply different criteria for different types of applications, for example based on business risk, multiple approaches are possible: + +* Use SSC/FoD application version/release attributes like `Business Risk` or other characteristics to identify what criteria to apply. This is the preferred approach, to allow the criteria to be applied to automatically match the centrally maintained attributes/characteristics. +* Have the action take one or more parameters that identify what criteria to apply. This could be a policy name, or a functional identifier like `--business-risk`. +* Publish separate actions that implement different policy criteria, like `check-high-risk-policy` and `check-low-risk-policy`. + +The examples above or based on actions that perform policy checks, but the same principles and considerations (may) apply to other types of 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-doc/src/main/java/com/fortify/cli/common/action/schema/generator/GenerateActionSchema.java b/fcli-other/fcli-doc/src/main/java/com/fortify/cli/common/action/schema/generator/GenerateActionSchema.java new file mode 100644 index 0000000000..73d8d5fa97 --- /dev/null +++ b/fcli-other/fcli-doc/src/main/java/com/fortify/cli/common/action/schema/generator/GenerateActionSchema.java @@ -0,0 +1,140 @@ +/** + * 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.schema.generator; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.action.helper.ActionSchemaHelper; +import com.fortify.cli.common.action.model.Action; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.spring.expression.wrapper.TemplateExpression; +import com.github.victools.jsonschema.generator.CustomDefinition; +import com.github.victools.jsonschema.generator.Option; +import com.github.victools.jsonschema.generator.OptionPreset; +import com.github.victools.jsonschema.generator.SchemaGenerator; +import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; +import com.github.victools.jsonschema.generator.SchemaKeyword; +import com.github.victools.jsonschema.generator.SchemaVersion; +import com.github.victools.jsonschema.module.jackson.JacksonModule; +import com.github.victools.jsonschema.module.jackson.JacksonOption; + +public class GenerateActionSchema { + private static final String DEV_VERSION = "dev"; + public static void main(String[] args) throws Exception { + if ( args.length!=3 ) { throw new IllegalArgumentException("This command must be run as GenerateActionSchema "); } + var isDevelopmentRelease = args[0]; + var actionSchemaVersion = args[1]; + var outputPath = Path.of(args[2]); + + var newSchema = generateSchema(); + var existingSchema = loadExistingSchema(actionSchemaVersion); + checkSchemaCompatibility(actionSchemaVersion, existingSchema, newSchema); + writeGitHubWarning(actionSchemaVersion, existingSchema); + + // If this is an fcli development release, we output the schema as a development release. + // Note that the same output file name will be used for any branch. + var outputVersion = isDevelopmentRelease.equals("true") ? DEV_VERSION : actionSchemaVersion; + if ( existingSchema!=null && !DEV_VERSION.equals(outputVersion) ) { + System.out.println("Fortify CLI action schema not being generated as "+outputVersion+" schema already exists"); + } else { + writeSchema(outputPath, newSchema, outputVersion); + } + } + + private static void writeSchema(Path outputPath, JsonNode newSchema, String outputVersion) throws IOException { + Files.createDirectories(outputPath); + var outputFile = outputPath.resolve(String.format("fcli-action-schema-%s.json", outputVersion)); + Files.writeString(outputFile, newSchema.toPrettyString(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + System.out.println("Fortify CLI action schema written to "+outputFile.toString()); + } + + private static void writeGitHubWarning(String actionSchemaVersion, JsonNode existingSchema) { + if ( existingSchema==null ) { + System.out.println("::warning ::New fcli action schema version "+actionSchemaVersion+ + " will be published upon fcli release. Please ensure that this version number" + + " properly represents schema changes (patch increase for non-structural changes" + + " like description changes, minor increase for backward-compatible structural" + + " changes like new optional properties, major increase for non-backward-compatible" + + " structural changes like new required properties or removed properties)."); + } + } + + private static final void checkSchemaCompatibility(String actionSchemaVersion, JsonNode existingSchema, JsonNode newSchema) throws Exception { + if ( existingSchema!=null && !existingSchema.equals(newSchema) ) { + throw new IllegalStateException(String.format(""" + \n\tSchema generated from current source code is different from existing schema + \tversion %s. If this is incorrect (action model hasn't changed), please update + \tthe schema compatibility check in .../fcli-doc/src/.../GenerateActionSchema.java. + \tIf the schema has indeed changed, please update the schema version number in + \tgradle.properties in the root fcli project. + """, actionSchemaVersion)); + } + } + + private static final JsonNode loadExistingSchema(String actionSchemaVersion) throws IOException { + try { + return JsonHelper.getObjectMapper().readTree(new URL(ActionSchemaHelper.toURI(actionSchemaVersion))); + } catch ( FileNotFoundException fnfe ) { + return null; // Schema doesn't exist yet + } catch ( IOException e ) { + throw e; + } + } + + private static final JsonNode generateSchema() { + var config = createGeneratorConfig(); + var generator = new SchemaGenerator(config); + return generator.generateSchema(Action.class); + } + + private static final SchemaGeneratorConfig createGeneratorConfig() { + SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON); + JacksonModule jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY); + configBuilder.forTypesInGeneral().withCustomDefinitionProvider((type, context) -> { + if (type.getErasedType() == TemplateExpression.class) { + var custom = context.getGeneratorConfig().createObjectNode(); + custom.put(context.getKeyword(SchemaKeyword.TAG_FORMAT), "spelTemplateExpression") + .putArray(context.getKeyword(SchemaKeyword.TAG_TYPE)) + .add(context.getKeyword(SchemaKeyword.TAG_TYPE_BOOLEAN)) + .add(context.getKeyword(SchemaKeyword.TAG_TYPE_INTEGER)) + .add(context.getKeyword(SchemaKeyword.TAG_TYPE_STRING)) + .add(context.getKeyword(SchemaKeyword.TAG_TYPE_NUMBER)); + return new CustomDefinition(custom, true); + } else if (type.getErasedType() == JsonNode.class ) { + var custom = context.getGeneratorConfig().createObjectNode(); + custom.put(context.getKeyword(SchemaKeyword.TAG_ADDITIONAL_PROPERTIES), true) + .putArray(context.getKeyword(SchemaKeyword.TAG_TYPE)) + .add(context.getKeyword(SchemaKeyword.TAG_TYPE_OBJECT)) + .add(context.getKeyword(SchemaKeyword.TAG_TYPE_STRING)); + return new CustomDefinition(custom, true); + } else { + return null; + } + }); + SchemaGeneratorConfig config = configBuilder + .with(jacksonModule) + .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) + .with(Option.FLATTENED_ENUMS) + .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT) + .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) + .build(); + return config; + } +} diff --git a/fcli-other/fcli-functional-test/build.gradle b/fcli-other/fcli-functional-test/build.gradle index f453251272..15eb6c79d5 100644 --- a/fcli-other/fcli-functional-test/build.gradle +++ b/fcli-other/fcli-functional-test/build.gradle @@ -5,11 +5,11 @@ testing { ftest(JvmTestSuite) { useJUnitJupiter() dependencies { - implementation platform('org.apache.groovy:groovy-bom:4.0.13') + implementation platform('org.apache.groovy:groovy-bom:4.0.20') implementation 'org.apache.groovy:groovy' implementation platform("org.spockframework:spock-bom:2.3-groovy-4.0") implementation "org.spockframework:spock-core" - implementation 'org.junit.platform:junit-platform-launcher:1.9.3' + implementation 'org.junit.platform:junit-platform-launcher:1.10.2' if ( !project.hasProperty('ftest.fcli') || project.property('ftest.fcli')=='build' ) { implementation project("${fcliAppRef}") } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/Fcli.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/Fcli.groovy index 0d4db840fe..3d198a68be 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/Fcli.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/Fcli.groovy @@ -1,12 +1,11 @@ package com.fortify.cli.ftest._common; -import java.nio.file.Files import java.nio.file.Path import org.spockframework.runtime.IStandardStreamsListener import org.spockframework.runtime.StandardStreamsCapturer -import com.fortify.cli.ftest._common.util.TempDirHelper +import com.fortify.cli.ftest._common.util.WorkDirHelper import groovy.transform.CompileStatic import groovy.transform.Immutable @@ -17,11 +16,10 @@ public class Fcli { private static IRunner runner private static Set stringsToMask = [] - static void initialize() { + static void initialize(Path fortifyDataDir) { System.setProperty("picocli.ansi", "false") - fcliDataDir = TempDirHelper.create() - System.setProperty("fcli.env.FORTIFY_DATA_DIR", fcliDataDir.toString()) - println("Using fcli data directory "+fcliDataDir) + System.setProperty("fcli.env.FORTIFY_DATA_DIR", fortifyDataDir.toString()) + println("Using fcli data directory "+fortifyDataDir) runner = createRunner() } @@ -105,7 +103,6 @@ public class Fcli { if ( runner ) { runner.close() } - TempDirHelper.rm(fcliDataDir) } private static IRunner createRunner() { diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/AbstractTempDirExtension.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/AbstractTempDirExtension.groovy deleted file mode 100644 index 196b102510..0000000000 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/AbstractTempDirExtension.groovy +++ /dev/null @@ -1,70 +0,0 @@ -package com.fortify.cli.ftest._common.extension; - -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption - -import org.spockframework.runtime.extension.IGlobalExtension -import org.spockframework.runtime.model.FieldInfo -import org.spockframework.runtime.model.SpecInfo - -import com.fortify.cli.ftest._common.spec.TestResource -import com.fortify.cli.ftest._common.util.TempDirHelper - -import groovy.transform.CompileStatic - -@CompileStatic -abstract class AbstractTempDirExtension implements IGlobalExtension { - private Path tempDir - - public final Path getTempDir() { return tempDir } - - @Override - public final void start() { - tempDir = TempDirHelper.create() - afterCreate() - } - - @Override - public final void stop() { - if ( !System.getProperty("ft.keep-temp-dirs") ) { - TempDirHelper.rm(tempDir) - } - } - - public void visitSpec(SpecInfo spec) { - // Register an interceptor for setting up any fields for which we can determine a path - spec.allFields.each { FieldInfo field -> - def path = getPathForField(field) - if ( path ) { - if ( field.shared ) { - spec.addSharedInitializerInterceptor { - setPathOnField(field, it.instance, path) - it.proceed() - } - } else { - spec.addSetupInterceptor { - setPathOnField(field, it.instance, path) - it.proceed() - } - } - } - } - } - - protected void afterCreate() {} - protected abstract Path getPathForField(FieldInfo field); - - private final void setPathOnField(FieldInfo field, Object instance, Path path) { - def type = field.reflection.type - if ( type.isAssignableFrom(Path) ) { - instance.metaClass.setProperty(instance, field.reflection.name, path) - } else if ( type.isAssignableFrom(File) ) { - instance.metaClass.setProperty(instance, field.reflection.name, path.toFile()) - } else if ( type.isAssignableFrom(String) ) { - instance.metaClass.setProperty(instance, field.reflection.name, path.toFile().absolutePath) - } else { - throw new RuntimeException("Only Path, File or String fields are supported, not "+type) - } - } -} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/DisplayNameExtension.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/DisplayNameExtension.groovy deleted file mode 100644 index 0f07b9bdd7..0000000000 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/DisplayNameExtension.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package com.fortify.cli.ftest._common.extension; - -import org.spockframework.runtime.extension.IGlobalExtension -import org.spockframework.runtime.model.SpecInfo - -import com.fortify.cli.ftest._common.spec.Prefix - -import groovy.transform.CompileStatic - -@CompileStatic -class DisplayNameExtension implements IGlobalExtension { - @Override - void visitSpec(SpecInfo spec) { - def prefixAnnotation = spec.getAnnotation(Prefix.class) - if ( prefixAnnotation ) { - spec.allFeatures.each { - it.name = prefixAnnotation.value()+"."+it.name - } - spec.name = prefixAnnotation.value()+" ("+spec.name+")" - } - } -} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/FcliGlobalExtension.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/FcliGlobalExtension.groovy new file mode 100644 index 0000000000..7686e3c7f9 --- /dev/null +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/FcliGlobalExtension.groovy @@ -0,0 +1,179 @@ +package com.fortify.cli.ftest._common.extension; + +import java.nio.file.Path + +import org.spockframework.runtime.extension.IGlobalExtension +import org.spockframework.runtime.model.FieldInfo +import org.spockframework.runtime.model.SpecElementInfo +import org.spockframework.runtime.model.SpecInfo + +import com.fortify.cli.ftest._common.Fcli +import com.fortify.cli.ftest._common.Input +import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType +import com.fortify.cli.ftest._common.spec.Global +import com.fortify.cli.ftest._common.spec.Global.IGlobalValueSupplier +import com.fortify.cli.ftest._common.spec.Prefix +import com.fortify.cli.ftest._common.spec.TempDir +import com.fortify.cli.ftest._common.spec.TempFile +import com.fortify.cli.ftest._common.spec.TestResource +import com.fortify.cli.ftest._common.util.WorkDirHelper + +import groovy.transform.CompileStatic + +@CompileStatic +public class FcliGlobalExtension implements IGlobalExtension { + private WorkDirHelper workDirHelper; + private Map, IGlobalValueSupplier> globalValueSuppliers = new HashMap<>(); + + @Override + public void start() { + workDirHelper = new WorkDirHelper(); + Fcli.initialize(workDirHelper.getFortifyDataDir()); + } + + @Override + public void visitSpec(SpecInfo spec) { + updateSpecName(spec); + skipFeatures(spec); + initFcliSessions(spec); + if ( !spec.skipped ) { + spec.allFields.each { FieldInfo field -> initializeField(spec, field) } + } + } + + @Override + public void stop() { + globalValueSuppliers.values().each { s -> s.close() } + FcliSessionType.logoutAll() + Fcli.close() + workDirHelper.close(); + } + + private void updateSpecName(SpecInfo spec) { + def prefixAnnotation = spec.getAnnotation(Prefix.class) + if ( prefixAnnotation ) { + spec.allFeatures.each { + it.name = prefixAnnotation.value()+"."+it.name + } + spec.name = prefixAnnotation.value()+" ("+spec.name+")" + } + } + + private void skipFeatures(SpecInfo spec) { + // Exclude any features not matching any of the feature names listed in + // the fcli.run property + // TODO Add support for skipping features based on tag include/exclude + // expressions + def run = Input.TestsToRun.get()?.split(",") + if (run) { + spec.allFeatures.each({ feature -> + if ( !run.any {feature.name.startsWith(it) && !feature.skipped } ) { + feature.skip "Not included in "+Input.TestsToRun.propertyName()+" property" + } + }) + skipSpec(spec); // Skip spec if all features skipped + } + } + + private void skipSpec(SpecInfo spec) { + if ( !spec.allFeatures.findAll({ f->!f.skipped }) ) { + spec.skip "All features skipped" + } + } + + private void initFcliSessions(SpecInfo spec) { + if ( !spec.skipped ) { + initFcliSession(spec, spec.getAnnotation(FcliSession.class)); + spec.allFeatures.each({ feature -> + if ( !feature.skipped ) { + initFcliSession(feature, feature.featureMethod.getAnnotation(FcliSession.class)); + } + }) + skipSpec(spec) + } + } + + private void initFcliSession(SpecElementInfo elt, FcliSession annotation) { + if ( annotation ) { + annotation.value().each { + def handler = it.handler + if ( !elt.excluded && !elt.skipped ) { + if (handler.isEnabled() ) { + if ( !handler.login() ) { + elt.skip "Skipped due to "+handler.friendlyName()+" login failure" + } + } else { + elt.skip "No "+handler.friendlyName()+ " session available"; + } + } + } + } + } + + private void initializeField(SpecInfo spec, FieldInfo field) { + addFieldTempDirInterceptor(spec, field); + addFieldTempFileInterceptor(spec, field); + addFieldResourceFileInterceptor(spec, field); + addFieldGlobalInterceptor(spec, field); + } + + private final void addFieldTempDirInterceptor(SpecInfo spec, FieldInfo field) { + def tempDirAnnotation = field.getAnnotation(TempDir.class) + if ( tempDirAnnotation!=null ) { + addFieldInterceptor(spec, field, convertPath(field, workDirHelper.getTempDir(tempDirAnnotation.value()))) + } + } + + private final void addFieldTempFileInterceptor(SpecInfo spec, FieldInfo field) { + def tempFileAnnotation = field.getAnnotation(TempFile.class) + if ( tempFileAnnotation!=null ) { + addFieldInterceptor(spec, field, convertPath(field, workDirHelper.getTempFile(tempFileAnnotation.value()))) + } + } + + private final void addFieldResourceFileInterceptor(SpecInfo spec, FieldInfo field) { + def resourceFileAnnotation = field.getAnnotation(TestResource.class) + if ( resourceFileAnnotation!=null ) { + addFieldInterceptor(spec, field, convertPath(field, workDirHelper.getResource(resourceFileAnnotation.value()))) + } + } + + private final void addFieldGlobalInterceptor(SpecInfo spec, FieldInfo field) { + def globalAnnotation = field.getAnnotation(Global.class) + if ( globalAnnotation!=null ) { + def clazz = globalAnnotation.value(); + def valueSupplier = globalValueSuppliers.computeIfAbsent(clazz, {c->c.newInstance()}); + addFieldInterceptor(spec, field, valueSupplier.getValue(workDirHelper)); + } + } + + private final void addFieldInterceptor(SpecInfo spec, FieldInfo field, Object value) { + if ( value ) { + if ( field.shared ) { + spec.addSharedInitializerInterceptor { + it.instance.metaClass.setProperty(it.instance, field.reflection.name, value) + it.proceed() + } + } else { + spec.addSetupInterceptor { + it.instance.metaClass.setProperty(it.instance, field.reflection.name, value) + it.proceed() + } + } + } + } + + private final Object convertPath(FieldInfo field, Path path) { + def type = field.reflection.type + if ( type.isAssignableFrom(Path) ) { + return path; + } else if ( type.isAssignableFrom(File) ) { + return path.toFile(); + } else if ( type.isAssignableFrom(String) ) { + return path.toFile().absolutePath; + } else { + throw new RuntimeException("Only Path, File or String fields are supported, not "+type) + } + } +} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/FcliInitializerExtension.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/FcliInitializerExtension.groovy deleted file mode 100644 index c30c0d0156..0000000000 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/FcliInitializerExtension.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package com.fortify.cli.ftest._common.extension; - -import org.spockframework.runtime.extension.IGlobalExtension - -import com.fortify.cli.ftest._common.Fcli -import com.fortify.cli.ftest._common.spec.FcliSessionType - -import groovy.transform.CompileStatic - -@CompileStatic -class FcliInitializerExtension implements IGlobalExtension { - @Override - public void start() { - Fcli.initialize(); - } - - @Override - public void stop() { - FcliSessionType.logoutAll() - Fcli.close() - } -} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/FcliSessionExtension.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/FcliSessionExtension.groovy deleted file mode 100644 index 2c566462c7..0000000000 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/FcliSessionExtension.groovy +++ /dev/null @@ -1,35 +0,0 @@ -package com.fortify.cli.ftest._common.extension; - -import org.spockframework.runtime.extension.IAnnotationDrivenExtension -import org.spockframework.runtime.model.FeatureInfo -import org.spockframework.runtime.model.SpecElementInfo -import org.spockframework.runtime.model.SpecInfo - -import com.fortify.cli.ftest._common.spec.FcliSession - -public class FcliSessionExtension implements IAnnotationDrivenExtension { - @Override - public void visitSpecAnnotation(FcliSession annotation, SpecInfo spec) { - visit(annotation, spec) - } - - @Override - public void visitFeatureAnnotation(FcliSession annotation, FeatureInfo feature) { - visit(annotation, feature) - } - - private void visit(FcliSession annotation, SpecElementInfo elt) { - annotation.value().each { - def handler = it.handler - if ( !elt.excluded && !elt.skipped ) { - if (handler.isEnabled() ) { - if ( !handler.login() ) { - elt.skip "Skipped due to "+handler.friendlyName()+" login failure" - } - } else { - elt.skip "No "+handler.friendlyName()+ " session available"; - } - } - } - } -} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/SkipFeaturesExtension.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/SkipFeaturesExtension.groovy deleted file mode 100644 index 010c2931c7..0000000000 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/SkipFeaturesExtension.groovy +++ /dev/null @@ -1,27 +0,0 @@ -package com.fortify.cli.ftest._common.extension; - -import org.spockframework.runtime.extension.IGlobalExtension -import org.spockframework.runtime.model.SpecInfo - -import com.fortify.cli.ftest._common.Input - -import groovy.transform.CompileStatic - -@CompileStatic -class SkipFeaturesExtension implements IGlobalExtension { - @Override - void visitSpec(SpecInfo spec) { - // Exclude any features not matching any of the feature names listed in - // the fcli.run property - // TODO Add support for skipping features based on tag include/exclude - // expressions - def run = Input.TestsToRun.get()?.split(",") - if (run) { - spec.allFeatures.each({ feature -> - if ( !run.any {feature.name.startsWith(it) && !feature.skipped } ) { - feature.skip "Not included in "+Input.TestsToRun.propertyName()+" property" - } - }) - } - } -} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/TempPathExtension.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/TempPathExtension.groovy deleted file mode 100644 index 731f81ed88..0000000000 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/TempPathExtension.groovy +++ /dev/null @@ -1,35 +0,0 @@ -package com.fortify.cli.ftest._common.extension; - -import java.nio.file.Path - -import org.spockframework.runtime.model.FieldInfo - -import com.fortify.cli.ftest._common.spec.TempDir -import com.fortify.cli.ftest._common.spec.TempFile - -import groovy.transform.CompileStatic - -@CompileStatic -class TempPathExtension extends AbstractTempDirExtension { - @Override - protected void afterCreate() { - println "Using temporary working directory $tempDir" - } - - @Override - protected Path getPathForField(FieldInfo field) { - Path result = null - def dirAnnotation = field.getAnnotation(TempDir.class) - if ( dirAnnotation!=null ) { - result = tempDir.resolve(dirAnnotation.value()) - result.toFile().mkdirs() // Make sure directory exists - } else { - def fileAnnotation = field.getAnnotation(TempFile.class) - if ( fileAnnotation!=null ) { - result = tempDir.resolve(fileAnnotation.value()) - result.parent.toFile().mkdirs() // Make sure parent directory exists - } - } - return result - } -} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/TestResourceExtension.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/TestResourceExtension.groovy deleted file mode 100644 index e83f3b9c5e..0000000000 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/extension/TestResourceExtension.groovy +++ /dev/null @@ -1,50 +0,0 @@ -package com.fortify.cli.ftest._common.extension; - -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption - -import org.spockframework.runtime.extension.IGlobalExtension -import org.spockframework.runtime.model.FieldInfo -import org.spockframework.runtime.model.SpecInfo - -import com.fortify.cli.ftest._common.spec.TestResource -import com.fortify.cli.ftest._common.util.TempDirHelper - -import groovy.transform.CompileStatic - -@CompileStatic -class TestResourceExtension extends AbstractTempDirExtension { - @Override - protected void afterCreate() { - println "Using test resources directory $tempDir" - } - - @Override - protected Path getPathForField(FieldInfo field) { - def annotation = field.getAnnotation(TestResource.class) - return annotation==null ? null : extractResource(annotation.value()) - } - - private Path extractResource(String resourceFile) { - getResource(resourceFile).withCloseable { - Path outputFilePath = tempDir.resolve(resourceFile) - outputFilePath.parent.toFile().mkdirs() - Files.copy(it, outputFilePath, StandardCopyOption.REPLACE_EXISTING) - return outputFilePath - } - } - - private InputStream getResource(String resourceFile) { - def cl = TestResourceExtension.class.classLoader - def stream = cl.getResourceAsStream(resourceFile) - if ( stream==null ) { - stream = cl.getResourceAsStream(resourceFile+"-no-shadow") - } - if ( stream==null ) { - throw new IllegalStateException("${resourceFile} (or ${resourceFile}-no-shadow) referenced in @TestResource not found") - } - return stream - } - -} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/FcliSession.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/FcliSession.groovy index e86556d53f..92dcba8006 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/FcliSession.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/FcliSession.groovy @@ -7,13 +7,234 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME import java.lang.annotation.Retention import java.lang.annotation.Target -import org.spockframework.runtime.extension.ExtensionAnnotation - -import com.fortify.cli.ftest._common.extension.FcliSessionExtension +import com.fortify.cli.ftest._common.Fcli +import com.fortify.cli.ftest._common.Input @Target([METHOD, TYPE]) @Retention(RUNTIME) -@ExtensionAnnotation(FcliSessionExtension.class) @interface FcliSession { FcliSessionType[] value() + + public static enum FcliSessionType { + SSC(new SSCSessionHandler()), + FOD(new FoDSessionHandler()), + SCSAST(new SCSastSessionHandler()), + SCDAST(new SCDastSessionHandler()) + + final ISessionHandler handler + + private FcliSessionType(ISessionHandler handler) { + this.handler = handler + } + + static void logoutAll() { + FcliSessionType.values().each { it.handler.logout() } + } + + public interface ISessionHandler { + String friendlyName(); + boolean isEnabled(); + boolean login(); + void logout(); + List getMaskedProperties(); + } + + private static abstract class AbstractSessionHandler implements ISessionHandler { + def STD_LOGIN_ARGS = [module(), "session","login"] + def STD_LOGOUT_ARGS = [module(), "session","logout"] + private boolean loggedIn = false; + private boolean failed = false; + + @Override + public final boolean login() { + if ( !loggedIn && !failed ) { + println("Logging in to "+friendlyName()) + try { + def loginCredentialOptions = loginCredentialOptions() + if ( loginCredentialOptions==null || loginCredentialOptions.size()==0 ) { + throw new IllegalArgumentException("No or incomplete "+friendlyName()+" credentials provided, tests will be skipped") + } + def valuesToMask = values(maskedProperties) + Fcli.stringsToMask += valuesToMask + Fcli.run( + STD_LOGIN_ARGS+loginOptions()+loginCredentialOptions, + {it.expectSuccess(true, "Error logging in to "+friendlyName()+", tests will be skipped")} + ) + + loggedIn = true + } catch ( Exception e ) { + e.printStackTrace() + failed = true + } + } + return !failed; + } + + @Override + public synchronized final void logout() { + if ( loggedIn ) { + Fcli.stringsToMask += values(maskedProperties) + def result = Fcli.run( + STD_LOGOUT_ARGS+logoutOptions(), + { + if ( !it.success ) { + err.println("Error logging out from "+friendlyName()+"\n"+it.stderr.join("\n ")) + } + }) + loggedIn = false + } + } + + abstract String module() + abstract List loginOptions() + abstract List loginCredentialOptions() + abstract List logoutOptions() + + String basePropertyName() { + Input.addPropertyPrefix(module()) + } + String get(String subPropertyName) { + System.properties[basePropertyName()+"."+subPropertyName] + } + boolean has(String subPropertyName) { + get(subPropertyName) + } + List option(String optName) { + has(optName) ? ["--"+optName, get(optName)] : [] + } + List options(String ... optNames) { + return optNames.every { has(it) } + ? optNames.stream().map(this.&option).collect().flatten() + : [] + } + List values(List optNames) { + return optNames.stream().map(this.&get).filter({it!=null}).collect().flatten(); + } + } + + private static class SSCSessionHandler extends AbstractSessionHandler { + @Override public String friendlyName() { "SSC" } + @Override public String module() { "ssc" } + + @Override + public boolean isEnabled() { + has("url") + } + + @Override + public List loginOptions() { + option("url") + } + + @Override + public List loginCredentialOptions() { + options("user", "password")+options("token")+options("ci-token") + } + + @Override + public List logoutOptions() { + def result = options("user", "password") + return result.size()==0 ? ["--no-revoke-token"] : result + } + + @Override + public List getMaskedProperties() { + ["url", "user", "password", "token", "ci-token"] + } + + } + + private static class FoDSessionHandler extends AbstractSessionHandler { + @Override public String friendlyName() { "FoD" } + @Override public String module() { "fod" } + + @Override + public boolean isEnabled() { + has("url") + } + + @Override + public List loginOptions() { + option("url") + } + + @Override + public List loginCredentialOptions() { + options("tenant", "user", "password")+options("client-id", "client-secret") + } + + @Override + public List logoutOptions() { + return [] + } + + @Override + public List getMaskedProperties() { + ["url", "tenant", "user", "password", "client-id", "client-secret"] + } + } + + private static class SCSastSessionHandler extends AbstractSessionHandler { + @Override public String friendlyName() { "ScanCentral SAST" } + @Override public String module() { "sc-sast" } + + @Override + public boolean isEnabled() { + has("ssc-url") + } + + @Override + public List loginOptions() { + option("ssc-url") + } + + @Override + public List loginCredentialOptions() { + options("ssc-user", "ssc-password", "client-auth-token")+options("ssc-ci-token", "client-auth-token") + } + + @Override + public List logoutOptions() { + def result = options("ssc-user", "ssc-password") + return result.size()==0 ? "--no-revoke-token" : result + } + + @Override + public List getMaskedProperties() { + ["ssc-url", "ssc-user", "ssc-password", "ssc-ci-token", "client-auth-token"] + } + } + + private static class SCDastSessionHandler extends AbstractSessionHandler { + @Override public String friendlyName() { "ScanCentral DAST" } + @Override public String module() { "sc-dast" } + + @Override + public boolean isEnabled() { + has("ssc-url") + } + + @Override + public List loginOptions() { + options("ssc-url") + } + + @Override + public List loginCredentialOptions() { + options("ssc-user", "ssc-password")+options("ssc-ci-token") + } + + @Override + public List logoutOptions() { + def result = options("ssc-user", "ssc-password") + return result.size()==0 ? "--no-revoke-token" : result + } + + @Override + public List getMaskedProperties() { + ["ssc-url", "ssc-user", "ssc-password", "ssc-ci-token"] + } + } + + } } \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/FcliSessionType.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/FcliSessionType.groovy deleted file mode 100644 index f7d253e9b4..0000000000 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/FcliSessionType.groovy +++ /dev/null @@ -1,229 +0,0 @@ -package com.fortify.cli.ftest._common.spec; - -import org.spockframework.runtime.extension.IMethodInterceptor - -import com.fortify.cli.ftest._common.Fcli -import com.fortify.cli.ftest._common.Input - -public enum FcliSessionType { - SSC(new SSCSessionHandler()), - FOD(new FoDSessionHandler()), - SCSAST(new SCSastSessionHandler()), - SCDAST(new SCDastSessionHandler()) - - final ISessionHandler handler - - private FcliSessionType(ISessionHandler handler) { - this.handler = handler - } - - static void logoutAll() { - FcliSessionType.values().each { it.handler.logout() } - } - - private interface ISessionHandler { - String friendlyName(); - boolean isEnabled(); - boolean login(); - void logout(); - List getMaskedProperties(); - } - - private static abstract class AbstractSessionHandler implements ISessionHandler { - def STD_LOGIN_ARGS = [module(), "session","login"] - def STD_LOGOUT_ARGS = [module(), "session","logout"] - private boolean loggedIn = false; - private boolean failed = false; - - @Override - public final boolean login() { - if ( !loggedIn && !failed ) { - println("Logging in to "+friendlyName()) - try { - def loginCredentialOptions = loginCredentialOptions() - if ( loginCredentialOptions==null || loginCredentialOptions.size()==0 ) { - throw new IllegalArgumentException("No or incomplete "+friendlyName()+" credentials provided, tests will be skipped") - } - def valuesToMask = values(maskedProperties) - Fcli.stringsToMask += valuesToMask - Fcli.run( - STD_LOGIN_ARGS+loginOptions()+loginCredentialOptions, - {it.expectSuccess(true, "Error logging in to "+friendlyName()+", tests will be skipped")} - ) - - loggedIn = true - } catch ( Exception e ) { - e.printStackTrace() - failed = true - } - } - return !failed; - } - - @Override - public synchronized final void logout() { - if ( loggedIn ) { - Fcli.stringsToMask += values(maskedProperties) - def result = Fcli.run( - STD_LOGOUT_ARGS+logoutOptions(), - { - if ( !it.success ) { - err.println("Error logging out from "+friendlyName()+"\n"+it.stderr.join("\n ")) - } - }) - loggedIn = false - } - } - - abstract String module() - abstract List loginOptions() - abstract List loginCredentialOptions() - abstract List logoutOptions() - - String basePropertyName() { - Input.addPropertyPrefix(module()) - } - String get(String subPropertyName) { - System.properties[basePropertyName()+"."+subPropertyName] - } - boolean has(String subPropertyName) { - get(subPropertyName) - } - List option(String optName) { - has(optName) ? ["--"+optName, get(optName)] : [] - } - List options(String ... optNames) { - return optNames.every { has(it) } - ? optNames.stream().map(this.&option).collect().flatten() - : [] - } - List values(List optNames) { - return optNames.stream().map(this.&get).filter({it!=null}).collect().flatten(); - } - } - - private static class SSCSessionHandler extends AbstractSessionHandler { - @Override public String friendlyName() { "SSC" } - @Override public String module() { "ssc" } - - @Override - public boolean isEnabled() { - has("url") - } - - @Override - public List loginOptions() { - option("url") - } - - @Override - public List loginCredentialOptions() { - options("user", "password")+options("token")+options("ci-token") - } - - @Override - public List logoutOptions() { - def result = options("user", "password") - return result.size()==0 ? ["--no-revoke-token"] : result - } - - @Override - public List getMaskedProperties() { - ["url", "user", "password", "token", "ci-token"] - } - - } - - private static class FoDSessionHandler extends AbstractSessionHandler { - @Override public String friendlyName() { "FoD" } - @Override public String module() { "fod" } - - @Override - public boolean isEnabled() { - has("url") - } - - @Override - public List loginOptions() { - option("url") - } - - @Override - public List loginCredentialOptions() { - options("tenant", "user", "password")+options("client-id", "client-secret") - } - - @Override - public List logoutOptions() { - return [] - } - - @Override - public List getMaskedProperties() { - ["url", "tenant", "user", "password", "client-id", "client-secret"] - } - } - - private static class SCSastSessionHandler extends AbstractSessionHandler { - @Override public String friendlyName() { "ScanCentral SAST" } - @Override public String module() { "sc-sast" } - - @Override - public boolean isEnabled() { - has("ssc-url") - } - - @Override - public List loginOptions() { - option("ssc-url") - } - - @Override - public List loginCredentialOptions() { - options("ssc-user", "ssc-password", "client-auth-token")+options("ssc-ci-token", "client-auth-token") - } - - @Override - public List logoutOptions() { - def result = options("ssc-user", "ssc-password") - return result.size()==0 ? "--no-revoke-token" : result - } - - @Override - public List getMaskedProperties() { - ["ssc-url", "ssc-user", "ssc-password", "ssc-ci-token", "client-auth-token"] - } - } - - private static class SCDastSessionHandler extends AbstractSessionHandler { - @Override public String friendlyName() { "ScanCentral DAST" } - @Override public String module() { "sc-dast" } - - @Override - public boolean isEnabled() { - has("ssc-url") - } - - @Override - public List loginOptions() { - options("ssc-url") - } - - @Override - public List loginCredentialOptions() { - options("ssc-user", "ssc-password")+options("ssc-ci-token") - } - - @Override - public List logoutOptions() { - def result = options("ssc-user", "ssc-password") - return result.size()==0 ? "--no-revoke-token" : result - } - - @Override - public List getMaskedProperties() { - ["ssc-url", "ssc-user", "ssc-password", "ssc-ci-token"] - } - } - -} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/Global.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/Global.groovy new file mode 100644 index 0000000000..8e7f140676 --- /dev/null +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/spec/Global.groovy @@ -0,0 +1,19 @@ +package com.fortify.cli.ftest._common.spec; + +import static java.lang.annotation.ElementType.FIELD +import static java.lang.annotation.RetentionPolicy.RUNTIME + +import java.lang.annotation.Retention +import java.lang.annotation.Target + +import com.fortify.cli.ftest._common.util.WorkDirHelper + +@Target([FIELD]) +@Retention(RUNTIME) +@interface Global { + Class value() + + public static interface IGlobalValueSupplier extends Closeable, AutoCloseable { + public Object getValue(WorkDirHelper workDirHelper); + } +} \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/util/TempDirHelper.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/util/TempDirHelper.groovy deleted file mode 100644 index 9284081c2d..0000000000 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/util/TempDirHelper.groovy +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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.ftest._common.util - -import java.nio.file.Files -import java.nio.file.Path - -class TempDirHelper { - static Path create() { - Files.createTempDirectory("fcli").toAbsolutePath() - } - static void rm(Path tempDir) { - try { - Files.walk(tempDir) - .sorted(Comparator.reverseOrder()) - .map(Path.&toFile) - .forEach(File.&delete); // For some reason this throws an exception on the - // top-level directory, but the full directory tree - // is being deleted anyway. As such, we just swallow - // any exceptions, and print an error if the directory - // still exists afterwards. - } catch ( IOException e ) {} - if ( tempDir.toFile().exists() ) { - println "Error deleting directory "+tempDir+", please clean up manually"; - } - } -} diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/util/WorkDirHelper.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/util/WorkDirHelper.groovy new file mode 100644 index 0000000000..b89913bd81 --- /dev/null +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/_common/util/WorkDirHelper.groovy @@ -0,0 +1,117 @@ +/** + * 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.ftest._common.util + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption + +import groovy.transform.CompileStatic + +@CompileStatic +class WorkDirHelper implements Closeable, AutoCloseable { + private Path workDir; + + @Override + public void close() { + removeWorkDir(); + } + + public final Path getFortifyDataDir() { + return resolve(".fortify", PathType.DIR); + } + + public final Path getResource(String name) { + return extractResource(name); + } + + public final Path getTempDir(String name) { + return getTempPath(name, PathType.DIR); + } + + public final Path getTempFile(String name) { + return getTempPath(name, PathType.FILE); + } + + private final Path getTempPath(String name, PathType type) { + return resolve(Path.of("temp", name), type); + } + + private final enum PathType { + DIR, FILE + } + + private final Path resolve(String relativePath, PathType type) { + return resolve(Path.of(relativePath), type); + } + + private final Path resolve(Path relativePath, PathType type) { + def path = getWorkDir().resolve(relativePath); + ensureDir(type==PathType.DIR ? path : path.getParent()); + return path; + } + + private final void ensureDir(Path path) { + if ( !Files.exists(path) ) { + Files.createDirectories(path); + } + } + + private final Path getWorkDir() { + if ( workDir==null ) { + workDir = Files.createTempDirectory("fcli").toAbsolutePath() + } + return workDir; + } + + private Path extractResource(String resourceFile) { + Path outputFilePath = resolve(resourceFile, PathType.FILE); + if ( !Files.exists(outputFilePath) ) { + getResourceInputStream(resourceFile).withCloseable { + outputFilePath.parent.toFile().mkdirs() + Files.copy(it, outputFilePath, StandardCopyOption.REPLACE_EXISTING) + } + } + return outputFilePath + } + + private InputStream getResourceInputStream(String resourceFile) { + def cl = this.class.classLoader + def stream = cl.getResourceAsStream(resourceFile) + if ( stream==null ) { + stream = cl.getResourceAsStream(resourceFile+"-no-shadow") + } + if ( stream==null ) { + throw new IllegalStateException("${resourceFile} (or ${resourceFile}-no-shadow) referenced in @TestResource not found") + } + return stream + } + + private void removeWorkDir() { + if ( workDir && !System.getProperty("ft.keep-temp-dirs") ) { + try { + Files.walk(workDir) + .sorted(Comparator.reverseOrder()) + .map(Path.&toFile) + .forEach(File.&delete); // For some reason this throws an exception on the + // top-level directory, but the full directory tree + // is being deleted anyway. As such, we just swallow + // any exceptions, and print an error if the directory + // still exists afterwards. + } catch ( IOException e ) {} + if ( workDir.toFile().exists() ) { + println "Error deleting directory "+workDir+", please clean up manually"; + } + } + } +} diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/config/ConfigProxySpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/config/ConfigProxySpec.groovy index ac5c711453..3fe6006aea 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/config/ConfigProxySpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/config/ConfigProxySpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.config; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -15,7 +15,7 @@ import spock.lang.Stepwise @Prefix("config.proxy") @Stepwise class ConfigProxySpec extends FcliBaseSpec { - @Shared @TempDir("sc-client") String scClientInstallDir; + @Shared @TempDir("fortify/tools") String baseDir; def setupSpec() { Fcli.run("config proxy clear") @@ -57,7 +57,7 @@ class ConfigProxySpec extends FcliBaseSpec { } def "install.failure"() { - def args = "tool sc-client install -y -d ${scClientInstallDir}" + def args = "tool sc-client install -y -b ${baseDir}" when: def result = Fcli.run(args, {it.expectSuccess(false)}) then: diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/config/ConfigTrustStoreSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/config/ConfigTrustStoreSpec.groovy index 8c0567391e..d1ff88d559 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/config/ConfigTrustStoreSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/config/ConfigTrustStoreSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.config; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -15,7 +15,7 @@ import spock.lang.Stepwise @Prefix("config.truststore") @Stepwise class ConfigTrustStoreSpec extends FcliBaseSpec { - @Shared @TempDir("sc-client") String scClientInstallDir; + @Shared @TempDir("fortify/tools") String baseDir; @Shared @TestResource("runtime/config/dummyStore.jks") String dummyStore; def setupSpec() { @@ -57,7 +57,7 @@ class ConfigTrustStoreSpec extends FcliBaseSpec { } def "install.failure"() { - def args = "tool sc-client install -y -d ${scClientInstallDir}" + def args = "tool sc-client install -y -b ${baseDir}" when: def result = Fcli.run(args, {it.expectSuccess(false)}) then: diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlGroupSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlGroupSpec.groovy index dbe7a02f97..d3370176fd 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlGroupSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlGroupSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.fod; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.FOD +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlRoleSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlRoleSpec.groovy index b42755578e..e941c050dd 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlRoleSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlRoleSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.fod; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.FOD +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -50,7 +50,7 @@ class FoDAccessControlRoleSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>2 - it[9].equals("roleName: \"Developer\"") + it[9].equals("roleName: Developer") } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlUserSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlUserSpec.groovy index 72f3e4d5a7..deaaa4ff6e 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlUserSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAccessControlUserSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.fod; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.FOD +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -20,6 +20,8 @@ class FoDAccessControlUserSpec extends FcliBaseSpec { @Shared @AutoCleanup FoDUserSupplier user = new FoDUserSupplier() @Shared @AutoCleanup FoDUserGroupSupplier group = new FoDUserGroupSupplier() @Shared @AutoCleanup FoDWebAppSupplier app = new FoDWebAppSupplier() + private final String random = System.currentTimeMillis() + final String randomEmail = "u${random}@test.test" def "list"() { def args = "fod ac list-users" @@ -96,12 +98,12 @@ class FoDAccessControlUserSpec extends FcliBaseSpec { } def "updateAddApps"() { - def args = "fod ac update-user ${user.get().userName} --add-apps=${app.get().appName} --email test2@test.test" + def args = "fod ac update-user ${user.get().userName} --add-apps=${app.get().appName} --email ${randomEmail}" when: def result = Fcli.run(args) then: verifyAll(result.stdout) { - it.any { it.contains("${user.get().userName}") && it.contains("test2@test.test") } + it.any { it.contains("${user.get().userName}") && it.contains(randomEmail) } } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAppSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAppSpec.groovy index 38b679c663..640fd7fbe4 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAppSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDAppSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.fod; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.FOD +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -79,7 +79,7 @@ class FoDAppSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>2 - it[2].equals("applicationName: \"" + webApp.get().appName + "\"") + it[2].equals("applicationName: " + webApp.get().appName) } } @@ -90,7 +90,7 @@ class FoDAppSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>2 - it[2].equals("applicationName: \"" + mobileApp.get().appName + "\"") + it[2].equals("applicationName: " + mobileApp.get().appName) } } @@ -101,7 +101,7 @@ class FoDAppSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>2 - it[2].equals("applicationName: \"" + microservicesApp.get().appName + "\"") + it[2].equals("applicationName: " + microservicesApp.get().appName) } } @@ -155,7 +155,7 @@ class FoDAppSpec extends FcliBaseSpec { def result = Fcli.run(args) then: verifyAll(result.stdout) { - it[6].equals("businessCriticalityType: \"High\"") + it[6].equals("businessCriticalityType: High") } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDBuiltinActionDefinitionSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDBuiltinActionDefinitionSpec.groovy new file mode 100644 index 0000000000..cbd9c7491f --- /dev/null +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDBuiltinActionDefinitionSpec.groovy @@ -0,0 +1,63 @@ +/** + * 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.ftest.fod + +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC + +import com.fortify.cli.ftest._common.Fcli +import com.fortify.cli.ftest._common.spec.FcliBaseSpec +import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Prefix + +import spock.lang.AutoCleanup +import spock.lang.Requires +import spock.lang.Shared + +@Prefix("fod.action.def") +class FoDBuiltinActionDefinitionSpec extends FcliBaseSpec { + + def "list"() { + def args = "fod action list -q origin=='FCLI'" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()>1 + it[0].replace(' ', '').equals("NameAuthorOriginStatusSignatureUsageheader") + // TODO Is this working correctly? Ideally, we should ignore empty lines, + // rather than lines not containing FCLI, but that doesn't work. + it[2..-1].every { + !it.contains('FCLI') || it.replace(' ', '').contains("FortifyFCLIVALIDVALID") + } + } + } + + def "help"() { + def args = "fod action help ${action}" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()>1 + it.any { + it.contains('Origin:') && it.contains('FCLI') + it.contains('Signature status:') && it.contains('VALID') + it.contains('Author:') && it.contains('Fortify') + it.contains('Signed by:') && it.contains('Fortify') + it.contains('Certified by:') && it.contains('Fortify') + } + } + where: + action << Fcli.run("fod action list -q origin=='FCLI' -o expr={name}\\n").stdout + } +} diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDBuiltinActionRunSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDBuiltinActionRunSpec.groovy new file mode 100644 index 0000000000..55a9267161 --- /dev/null +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDBuiltinActionRunSpec.groovy @@ -0,0 +1,82 @@ +/** + * 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.ftest.fod + +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD + +import java.nio.file.Files +import java.nio.file.Path + +import com.fortify.cli.ftest._common.Fcli +import com.fortify.cli.ftest._common.spec.FcliBaseSpec +import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Global +import com.fortify.cli.ftest._common.spec.Prefix +import com.fortify.cli.ftest._common.spec.TempDir +import com.fortify.cli.ftest.fod._common.FoDReleaseSupplier + +import spock.lang.Shared + +@Prefix("fod.action.run") @FcliSession(FOD) +class FoDBuiltinActionRunSpec extends FcliBaseSpec { + @Shared @TempDir("action-output") String actionOutputDir; + @Global(FoDReleaseSupplier.EightBall.class) FoDReleaseSupplier eightBallReleaseSupplier; + + def "runWithOutputFile"() { + def random = System.currentTimeMillis() + def outputFile = "${actionOutputDir}/output-${random}" + def args = "fod action run ${action} -f ${outputFile} --rel ${eightBallReleaseSupplier.release.get("releaseId")}" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()==1 + Files.exists(Path.of(outputFile)) + } + where: + action << ['release-summary', + 'github-sast-report', + 'gitlab-dast-report', + 'gitlab-sast-report', + 'sarif-sast-report', + 'sonarqube-sast-report'] + } + + + def "runBitBucketSastReport"() { + def random = System.currentTimeMillis() + def reportFile = "${actionOutputDir}/bb-report-${random}" + def annotationsFile = "${actionOutputDir}/bb-annotations-${random}" + def args = "fod action run bitbucket-sast-report -r ${reportFile} -a ${annotationsFile} --rel ${eightBallReleaseSupplier.release.get("releaseId")}" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()==2 + Files.exists(Path.of(reportFile)) + Files.exists(Path.of(annotationsFile)) + } + } + + def "runCheckPolicy"() { + def args = "fod action run check-policy --rel ${eightBallReleaseSupplier.release.get("releaseId")}" + when: + def result = Fcli.run(args, {}) + then: + verifyAll(result.stdout) { + size()>1 + it.any { it.contains('PASS') || it.contains('FAIL') } + it.any { it.contains("Status: ") } + } + } +} diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDMicroserviceSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDMicroserviceSpec.groovy index ae1d7e4e7b..9e0bd441e4 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDMicroserviceSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDMicroserviceSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.fod; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.FOD +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDReleaseSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDReleaseSpec.groovy index 9c826d8708..30da94155a 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDReleaseSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDReleaseSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.fod; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.FOD +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -85,8 +85,8 @@ class FoDReleaseSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>2 - it.any { it.contains("releaseName: \"testrel\"") } - it.any { it.contains("applicationName: \"${app.get().appName}\"") } + it.any { it.contains("releaseName: testrel") } + it.any { it.contains("applicationName: ${app.get().appName}") } } } @@ -97,8 +97,8 @@ class FoDReleaseSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>2 - it.any { it.contains("releaseName: \"testrel\"") } - it.any { it.contains("applicationName: \"${app.get().appName}\"") } + it.any { it.contains("releaseName: testrel") } + it.any { it.contains("applicationName: ${app.get().appName}") } } } @@ -119,7 +119,7 @@ class FoDReleaseSpec extends FcliBaseSpec { def result = Fcli.run(args) then: verifyAll(result.stdout) { - it.any {it.equals("sdlcStatusType: \"QA\"") } + it.any {it.equals("sdlcStatusType: QA") } } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDRestSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDRestSpec.groovy index 36c8d57544..e044726df3 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDRestSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDRestSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.fod; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.FOD +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanImportSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanImportSpec.groovy index c153281c83..ebdb9b025d 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanImportSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanImportSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.fod; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.FOD +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -29,7 +29,7 @@ class FoDScanImportSpec extends FcliBaseSpec { def result = Fcli.run(args) then: verifyAll(result.stdout) { - size()>2 + size()>=2 it.last().contains("IMPORT_REQUESTED") } } @@ -40,7 +40,7 @@ class FoDScanImportSpec extends FcliBaseSpec { def result = Fcli.run(args) then: verifyAll(result.stdout) { - size()>2 + size()>=2 it.last().contains("IMPORT_REQUESTED") } } @@ -51,7 +51,7 @@ class FoDScanImportSpec extends FcliBaseSpec { def result = Fcli.run(args) then: verifyAll(result.stdout) { - size()>2 + size()>=2 it.last().contains("IMPORT_REQUESTED") } } @@ -62,7 +62,7 @@ class FoDScanImportSpec extends FcliBaseSpec { def result = Fcli.run(args) then: verifyAll(result.stdout) { - size()>2 + size()>=2 it.last().contains("IMPORT_REQUESTED") } } @@ -75,7 +75,7 @@ class FoDScanImportSpec extends FcliBaseSpec { def success = false; while(true){ def result = Fcli.run("fod rest call ${relScanurl}") - if(result.stdout.findAll{element -> element.contains("analysisStatusType: \"Completed\"")}.size()==3) { + if(result.stdout.findAll{element -> element.contains("analysisStatusType: Completed")}.size()==3) { success=true; break; } else if(System.currentTimeMillis()-start > timeoutMs) { @@ -95,7 +95,7 @@ class FoDScanImportSpec extends FcliBaseSpec { def success = false; while(true){ def result = Fcli.run("fod rest call ${relScanurl}") - if(result.stdout.findAll{element -> element.contains("analysisStatusType: \"Completed\"")}.size()==1) { + if(result.stdout.findAll{element -> element.contains("analysisStatusType: Completed")}.size()==1) { success=true; break; } else if(System.currentTimeMillis()-start > timeoutMs) { diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanSpec.groovy index 9081726115..e684432d5c 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.fod; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.FOD +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.FOD import com.fortify.cli.ftest._common.spec.FcliBaseSpec import com.fortify.cli.ftest._common.spec.FcliSession @@ -83,7 +83,7 @@ class FoDScanSpec extends FcliBaseSpec { while(true){ def result = Fcli.run("fod rest call ${relScanurl}") if(result.stdout.findAll{ - element -> element.contains("analysisStatusType: \"Completed\"")}.size()==4) { + element -> element.contains("analysisStatusType: Completed")}.size()==4) { success=true; break; } else if(System.currentTimeMillis()-start > timeoutMs) { @@ -114,7 +114,7 @@ class FoDScanSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>=2 - //it.any {it.contains("applicationName: \"${webApp.get().appName}\"")} + //it.any {it.contains("applicationName: ${webApp.get().appName}")} } } @@ -135,7 +135,7 @@ class FoDScanSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>=2 - it.last().contains("state: \"Configured\"") + it.last().contains("state: Configured") } } /* @@ -179,7 +179,7 @@ class FoDScanSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>=2 - //it.any {it.contains("applicationName: \"${webApp.get().appName}\"")} + //it.any {it.contains("applicationName: ${webApp.get().appName}")} } } @@ -190,7 +190,7 @@ class FoDScanSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>=2 - it.last().contains("state: \"Not configured\"") + it.last().contains("state: Not configured") } } @@ -234,7 +234,7 @@ class FoDScanSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>=2 - //it.any {it.contains("applicationName: \"${webApp.get().appName}\"")} + //it.any {it.contains("applicationName: ${webApp.get().appName}")} } } @@ -245,7 +245,7 @@ class FoDScanSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>=2 - it.last().contains("state: \"Not configured\"") + it.last().contains("state: Not configured") } } @@ -289,7 +289,7 @@ class FoDScanSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>=2 - //it.any {it.contains("applicationName: \"${webApp.get().appName}\"")} + //it.any {it.contains("applicationName: ${webApp.get().appName}")} } } @@ -300,7 +300,7 @@ class FoDScanSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>=2 - it.last().contains("state: \"Not configured\"") + it.last().contains("state: Not configured") } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/_common/FoDReleaseSupplier.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/_common/FoDReleaseSupplier.groovy new file mode 100644 index 0000000000..3acbbcc8b2 --- /dev/null +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/_common/FoDReleaseSupplier.groovy @@ -0,0 +1,144 @@ +/** + * 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.ftest.fod._common + +import java.nio.file.Path + +import com.fortify.cli.ftest._common.Fcli +import com.fortify.cli.ftest._common.spec.Global.IGlobalValueSupplier +import com.fortify.cli.ftest._common.util.WorkDirHelper +import com.fortify.cli.ftest.fod._common.AbstractFoDAppSupplier.FoDApp +import com.fortify.cli.ftest.ssc._common.SSCAppVersionSupplier.SSCAppVersion + +public class FoDReleaseSupplier implements Closeable, AutoCloseable { + private final Closure init; + private FoDRelease release; + + public FoDReleaseSupplier() { + this({}); + } + + public FoDReleaseSupplier(Closure init) { + this.init = init; + } + + public FoDRelease getRelease() { + if ( !release ) { + release = new FoDRelease().create() + init(release); + } + return release + } + + @Override + public void close() { + if ( release ) { + release.close(); + release = null; + } + } + + public static class FoDRelease { + private final String random = System.currentTimeMillis() + private final String fcliVariableName = "fod_release_"+random + private final String releaseName = "vx"+random + private final FoDApp app = new FoDWebAppSupplier().createInstance() + + public FoDRelease create() { + Fcli.run("fod release create ${app.appName}:${releaseName} "+ + "--status Development --store ${fcliVariableName}", + {it.expectSuccess(true, "Unable to create application release")}) + return this + } + + public String get(String propertyPath) { + Fcli.run("util var contents $fcliVariableName -o expr={$propertyPath}", + {it.expectSuccess(true, "Error getting application version property "+propertyPath)}) + .stdout[0] + } + + public String getVariableName() { + return fcliVariableName + } + + public String getVariableRef() { + return "::"+fcliVariableName+"::" + } + + public void close() { + app.close() // Should automatically delete release? + } + } + + private static abstract class ReleaseGlobalValueSupplier implements IGlobalValueSupplier { + private FoDReleaseSupplier releaseSupplier; + public final FoDReleaseSupplier getValue(WorkDirHelper workDirHelper) { + if ( !releaseSupplier ) { + releaseSupplier = new FoDReleaseSupplier({ FoDRelease r -> initRelease(workDirHelper, r) }); + } + return releaseSupplier + } + @Override + public final void close() { + if ( releaseSupplier ) { + releaseSupplier.close(); + releaseSupplier = null; + } + } + protected String upload(FoDRelease release, Path fprPath) { + def varName = "global${this.class.simpleName}Fpr" + Fcli.run("fod sast-scan import -f $fprPath --release ${release.variableRef} --store ${varName}") + waitForScan(release) + return varName; + } + protected abstract void initRelease(WorkDirHelper workDirHelper, FoDRelease version); + + private void waitForScan(FoDRelease release) { + def relScanurl = Fcli.run("fod release get ${release.variableRef} -o expr=/api/v3/releases/{releaseId}/scans --store relId").stdout[0] + def timeoutMs = 60000 + def start = System.currentTimeMillis() + def success = false; + while(true){ + def result = Fcli.run("fod rest call ${relScanurl}") + if(result.stdout.findAll{element -> element.contains("analysisStatusType: Completed")}.size()>0) { + success=true; + break; + } else if(System.currentTimeMillis()-start > timeoutMs) { + break; + } + sleep(3000) + } + } + } + + public static class Empty extends ReleaseGlobalValueSupplier { + @Override + protected void initRelease(WorkDirHelper workDirHelper, FoDRelease version) {} + } + + public static class EightBall extends ReleaseGlobalValueSupplier { + @Override + protected void initRelease(WorkDirHelper workDirHelper, FoDRelease version) { + upload(version, workDirHelper.getResource("runtime/shared/EightBall-22.1.0.fpr")); + } + } + + public static class LoginProject extends ReleaseGlobalValueSupplier { + @Override + protected void initRelease(WorkDirHelper workDirHelper, FoDRelease version) { + upload(version, workDirHelper.getResource("runtime/shared/LoginProject.fpr")); + } + } +} + + diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/license/NcdReportSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/license/NcdReportSpec.groovy index 01ad15d426..9a9ef923cd 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/license/NcdReportSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/license/NcdReportSpec.groovy @@ -36,8 +36,8 @@ class NcdReportSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size() == 3 - it[1] ==~ /^path: "$sampleConfigOutputFile"$/ - it[2] ==~ /^__action__: "GENERATED"$/ + it[1] ==~ /^path: $sampleConfigOutputFile$/ + it[2] ==~ /^__action__: GENERATED$/ } new File(sampleConfigOutputFile).exists() } @@ -58,8 +58,8 @@ class NcdReportSpec extends FcliBaseSpec { new File("${reportOutputDir}/details/contributors-by-repository.csv").exists() new File("${reportOutputDir}/details/repositories.csv").exists() verifyAll(result.stdout) { - it.any { it == "reportPath: \"${reportOutputDir}\"" } - it.any { it == ' reportType: "Number of Contributing Developers (NCD) Report"' } + it.any { it == "reportPath: ${reportOutputDir}" } + it.any { it == ' reportType: Number of Contributing Developers (NCD) Report' } it.any { it.contains("repositoryCounts:") } it.any { it.contains("commitCount:") } it.any { it.contains("authorCount:") } @@ -74,8 +74,8 @@ class NcdReportSpec extends FcliBaseSpec { def result = Fcli.run(args) then: verifyAll(result.stdout) { - it.any { it == "reportPath: \"${reportOutputZip}\"" } - it.any { it == ' reportType: "Number of Contributing Developers (NCD) Report"' } + it.any { it == "reportPath: ${reportOutputZip}" } + it.any { it == ' reportType: Number of Contributing Developers (NCD) Report' } it.any { it.contains("repositoryCounts:") } it.any { it.contains("commitCount:") } it.any { it.contains("authorCount:") } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_dast/SCDastRestCallSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_dast/SCDastRestCallSpec.groovy index 21835aa76f..21c29d9d67 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_dast/SCDastRestCallSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_dast/SCDastRestCallSpec.groovy @@ -12,8 +12,8 @@ */ package com.fortify.cli.ftest.sc_dast -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SCDAST -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SCSAST +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SCDAST +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SCSAST import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_dast/SCDastSensorSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_dast/SCDastSensorSpec.groovy index 3e6f40c223..382d33f924 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_dast/SCDastSensorSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_dast/SCDastSensorSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.sc_dast; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SCDAST +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SCDAST import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_sast/SCSastRestCallSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_sast/SCSastRestCallSpec.groovy index 86733ae36f..1dbe201818 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_sast/SCSastRestCallSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_sast/SCSastRestCallSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.sc_sast -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SCSAST +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SCSAST import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -32,7 +32,7 @@ class SCSastRestCallSpec extends FcliBaseSpec { verifyAll(result.stdout) { size()==2 it[0] == '---' - it[1] =~ '- message: ".* I am still alive."' + it[1] =~ '- message: .* I am still alive.' } } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_sast/SCSastScanSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_sast/SCSastScanSpec.groovy index 845af96791..fc0533f50b 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_sast/SCSastScanSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/sc_sast/SCSastScanSpec.groovy @@ -1,7 +1,7 @@ package com.fortify.cli.ftest.sc_sast; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SCSAST -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SCSAST +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlAppVersionUserSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlAppVersionUserSpec.groovy index fecd2ed1ee..2e14f830e3 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlAppVersionUserSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlAppVersionUserSpec.groovy @@ -1,21 +1,22 @@ package com.fortify.cli.ftest.ssc; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli -import com.fortify.cli.ftest._common.Fcli.UnexpectedFcliResultException import com.fortify.cli.ftest._common.spec.FcliBaseSpec import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Global import com.fortify.cli.ftest._common.spec.Prefix import com.fortify.cli.ftest.ssc._common.SSCAppVersionSupplier import com.fortify.cli.ftest.ssc._common.SSCLocalUserSupplier + import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Stepwise @Prefix("ssc.appversion-user") @FcliSession(SSC) @Stepwise class SSCAccessControlAppVersionUserSpec extends FcliBaseSpec { - @Shared @AutoCleanup SSCAppVersionSupplier versionSupplier = new SSCAppVersionSupplier() + @Global(SSCAppVersionSupplier.Empty.class) SSCAppVersionSupplier versionSupplier; @Shared @AutoCleanup SSCLocalUserSupplier userSupplier = new SSCLocalUserSupplier() def "list"() { diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlPermissionSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlPermissionSpec.groovy index 98c75f248a..f08d459568 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlPermissionSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlPermissionSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -45,7 +45,7 @@ class SSCAccessControlPermissionSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it[1].equals("id: \"user_view\"") + it[1].equals("id: user_view") } } @@ -56,7 +56,7 @@ class SSCAccessControlPermissionSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it[1].equals("id: \"user_view\"") + it[1].equals("id: user_view") } } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlRoleSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlRoleSpec.groovy index 72e617966c..076afe279d 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlRoleSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlRoleSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -55,7 +55,7 @@ class SSCAccessControlRoleSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it[2].equals("name: \"" + roleSupplier.role.roleName + "\"") + it[2].equals("name: " + roleSupplier.role.roleName) } } @@ -66,7 +66,7 @@ class SSCAccessControlRoleSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it[2].equals("name: \"" + roleSupplier.role.roleName + "\"") + it[2].equals("name: " + roleSupplier.role.roleName) } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlTokenDefinitionSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlTokenDefinitionSpec.groovy index bf36ac749a..ef1c930b91 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlTokenDefinitionSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlTokenDefinitionSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlTokenSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlTokenSpec.groovy index 7f57f6f086..ab2650e0f8 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlTokenSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlTokenSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlUserSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlUserSpec.groovy index 6601b06b11..955e11019a 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlUserSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAccessControlUserSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -30,9 +30,9 @@ class SSCAccessControlUserSpec extends FcliBaseSpec { @Shared private final String random = System.currentTimeMillis() @Shared private final String userName = "fclitemporarytestuser"+random - @Shared private String unameval = "userName: \"$userName\"" - @Shared private String entityval = "entityName: \"$userName\"" - @Shared private String mailval = "email: \"$random@mail.mail\"" + @Shared private String unameval = "userName: $userName" + @Shared private String entityval = "entityName: $userName" + @Shared private String mailval = "email: $random@mail.mail" def "list"() { def args = "ssc ac list-users --store users" @@ -54,8 +54,8 @@ class SSCAccessControlUserSpec extends FcliBaseSpec { verifyAll(result.stdout) { size()>0 it[2].equals(unameval) - it[3].equals("firstName: \"fName\"") - it[4].equals("lastName: \"lName\"") + it[3].equals("firstName: fName") + it[4].equals("lastName: lName") it[5].equals(mailval) !it[11].equals("roles: null") } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAlertDefinitionSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAlertDefinitionSpec.groovy index 407b4eba24..956b51168a 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAlertDefinitionSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAlertDefinitionSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAlertSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAlertSpec.groovy index c95f7f6d3b..21c2df2047 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAlertSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAlertSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAppSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAppSpec.groovy index e211aa4fbf..cdc2aa1e7d 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAppSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAppSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.ssc; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAppVersionSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAppVersionSpec.groovy index 3ef9960859..cdb862063a 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAppVersionSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAppVersionSpec.groovy @@ -1,6 +1,6 @@ package com.fortify.cli.ftest.ssc; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.Fcli.UnexpectedFcliResultException @@ -45,8 +45,8 @@ class SSCAppVersionSpec extends FcliBaseSpec { def result = Fcli.run(args) then: verifyAll(result.stdout) { - it[4].equals(" name: \"" + versionSupplier.version.appName + "\""); - it[9].equals("name: \"" + versionSupplier.version.versionName + "\""); + it[4].equals(" name: " + versionSupplier.version.appName); + it[9].equals("name: " + versionSupplier.version.versionName); } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCArtifactUploadSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCArtifactUploadSpec.groovy index 4d68b93e71..37b67ba9b7 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCArtifactUploadSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCArtifactUploadSpec.groovy @@ -1,11 +1,12 @@ package com.fortify.cli.ftest.ssc; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec import com.fortify.cli.ftest._common.spec.FcliSession import com.fortify.cli.ftest._common.spec.Prefix +import com.fortify.cli.ftest._common.spec.TempFile import com.fortify.cli.ftest._common.spec.TestResource import com.fortify.cli.ftest.ssc._common.SSCAppVersionSupplier @@ -21,6 +22,7 @@ class SSCArtifactUploadSpec extends FcliBaseSpec { @Shared @TestResource("runtime/shared/LoginProject.fpr") String diffpr @Shared String uploadVariableName = versionSupplier.version.fcliVariableName+"_artifact" @Shared String uploadVariableRef = "::$uploadVariableName::" + @Shared @TempFile("artifactUploadSpec/download.fpr") String downloadedFpr; def "upload"() { def args = "ssc artifact upload -f $fpr --appversion "+ @@ -120,16 +122,17 @@ class SSCArtifactUploadSpec extends FcliBaseSpec { def result = Fcli.run(args) then: verifyAll(result.stdout) { - it.any { it.equals("originalFileName: \"EightBall-22.1.0.fpr\"") } + it.any { it.equals("originalFileName: EightBall-22.1.0.fpr") } } } def "download"() { - def args = "ssc artifact download ::upload::id -f download.fpr --no-include-sources" + def args = "ssc artifact download ::upload::id -f ${downloadedFpr} --no-include-sources" when: def result = Fcli.run(args) then: noExceptionThrown() + new File(downloadedFpr).exists() verifyAll(result.stdout) { it.last().contains("ARTIFACT_DOWNLOADED"); } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAttributeDefinitionSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAttributeDefinitionSpec.groovy index 96cdaaaa61..5d5d6c7324 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAttributeDefinitionSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAttributeDefinitionSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAttributeSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAttributeSpec.groovy index 3d5a3583bb..9d73b44856 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAttributeSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAttributeSpec.groovy @@ -1,11 +1,12 @@ package com.fortify.cli.ftest.ssc; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.Fcli.UnexpectedFcliResultException import com.fortify.cli.ftest._common.spec.FcliBaseSpec import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Global import com.fortify.cli.ftest._common.spec.Prefix import com.fortify.cli.ftest.ssc._common.SSCAppVersionSupplier @@ -15,7 +16,7 @@ import spock.lang.Stepwise @Prefix("ssc.appversion-attribute") @FcliSession(SSC) @Stepwise class SSCAttributeSpec extends FcliBaseSpec { - @Shared @AutoCleanup SSCAppVersionSupplier versionSupplier = new SSCAppVersionSupplier() + @Global(SSCAppVersionSupplier.Empty.class) SSCAppVersionSupplier versionSupplier; def "list"() { def args = "ssc attribute list --appversion " + versionSupplier.version.appName + ":" + versionSupplier.version.versionName diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCBuiltinActionDefinitionSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCBuiltinActionDefinitionSpec.groovy new file mode 100644 index 0000000000..8095a6fb5f --- /dev/null +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCBuiltinActionDefinitionSpec.groovy @@ -0,0 +1,63 @@ +/** + * 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.ftest.ssc + +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC + +import com.fortify.cli.ftest._common.Fcli +import com.fortify.cli.ftest._common.spec.FcliBaseSpec +import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Prefix + +import spock.lang.AutoCleanup +import spock.lang.Requires +import spock.lang.Shared + +@Prefix("ssc.action.def") +class SSCBuiltinActionDefinitionSpec extends FcliBaseSpec { + + def "list"() { + def args = "ssc action list -q origin=='FCLI'" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()>1 + it[0].replace(' ', '').equals("NameAuthorOriginStatusSignatureUsageheader") + // TODO Is this working correctly? Ideally, we should ignore empty lines, + // rather than lines not containing FCLI, but that doesn't work. + it[2..-1].every { + !it.contains('FCLI') || it.replace(' ', '').contains("FortifyFCLIVALIDVALID") + } + } + } + + def "help"() { + def args = "ssc action help ${action}" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()>1 + it.any { + it.contains('Origin:') && it.contains('FCLI') + it.contains('Signature status:') && it.contains('VALID') + it.contains('Author:') && it.contains('Fortify') + it.contains('Signed by:') && it.contains('Fortify') + it.contains('Certified by:') && it.contains('Fortify') + } + } + where: + action << Fcli.run("ssc action list -q origin=='FCLI' -o expr={name}\\n").stdout + } +} diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCBuiltinActionRunSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCBuiltinActionRunSpec.groovy new file mode 100644 index 0000000000..b89a7b838c --- /dev/null +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCBuiltinActionRunSpec.groovy @@ -0,0 +1,83 @@ +/** + * 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.ftest.ssc + +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC + +import java.nio.file.Files +import java.nio.file.Path + +import com.fortify.cli.ftest._common.Fcli +import com.fortify.cli.ftest._common.spec.FcliBaseSpec +import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Global +import com.fortify.cli.ftest._common.spec.Prefix +import com.fortify.cli.ftest._common.spec.TempDir +import com.fortify.cli.ftest.ssc._common.SSCAppVersionSupplier + +import spock.lang.Shared + +@Prefix("ssc.action.run") @FcliSession(SSC) +class SSCBuiltinActionRunSpec extends FcliBaseSpec { + @Shared @TempDir("action-output") String actionOutputDir; + @Global(SSCAppVersionSupplier.EightBall.class) SSCAppVersionSupplier eightBallVersionSupplier; + + def "runWithOutputFile"() { + def random = System.currentTimeMillis() + def outputFile = "${actionOutputDir}/output-${random}" + def args = "ssc action run ${action} -f ${outputFile} --av ${eightBallVersionSupplier.version.get("id")}" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()==1 + Files.exists(Path.of(outputFile)) + } + where: + action << ['appversion-summary', + 'github-sast-report', + 'gitlab-dast-report', + 'gitlab-debricked-report', + 'gitlab-sast-report', + 'gitlab-sonatype-report', + 'sarif-sast-report', + 'sonarqube-sast-report'] + } + + def "runBitBucketSastReport"() { + def random = System.currentTimeMillis() + def reportFile = "${actionOutputDir}/bb-report-${random}" + def annotationsFile = "${actionOutputDir}/bb-annotations-${random}" + def args = "ssc action run bitbucket-sast-report -r ${reportFile} -a ${annotationsFile} --av ${eightBallVersionSupplier.version.get("id")}" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()==2 + Files.exists(Path.of(reportFile)) + Files.exists(Path.of(annotationsFile)) + } + } + + def "runCheckPolicy"() { + def args = "ssc action run check-policy --av ${eightBallVersionSupplier.version.get("id")}" + when: + def result = Fcli.run(args, {}) + then: + verifyAll(result.stdout) { + size()>1 + it.any { it.contains('PASS') || it.contains('FAIL') } + it.any { it.contains("Status: ") } + } + } +} diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCIssueFilterSetSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCIssueFilterSetSpec.groovy index 85692fbe5c..1eefc04175 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCIssueFilterSetSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCIssueFilterSetSpec.groovy @@ -1,11 +1,12 @@ package com.fortify.cli.ftest.ssc; -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.Fcli.UnexpectedFcliResultException import com.fortify.cli.ftest._common.spec.FcliBaseSpec import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Global import com.fortify.cli.ftest._common.spec.Prefix import com.fortify.cli.ftest.ssc._common.SSCAppVersionSupplier @@ -15,7 +16,7 @@ import spock.lang.Stepwise @Prefix("ssc.appversion-filterset") @FcliSession(SSC) @Stepwise class SSCIssueFilterSetSpec extends FcliBaseSpec { - @Shared @AutoCleanup SSCAppVersionSupplier versionSupplier = new SSCAppVersionSupplier() + @Global(SSCAppVersionSupplier.Empty.class) SSCAppVersionSupplier versionSupplier; def "list"() { def args = "ssc issue list-filtersets --appversion " + versionSupplier.version.appName + ":" + versionSupplier.version.versionName + " --store filtersets" @@ -36,7 +37,7 @@ class SSCIssueFilterSetSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>2 - it[2].equals("title: \"Security Auditor View\"") + it[2].equals("title: Security Auditor View") } } @@ -47,7 +48,7 @@ class SSCIssueFilterSetSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>2 - it[2].equals("title: \"Security Auditor View\"") + it[2].equals("title: Security Auditor View") } } } \ No newline at end of file diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCIssueTemplateSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCIssueTemplateSpec.groovy index e92c87d690..bf3c460236 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCIssueTemplateSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCIssueTemplateSpec.groovy @@ -12,22 +12,22 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec import com.fortify.cli.ftest._common.spec.FcliSession import com.fortify.cli.ftest._common.spec.Prefix +import com.fortify.cli.ftest._common.spec.TempFile import com.fortify.cli.ftest._common.spec.TestResource -import spock.lang.AutoCleanup -import spock.lang.Requires import spock.lang.Shared import spock.lang.Stepwise @Prefix("ssc.issue-template") @FcliSession(SSC) @Stepwise class SSCIssueTemplateSpec extends FcliBaseSpec { @Shared @TestResource("runtime/ssc/issueTemplate.xml") String templateFile + @Shared @TempFile("issueTemplateSpec/download.xml") String downloadedTemplateFile private static final String random = System.currentTimeMillis() private static final String templateName = "fcli-test-Template"+random @@ -63,7 +63,7 @@ class SSCIssueTemplateSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it[2].equals("name: \"" + templateName + "\"") + it[2].equals("name: " + templateName) } } @@ -74,7 +74,7 @@ class SSCIssueTemplateSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it[2].equals("name: \"" + templateName + "\"") + it[2].equals("name: " + templateName) } } @@ -97,15 +97,16 @@ class SSCIssueTemplateSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it[2].equals("name: \"updatedName\"") + it[2].equals("name: updatedName") } } def "download"() { - def args = "ssc issue download-template ::template::id" + def args = "ssc issue download-template ::template::id -f ${downloadedTemplateFile}" when: def result = Fcli.run(args) then: + new File(downloadedTemplateFile).exists() verifyAll(result.stdout) { size()>0 it.last().contains("DOWNLOADED") diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCPluginSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCPluginSpec.groovy index e726bb517f..3eefc76196 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCPluginSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCPluginSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec @@ -88,8 +88,8 @@ class SSCPluginSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it.any { it.equals("pluginName: \"Alternative sample parser plugin\"") } - it.any { it.equals("pluginState: \"STOPPED\"") } + it.any { it.equals("pluginName: Alternative sample parser plugin") } + it.any { it.equals("pluginState: STOPPED") } } } @@ -101,8 +101,8 @@ class SSCPluginSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it.any { it.equals("pluginName: \"Alternative sample parser plugin\"") } - it.any { it.equals("pluginState: \"STOPPED\"") } + it.any { it.equals("pluginName: Alternative sample parser plugin") } + it.any { it.equals("pluginState: STOPPED") } } } @@ -126,8 +126,8 @@ class SSCPluginSpec extends FcliBaseSpec { then: verifyAll(result.stdout) { size()>0 - it.any { it.equals("pluginName: \"Alternative sample parser plugin\"") } - it.any { it.equals("pluginState: \"STARTED\"") } + it.any { it.equals("pluginName: Alternative sample parser plugin") } + it.any { it.equals("pluginState: STARTED") } } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCReportSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCReportSpec.groovy index 43b772614a..cace3777a7 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCReportSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCReportSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import java.nio.file.Files import java.nio.file.Path @@ -21,6 +21,7 @@ import java.text.SimpleDateFormat import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Global import com.fortify.cli.ftest._common.spec.Prefix import com.fortify.cli.ftest._common.spec.TempDir import com.fortify.cli.ftest._common.spec.TempFile @@ -34,23 +35,15 @@ import spock.lang.Stepwise @Prefix("ssc.report") @FcliSession(SSC) @Stepwise class SSCReportSpec extends FcliBaseSpec { @Shared @TempFile("report.pdf") String reportFile; - @Shared @AutoCleanup SSCAppVersionSupplier version1Supplier = new SSCAppVersionSupplier() - @Shared @AutoCleanup SSCAppVersionSupplier version2Supplier = new SSCAppVersionSupplier() - @Shared @TestResource("runtime/shared/EightBall-22.1.0.fpr") String fpr1 - @Shared @TestResource("runtime/shared/LoginProject.fpr") String fpr2 + @Global(SSCAppVersionSupplier.EightBall.class) SSCAppVersionSupplier eightBallVersionSupplier; + @Global(SSCAppVersionSupplier.LoginProject.class) SSCAppVersionSupplier loginProjectVersionSupplier; @Shared String random = System.currentTimeMillis() @Shared String owaspReportName = "fcli-OWASP-${random}" @Shared String trendingReportName = "fcli-Trending-${random}" @Shared String kpiReportName = "fcli-KPI-${random}" - def setupSpec() { - Fcli.run("ssc artifact upload -f $fpr1 --appversion ${version1Supplier.version.variableRef} --store fpr1") - Fcli.run("ssc artifact upload -f $fpr2 --appversion ${version2Supplier.version.variableRef} --store fpr2") - Fcli.run("ssc artifact wait-for ::fpr1:: ::fpr2:: -i 2s") - } - def "createOWASP"() { - def args = "ssc report create --template OWASP\\ Top\\ 10 --name ${owaspReportName} -p Application\\ Version=${version1Supplier.version.get("id")} --store owasp" + def args = "ssc report create --template OWASP\\ Top\\ 10 --name ${owaspReportName} -p Application\\ Version=${eightBallVersionSupplier.version.get("id")} --store owasp" when: def result = Fcli.run(args) then: @@ -62,7 +55,7 @@ class SSCReportSpec extends FcliBaseSpec { def "createTrending"() { def today = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); - def args = "ssc report create --template Issue\\ Trending --name ${trendingReportName} -p startDate=2010-01-01,endDate=${today},projectversionids=[${version1Supplier.version.get("id")};${version2Supplier.version.get("id")}] --store trending" + def args = "ssc report create --template Issue\\ Trending --name ${trendingReportName} -p startDate=2010-01-01,endDate=${today},projectversionids=[${eightBallVersionSupplier.version.get("id")};${loginProjectVersionSupplier.version.get("id")}] --store trending" when: def result = Fcli.run(args) then: @@ -73,7 +66,7 @@ class SSCReportSpec extends FcliBaseSpec { } def "createKPI"() { - def args = "ssc report create --template Key\\ Performance\\ Indicators --name ${kpiReportName} -p Application\\ Attribute=Accessibility,projectversionids=[${version1Supplier.version.get("id")};${version2Supplier.version.get("id")}] --store kpi" + def args = "ssc report create --template Key\\ Performance\\ Indicators --name ${kpiReportName} -p Application\\ Attribute=Accessibility,projectversionids=[${eightBallVersionSupplier.version.get("id")};${loginProjectVersionSupplier.version.get("id")}] --store kpi" when: def result = Fcli.run(args) then: diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCReportTemplateSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCReportTemplateSpec.groovy index bfb238cfb1..ad0e650fbc 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCReportTemplateSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCReportTemplateSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCRestCallSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCRestCallSpec.groovy index 17205209ee..88212fe943 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCRestCallSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCRestCallSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateActivitySpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateActivitySpec.groovy index 17edbfc8b4..7a7693e9dc 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateActivitySpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateActivitySpec.groovy @@ -12,11 +12,12 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Global import com.fortify.cli.ftest._common.spec.Prefix import com.fortify.cli.ftest.ssc._common.SSCAppVersionSupplier @@ -27,7 +28,7 @@ import spock.lang.Shared @Prefix("ssc.activity-feed") @FcliSession(SSC) @Requires({System.getProperty('ft.include.long-running')}) class SSCSystemStateActivitySpec extends FcliBaseSpec { - @Shared @AutoCleanup SSCAppVersionSupplier versionSupplier = new SSCAppVersionSupplier() + @Global(SSCAppVersionSupplier.Empty.class) SSCAppVersionSupplier versionSupplier; def "list"() { def args = "ssc state list-activities" diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateEventSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateEventSpec.groovy index a1e8fd3be9..5cd1512e2b 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateEventSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateEventSpec.groovy @@ -12,11 +12,12 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec import com.fortify.cli.ftest._common.spec.FcliSession +import com.fortify.cli.ftest._common.spec.Global import com.fortify.cli.ftest._common.spec.Prefix import com.fortify.cli.ftest.ssc._common.SSCAppVersionSupplier @@ -27,7 +28,7 @@ import spock.lang.Shared @Prefix("ssc.event") @FcliSession(SSC) @Requires({System.getProperty('ft.include.long-running')}) class SSCSystemStateEventSpec extends FcliBaseSpec { - @Shared @AutoCleanup SSCAppVersionSupplier versionSupplier = new SSCAppVersionSupplier() + @Global(SSCAppVersionSupplier.Empty.class) SSCAppVersionSupplier versionSupplier; def "list"() { def args = "ssc state list-events" diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateJobSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateJobSpec.groovy index 4a2310e0c8..c706fa32eb 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateJobSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCSystemStateJobSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.ssc -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/_common/SSCAppVersionSupplier.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/_common/SSCAppVersionSupplier.groovy index b0116b6d64..d72d0ca088 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/_common/SSCAppVersionSupplier.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/_common/SSCAppVersionSupplier.groovy @@ -12,15 +12,28 @@ */ package com.fortify.cli.ftest.ssc._common +import java.nio.file.Path + import com.fortify.cli.ftest._common.Fcli -import com.fortify.cli.ftest.ssc._common.SSCLocalUserSupplier.SSCUser +import com.fortify.cli.ftest._common.spec.Global.IGlobalValueSupplier +import com.fortify.cli.ftest._common.util.WorkDirHelper public class SSCAppVersionSupplier implements Closeable, AutoCloseable { + private final Closure init; private SSCAppVersion version; + public SSCAppVersionSupplier() { + this({}); + } + + public SSCAppVersionSupplier(Closure init) { + this.init = init; + } + public SSCAppVersion getVersion() { if ( !version ) { version = new SSCAppVersion().create() + init(version); } return version } @@ -33,9 +46,7 @@ public class SSCAppVersionSupplier implements Closeable, AutoCloseable { } } - - - public class SSCAppVersion { + public static class SSCAppVersion { private final String random = System.currentTimeMillis() private final String fcliVariableName = "ssc_appversion_"+random private final String appName = "fcli-"+random @@ -69,6 +80,48 @@ public class SSCAppVersionSupplier implements Closeable, AutoCloseable { } } + private static abstract class AppVersionGlobalValueSupplier implements IGlobalValueSupplier { + private SSCAppVersionSupplier versionSupplier; + public final SSCAppVersionSupplier getValue(WorkDirHelper workDirHelper) { + if ( !versionSupplier ) { + versionSupplier = new SSCAppVersionSupplier({ SSCAppVersion v -> initVersion(workDirHelper, v) }); + } + return versionSupplier + } + @Override + public final void close() { + if ( versionSupplier ) { + versionSupplier.close(); + versionSupplier = null; + } + } + protected String upload(SSCAppVersion version, Path fprPath) { + def varName = "global${this.class.simpleName}Fpr" + Fcli.run("ssc artifact upload -f $fprPath --appversion ${version.variableRef} --store ${varName}") + Fcli.run("ssc artifact wait-for ::${varName}:: -i 2s") + return varName; + } + protected abstract void initVersion(WorkDirHelper workDirHelper, SSCAppVersion version); + } + + public static class Empty extends AppVersionGlobalValueSupplier { + @Override + protected void initVersion(WorkDirHelper workDirHelper, SSCAppVersion version) {} + } + + public static class EightBall extends AppVersionGlobalValueSupplier { + @Override + protected void initVersion(WorkDirHelper workDirHelper, SSCAppVersion version) { + upload(version, workDirHelper.getResource("runtime/shared/EightBall-22.1.0.fpr")); + } + } + + public static class LoginProject extends AppVersionGlobalValueSupplier { + @Override + protected void initVersion(WorkDirHelper workDirHelper, SSCAppVersion version) { + upload(version, workDirHelper.getResource("runtime/shared/LoginProject.fpr")); + } + } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolBugTrackerUtilitySpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolBugTrackerUtilitySpec.groovy index 228f5a0d34..ea86326fb1 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolBugTrackerUtilitySpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolBugTrackerUtilitySpec.groovy @@ -30,7 +30,7 @@ class ToolBugTrackerUtilitySpec extends FcliBaseSpec { @Shared Path globalBinScript = Path.of(baseDir).resolve("bin/FortifyBugTrackerUtility.bat"); @Shared Path binScript = Path.of(baseDir).resolve("bugtracker-utility/${version}/bin/FortifyBugTrackerUtility.bat"); def "install"() { - def args = "tool bugtracker-utility install -y -v=${version} --progress=none -b ${baseDir}" + def args = "tool bugtracker-utility install -y -v=${version} -b ${baseDir} --progress none" when: def result = Fcli.run(args, {it.expectZeroExitCode()}) then: @@ -56,7 +56,7 @@ class ToolBugTrackerUtilitySpec extends FcliBaseSpec { } def "uninstall"() { - def args = "tool bugtracker-utility uninstall -y --progress=none -v=${version}" + def args = "tool bugtracker-utility uninstall -y -v=${version} --progress none" when: def result = Fcli.run(args) then: diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolDebrickedSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolDebrickedSpec.groovy index 5fa5d0201f..095e023231 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolDebrickedSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolDebrickedSpec.groovy @@ -31,7 +31,7 @@ class ToolDebrickedSpec extends FcliBaseSpec { @Shared Path binScript = Path.of(baseDir).resolve("debricked-cli/${version}/bin/debricked.exe"); def "install"() { - def args = "tool debricked-cli install -y -v=${version} --progress=none -b ${baseDir} --platform windows/x64" + def args = "tool debricked-cli install -y -v=${version} -b ${baseDir} --platform windows/x64 --progress none" when: def result = Fcli.run(args, {it.expectZeroExitCode()}) then: @@ -57,7 +57,7 @@ class ToolDebrickedSpec extends FcliBaseSpec { } def "uninstall"() { - def args = "tool debricked-cli uninstall -y --progress=none -v=${version}" + def args = "tool debricked-cli uninstall -y -v=${version} --progress none" when: def result = Fcli.run(args) then: diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolDefinitionsSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolDefinitionsSpec.groovy index 232dc4e273..7644e0426c 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolDefinitionsSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolDefinitionsSpec.groovy @@ -12,7 +12,7 @@ */ package com.fortify.cli.ftest.tool -import static com.fortify.cli.ftest._common.spec.FcliSessionType.SSC +import static com.fortify.cli.ftest._common.spec.FcliSession.FcliSessionType.SSC import com.fortify.cli.ftest._common.Fcli import com.fortify.cli.ftest._common.spec.FcliBaseSpec diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolFcliSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolFcliSpec.groovy index 8ef958c4b7..0bcdab8793 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolFcliSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolFcliSpec.groovy @@ -31,7 +31,7 @@ class ToolFcliSpec extends FcliBaseSpec { @Shared Path binScript = Path.of(baseDir).resolve("fcli/${version}/bin/fcli.exe"); def "install"() { - def args = "tool fcli install -y -v=${version} --progress=none -b ${baseDir} --platform windows/x64" + def args = "tool fcli install -y -v=${version} -b ${baseDir} --platform windows/x64 --progress none" when: def result = Fcli.run(args, {it.expectZeroExitCode()}) then: @@ -57,7 +57,7 @@ class ToolFcliSpec extends FcliBaseSpec { } def "uninstall"() { - def args = "tool fcli uninstall -y --progress=none -v=${version}" + def args = "tool fcli uninstall -y -v=${version} --progress none" when: def result = Fcli.run(args) then: diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolFoDUploaderSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolFoDUploaderSpec.groovy index b3fc99e638..6a6f032e5c 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolFoDUploaderSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolFoDUploaderSpec.groovy @@ -31,7 +31,7 @@ class ToolFoDUploaderSpec extends FcliBaseSpec { @Shared Path binScript = Path.of(baseDir).resolve("fod-uploader/${version}/bin/FoDUpload.bat"); def "installLatest"() { - def args = "tool fod-uploader install -y -v=${version} --progress=none -b ${baseDir}" + def args = "tool fod-uploader install -y -v=${version} -b ${baseDir} --progress none" when: def result = Fcli.run(args, {it.expectZeroExitCode()}) then: @@ -57,7 +57,7 @@ class ToolFoDUploaderSpec extends FcliBaseSpec { } def "uninstall"() { - def args = "tool fod-uploader uninstall -y --progress=none -v=${version}" + def args = "tool fod-uploader uninstall -y -v=${version} --progress none" when: def result = Fcli.run(args) then: @@ -79,8 +79,10 @@ class ToolFoDUploaderSpec extends FcliBaseSpec { verifyAll(result.stdout) { size()>0 it[0].replace(' ', '').equals("NameVersionAliasesStableInstalldirAction") - it[1].contains("5.4.0") - it[1].contains(" INSTALLED") + it.any { + it.contains("5.4.0") + it.contains(" INSTALLED") + } } } diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolScClientSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolScClientSpec.groovy index b2ca6b3e2a..2135966829 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolScClientSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolScClientSpec.groovy @@ -31,7 +31,7 @@ class ToolScClientSpec extends FcliBaseSpec { @Shared Path binScript = Path.of(baseDir).resolve("sc-client/${version}/bin/scancentral.bat") def "install"() { - def args = "tool sc-client install -y -v=${version} --progress=none -b ${baseDir}" + def args = "tool sc-client install -y -v=${version} -b ${baseDir} --progress none" when: def result = Fcli.run(args, {it.expectZeroExitCode()}) then: @@ -57,7 +57,7 @@ class ToolScClientSpec extends FcliBaseSpec { } def "uninstall"() { - def args = "tool sc-client uninstall -y --progress=none -v=${version}" + def args = "tool sc-client uninstall -y -v=${version} --progress none" when: def result = Fcli.run(args) then: diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolVulnExporterSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolVulnExporterSpec.groovy index 6eb717504c..e06bad8672 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolVulnExporterSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/tool/ToolVulnExporterSpec.groovy @@ -31,7 +31,7 @@ class ToolVulnExporterSpec extends FcliBaseSpec { @Shared Path binScript = Path.of(baseDir).resolve("vuln-exporter/${version}/bin/FortifyVulnerabilityExporter.bat"); def "install"() { - def args = "tool vuln-exporter install -y -v=${version} --progress=none -b ${baseDir}" + def args = "tool vuln-exporter install -y -v=${version} -b ${baseDir} --progress none" when: def result = Fcli.run(args, {it.expectZeroExitCode()}) then: @@ -57,7 +57,7 @@ class ToolVulnExporterSpec extends FcliBaseSpec { } def "uninstall"() { - def args = "tool vuln-exporter uninstall -y --progress=none -v=${version}" + def args = "tool vuln-exporter uninstall -y -v=${version} --progress none" when: def result = Fcli.run(args) then: diff --git a/fcli-other/fcli-functional-test/src/ftest/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension b/fcli-other/fcli-functional-test/src/ftest/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension index a2c482bbb2..2cde83f43f 100644 --- a/fcli-other/fcli-functional-test/src/ftest/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension +++ b/fcli-other/fcli-functional-test/src/ftest/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension @@ -1,5 +1 @@ -com.fortify.cli.ftest._common.extension.FcliInitializerExtension -com.fortify.cli.ftest._common.extension.TestResourceExtension -com.fortify.cli.ftest._common.extension.TempPathExtension -com.fortify.cli.ftest._common.extension.DisplayNameExtension -com.fortify.cli.ftest._common.extension.SkipFeaturesExtension \ No newline at end of file +com.fortify.cli.ftest._common.extension.FcliGlobalExtension \ No newline at end of file diff --git a/fcli-other/fcli-gradle/fcli-java.gradle b/fcli-other/fcli-gradle/fcli-java.gradle index ee1eb57ca8..18adaafd5d 100644 --- a/fcli-other/fcli-gradle/fcli-java.gradle +++ b/fcli-other/fcli-gradle/fcli-java.gradle @@ -7,6 +7,24 @@ java { targetCompatibility = JavaVersion.toVersion("17") } +/* +eclipse { + classpath { + file.whenMerged { cp -> + cp.entries.add( new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/main/zipped-resources', null) ) + } + } +} +*/ +sourceSets { + main { + resources { + exclude '**/zip/*' + exclude '**/zip' + } + } +} + dependencies { implementation platform(project("${fcliBomRef}")) annotationProcessor platform(project("${fcliBomRef}")) @@ -60,8 +78,11 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - //required for unpacking tar.gz (debricked cli) - implementation('org.apache.commons:commons-compress') + // Required for unpacking tar.gz (debricked cli) + implementation('org.apache.commons:commons-compress') + + // Used for processing HTML text returned by SSC/FoD endpoints like issue summaries/details/... + implementation('org.jsoup:jsoup') } compileJava { @@ -81,16 +102,42 @@ test { showStandardStreams = true } } + +ext.generatedZipResourcesDir = "${buildDir}/generated-zip-resources" +/* For some reason the below doesn't work, probably related to https://github.com/gradle/gradle/issues/24368 + * For now, we just have dedicated zip tasks in SSC/FoD build.gradle files +fileTree(dir: 'src/main/zipped-resources') + .visit { e -> if ( e.isDirectory() && e.name=='zip' ) { + def zipTaskName = "zipResources_${e.path.replaceAll('/', '_')}" + def destDir = "$generatedZipResourcesDir/${e.relativePath}" + def zipName = "${e.relativePath.parent.lastName}.zip" + def from = "${e.file.absolutePath}" + println "zipTaskName: $zipTaskName" + println "destDir: $destDir" + println "zipName: $zipName" + println "from: $from" + tasks.create(name: "$zipTaskName", type: Zip) { + destinationDirectory = file("$destDir") + archiveFileName = "$zipName" + from = file("$from") + } + }} +*/ +task generateZipResources(dependsOn: tasks.matching { Task task -> task.name.startsWith("zipResources_")}) +sourceSets.main.output.dir generatedZipResourcesDir, builtBy: generateZipResources // Generate resource-config.json ext.generatedResourceConfigDir = "${buildDir}/generated-resource-config" tasks.register('generateResourceConfig') { + dependsOn = [generateZipResources] doLast { def outputDir = "${generatedResourceConfigDir}/META-INF/native-image/fcli-generated/${project.name}"; mkdir "${outputDir}" def entries = []; - fileTree(dir: 'src/main/resources', excludes: ['**/i18n/**', 'META-INF/**']) + fileTree(dir: 'src/main/resources', excludes: ['**/i18n/**', 'META-INF/**', '**/zip/**']) .visit {e -> if ( !e.isDirectory() ) {entries << '\n {"pattern":"'+e.relativePath+'"}'}}; + fileTree(dir: generatedZipResourcesDir) + .visit {e -> if ( !e.isDirectory() ) {entries << '\n {"pattern":"'+e.relativePath+'"}'}}; if ( entries.size>0 ) { def contents = '{"resources":[' + entries.join(",") + '\n]}'; file("${outputDir}/resource-config.json").text = contents; diff --git a/gradle.properties b/gradle.properties index 73f8d647be..9bdd4209b2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,4 +36,18 @@ fcliRootCommandsClassName=com.fortify.cli.app._main.cli.cmd.FCLIRootCommands // Define the main class name for running fcli. // FortifyCLITest checks that this property contains a valid class name. -fcliMainClassName=com.fortify.cli.app.FortifyCLI \ No newline at end of file +fcliMainClassName=com.fortify.cli.app.FortifyCLI + +// Define fcli action schema version. This must be manually maintained, and must +// be updated whenever the action schema model is changed: +// - Increase patch version for non-structural changes, like description updates. +// - Increase minor version for backward-compatible structural changes, like +// adding new (optional) step types or adding new optional properties to existing +// types. +// - Increase major version for non-backward-compatible changes, like adding new +// required properties, or changing the meaning/value type of an existing property. +// To allow for proper detection of whether a given fcli version is compatible with a +// given schema version, it is very important to maintain this correctly. At all cost, +// we should avoid for example updating only patch version if there are any structural +// changes. +fcliActionSchemaVersion=1.0.0 diff --git a/settings.gradle b/settings.gradle index 5153ec7544..8360d10f8e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,4 +13,4 @@ include "$fcliUtilRef" include "$fcliAppRef" include "$fcliFunctionalTestRef" include "$fcliAutoCompleteRef" -include "$fcliDocRef" \ No newline at end of file +include "$fcliDocRef"