diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 889ca9e978..3de154b732 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,12 +26,11 @@ jobs: - name: PROD - Prepare GitHub release id: create_prod_release uses: GoogleCloudPlatform/release-please-action@v3 - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/v2.x' with: command: github-release release-type: simple package-name: ${{ github.event.repository.name }} - default-branch: main - name: PROD - Define release info if: steps.create_prod_release.outputs.release_created @@ -266,13 +265,12 @@ jobs: name: combined-artifacts - name: PROD - Prepare release PR - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/v2.x' uses: GoogleCloudPlatform/release-please-action@v3 with: command: release-pr release-type: simple package-name: ${{ github.event.repository.name }} - default-branch: main - name: DEV - Prepare GitHub release if: needs.build.outputs.do_dev_release @@ -322,7 +320,7 @@ jobs: rm -rf docs/.git* # Extract top-level documentation resources - # TODO Should we do this only when building a release, or also for dev_main, + # TODO Should we do this only when building a release, or also for dev_v2.x, # or for all (dev & release) versions like we do now? unzip -o artifacts/docs-gh-pages.zip -d "docs" 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 index c7fcc206a0..652530708e 100644 --- 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 @@ -29,6 +29,7 @@ import com.fortify.cli.common.progress.helper.ProgressWriterType; import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.DisableTest.TestType; +import com.fortify.cli.common.util.EnvHelper; import lombok.SneakyThrows; import picocli.CommandLine.Mixin; @@ -68,7 +69,26 @@ public final Integer 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); + // We need to set the FCLI_DEFAULT__SESSION environment variable to allow fcli: statements to + // pick up the current session name, and (although probably not needed currently), reset the default + // session name to the previous value once the action completes. + var sessionEnvName = String.format("%s_%s_SESSION", System.getProperty("fcli.env.default.prefix", "FCLI_DEFAULT"), getType().toUpperCase()); + var sessionPropertyName = EnvHelper.envSystemPropertyName(sessionEnvName); + var sessionEnvOrgValue = EnvHelper.env(sessionEnvName); + try { + setOrClearSystemProperty(sessionPropertyName, getSessionName()); + return actionRunner.run(actionArgs); + } finally { + setOrClearSystemProperty(sessionPropertyName, sessionEnvOrgValue); + } + } + + private void setOrClearSystemProperty(String name, String value) { + if ( value==null ) { + System.clearProperty(name); + } else { + System.setProperty(name, value); + } } private ParameterException onValidationErrors(OptionsParseResult optionsParseResult) { @@ -79,5 +99,6 @@ private ParameterException onValidationErrors(OptionsParseResult optionsParseRes } protected abstract String getType(); + protected abstract String getSessionName(); protected abstract void configure(ActionRunner actionRunner, SimpleEvaluationContext context); } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/import_debricked/SSCArtifactImportDebrickedCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/import_debricked/DebrickedHelper.java similarity index 62% rename from fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/import_debricked/SSCArtifactImportDebrickedCommand.java rename to fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/import_debricked/DebrickedHelper.java index d6dbd9a48a..ebc957974f 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/import_debricked/SSCArtifactImportDebrickedCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/import_debricked/DebrickedHelper.java @@ -10,7 +10,7 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.ssc.artifact.cli.cmd.import_debricked; +package com.fortify.cli.common.cli.cmd.import_debricked; import java.io.File; import java.nio.file.StandardCopyOption; @@ -24,85 +24,35 @@ import com.fasterxml.jackson.databind.util.RawValue; import com.fortify.cli.common.http.proxy.helper.ProxyHelper; import com.fortify.cli.common.json.JsonHelper; -import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; -import com.fortify.cli.common.progress.helper.IProgressWriterI18n; -import com.fortify.cli.common.rest.unirest.GenericUnirestFactory; 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; import com.fortify.cli.common.util.StringUtils; -import com.fortify.cli.ssc.artifact.cli.cmd.AbstractSSCArtifactUploadCommand; -import com.fortify.cli.ssc.artifact.cli.cmd.import_debricked.DebrickedLoginOptions.DebrickedAccessTokenCredentialOptions; -import com.fortify.cli.ssc.artifact.cli.cmd.import_debricked.DebrickedLoginOptions.DebrickedAuthOptions; -import com.fortify.cli.ssc.artifact.cli.cmd.import_debricked.DebrickedLoginOptions.DebrickedUserCredentialOptions; +import com.fortify.cli.common.cli.cmd.import_debricked.DebrickedLoginOptions.DebrickedAccessTokenCredentialOptions; +import com.fortify.cli.common.cli.cmd.import_debricked.DebrickedLoginOptions.DebrickedAuthOptions; +import com.fortify.cli.common.cli.cmd.import_debricked.DebrickedLoginOptions.DebrickedUserCredentialOptions; import kong.unirest.UnirestInstance; -import lombok.Getter; import lombok.SneakyThrows; -import picocli.CommandLine.Command; -import picocli.CommandLine.Mixin; -import picocli.CommandLine.Option; - -@Command(name = "import-debricked") -public class SSCArtifactImportDebrickedCommand extends AbstractSSCArtifactUploadCommand { - @Mixin @Getter private OutputHelperMixins.TableNoQuery outputHelper; - @Mixin private DebrickedLoginOptions debrickedLoginOptions; - - @Option(names = {"-e", "--engine-type"}, required = true, defaultValue = "DEBRICKED") - @Getter private String engineType; - - @Option(names = {"-f", "--save-sbom-as"}, required = false) - private String fileName; - - @Option(names = {"-r", "--repository"}, required = true) - private String repository; - - @Option(names = {"-b", "--branch"}, required = true) - private String branch; - - @Override - public boolean isSingular() { - return true; - } - - @Override @SneakyThrows - protected File getFile() { - File sbomFile = null; - if ( StringUtils.isNotBlank(fileName) ) { - sbomFile = new File(fileName); - } else { - sbomFile = File.createTempFile("debricked", ".json"); - sbomFile.deleteOnExit(); - } - return sbomFile; - } - - @Override - protected void preUpload(UnirestInstance unirest, IProgressWriterI18n progressWriter, File file) { - progressWriter.writeProgress("Status: Generating & downloading SBOM"); - try ( var debrickedUnirest = GenericUnirestFactory.createUnirestInstance() ) { - downloadSbom(debrickedUnirest, file); - } - progressWriter.writeProgress("Status: Uploading SBOM to SSC"); - } - - @Override - protected void postUpload(UnirestInstance unirest, IProgressWriterI18n progressWriter, File file) { - if ( StringUtils.isBlank(fileName) ) { - file.delete(); - } - progressWriter.writeProgress("Status: SBOM uploaded to SSC"); - progressWriter.clearProgress(); - } +public final class DebrickedHelper { - private Void downloadSbom(UnirestInstance debrickedUnirest, File file) { + private DebrickedLoginOptions debrickedLoginOptions; + private String repository; + private String branch; + + public DebrickedHelper(DebrickedLoginOptions debrickedLoginOptions, String repository, String branch) { + this.debrickedLoginOptions = debrickedLoginOptions; + this.repository = repository; + this.branch = branch; + } + + public final void downloadSbom(UnirestInstance debrickedUnirest, File file) { configureDebrickedUnirest(debrickedUnirest); String reportUuid = startSbomGeneration(debrickedUnirest); waitSbomGeneration(debrickedUnirest, reportUuid, file); - return null; } - private void configureDebrickedUnirest(UnirestInstance debrickedUnirest) { + public final void configureDebrickedUnirest(UnirestInstance debrickedUnirest) { UnirestUnexpectedHttpResponseConfigurer.configure(debrickedUnirest); DebrickedUrlConfigOptions debrickedUrlConfig = debrickedLoginOptions.getUrlConfigOptions(); UnirestUrlConfigConfigurer.configure(debrickedUnirest, debrickedUrlConfig); @@ -113,7 +63,7 @@ private void configureDebrickedUnirest(UnirestInstance debrickedUnirest) { debrickedUnirest.config().setDefaultHeader("Authorization", authHeader); } - private String getDebrickedJwtToken(UnirestInstance debrickedUnirest) { + public final String getDebrickedJwtToken(UnirestInstance debrickedUnirest) { DebrickedAuthOptions authOptions = debrickedLoginOptions.getAuthOptions(); DebrickedUserCredentialOptions userCredentialsOptions = authOptions.getUserCredentialsOptions(); DebrickedAccessTokenCredentialOptions tokenOptions = authOptions.getTokenOptions(); @@ -126,7 +76,7 @@ private String getDebrickedJwtToken(UnirestInstance debrickedUnirest) { } } - private String getDebrickedJwtToken(UnirestInstance debrickedUnirest, DebrickedAccessTokenCredentialOptions tokenOptions) { + public final String getDebrickedJwtToken(UnirestInstance debrickedUnirest, DebrickedAccessTokenCredentialOptions tokenOptions) { return debrickedUnirest.post("/api/login_refresh") .header("Content-Type", "application/x-www-form-urlencoded") .field("refresh_token", new String(tokenOptions.getAccessToken())) @@ -136,7 +86,7 @@ private String getDebrickedJwtToken(UnirestInstance debrickedUnirest, DebrickedA .asText(); } - private String getDebrickedJwtToken(UnirestInstance debrickedUnirest, DebrickedUserCredentialOptions userCredentialsOptions) { + public final String getDebrickedJwtToken(UnirestInstance debrickedUnirest, DebrickedUserCredentialOptions userCredentialsOptions) { return debrickedUnirest.post("/api/login_check") .header("Content-Type", "application/x-www-form-urlencoded") .field("_username", userCredentialsOptions.getUser()) @@ -147,7 +97,7 @@ private String getDebrickedJwtToken(UnirestInstance debrickedUnirest, DebrickedU .asText(); } - private String getRepositoryId(UnirestInstance debrickedUnirest) { + public final String getRepositoryId(UnirestInstance debrickedUnirest) { try { Integer.parseInt(repository); return repository; @@ -166,7 +116,7 @@ private String getRepositoryId(UnirestInstance debrickedUnirest) { } } - private String startSbomGeneration(UnirestInstance debrickedUnirest) { + public final String startSbomGeneration(UnirestInstance debrickedUnirest) { ObjectNode body = new ObjectMapper().createObjectNode() // TODO generate a proper ArrayNode .putRawValue("repositoryIds", new RawValue("["+getRepositoryId(debrickedUnirest)+"]")) @@ -184,7 +134,7 @@ private String startSbomGeneration(UnirestInstance debrickedUnirest) { } @SneakyThrows - private void waitSbomGeneration(UnirestInstance debrickedUnirest, String reportUuid, File outputFile) { + public final void waitSbomGeneration(UnirestInstance debrickedUnirest, String reportUuid, File outputFile) { int status = 202; while ( status==202 ) { Thread.sleep(5000L); diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/import_debricked/DebrickedLoginOptions.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/import_debricked/DebrickedLoginOptions.java similarity index 97% rename from fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/import_debricked/DebrickedLoginOptions.java rename to fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/import_debricked/DebrickedLoginOptions.java index 7b857f1737..abcfe932a0 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/import_debricked/DebrickedLoginOptions.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/import_debricked/DebrickedLoginOptions.java @@ -10,7 +10,7 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.ssc.artifact.cli.cmd.import_debricked; +package com.fortify.cli.common.cli.cmd.import_debricked; import com.fortify.cli.common.rest.unirest.config.IUserCredentialsConfig; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/import_debricked/DebrickedUrlConfigOptions.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/import_debricked/DebrickedUrlConfigOptions.java similarity index 95% rename from fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/import_debricked/DebrickedUrlConfigOptions.java rename to fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/import_debricked/DebrickedUrlConfigOptions.java index af6e78c15d..fff8a54b97 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/import_debricked/DebrickedUrlConfigOptions.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/cmd/import_debricked/DebrickedUrlConfigOptions.java @@ -10,7 +10,7 @@ * herein. The information contained herein is subject to change * without notice. *******************************************************************************/ -package com.fortify.cli.ssc.artifact.cli.cmd.import_debricked; +package com.fortify.cli.common.cli.cmd.import_debricked; import com.fortify.cli.common.rest.cli.mixin.ConnectionConfigOptions; import com.fortify.cli.common.rest.unirest.config.IUrlConfig; 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 index d6004ab4ad..acc37cfba8 100644 --- 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 @@ -56,7 +56,7 @@ public JSONDateTimeConverter(DateTimeFormatter fmtDateTime, ZoneId defaultZoneId } public static final DateTimeFormatter createDefaultDateTimeFormatter() { - return DateTimeFormatter.ofPattern("yyyy-MM-dd[['T'][' ']HH:mm:ss[.SSS][.SS]][ZZZZ][Z][XXX][XX][X]"); + return DateTimeFormatter.ofPattern("yyyy-MM-dd[['T'][' ']HH:mm:ss[.SSS][.SS][.S]][ZZZZ][Z][XXX][XX][X]"); } @Override 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 470f8c8428..5816e5d9a3 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 @@ -13,7 +13,6 @@ package com.fortify.cli.common.progress.cli.mixin; import com.fortify.cli.common.cli.mixin.CommandHelperMixin; -import com.fortify.cli.common.progress.cli.mixin.ProgressWriterTypeConverter.ProgressWriterTypeIterable; import com.fortify.cli.common.progress.helper.IProgressWriterI18n; import com.fortify.cli.common.progress.helper.ProgressWriterI18n; import com.fortify.cli.common.progress.helper.ProgressWriterType; @@ -24,7 +23,7 @@ public class ProgressWriterFactoryMixin { @Mixin private CommandHelperMixin commandHelper; - @Getter @Option(names="--progress", defaultValue = "auto", completionCandidates = ProgressWriterTypeIterable.class, converter = ProgressWriterTypeConverter.class ) + @Getter @Option(names="--progress", defaultValue = "auto") private ProgressWriterType type; public final IProgressWriterI18n create() { diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/cli/mixin/ProgressWriterTypeConverter.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/cli/mixin/ProgressWriterTypeConverter.java deleted file mode 100644 index f23a2c0644..0000000000 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/progress/cli/mixin/ProgressWriterTypeConverter.java +++ /dev/null @@ -1,38 +0,0 @@ -/******************************************************************************* - * 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.progress.cli.mixin; - -import java.util.ArrayList; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import com.fortify.cli.common.progress.helper.ProgressWriterType; - -import picocli.CommandLine.ITypeConverter; - -public final class ProgressWriterTypeConverter implements ITypeConverter { - @Override - public ProgressWriterType convert(String value) throws Exception { - return ProgressWriterType.valueOf(value.replace('-', '_')); - } - - public static final class ProgressWriterTypeIterable extends ArrayList { - private static final long serialVersionUID = 1L; - public ProgressWriterTypeIterable() { - super(Stream.of(ProgressWriterType.values()) - .map(Enum::name) - .map(s->s.replace('_', '-')) - .collect(Collectors.toList())); - } - } -} \ No newline at end of file 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 2dd21410b6..b1f8862351 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 @@ -28,6 +28,13 @@ public enum ProgressWriterType { single_line(SingleLineProgressWriter::new), ansi(AnsiProgressWriter::new); + @Override + public String toString() { + // Show and accept dashes instead of underscores when this + // enum is used in picocli options. + return name().replace('_', '-'); + } + private final Supplier factory; public IProgressWriter create() { diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLogoutCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLogoutCommand.java index 4b1fce8637..ebba18bbdc 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLogoutCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/session/cli/cmd/AbstractSessionLogoutCommand.java @@ -19,6 +19,7 @@ import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.common.session.cli.mixin.SessionNameMixin; import com.fortify.cli.common.session.helper.ISessionDescriptor; +import com.fortify.cli.common.session.helper.SessionLogoutException; import lombok.Getter; import picocli.CommandLine.Mixin; @@ -36,11 +37,15 @@ public JsonNode getJsonNode() { result = sessionHelper.sessionSummaryAsObjectNode(sessionName); try { logout(sessionName, sessionHelper.get(sessionName, false)); - } catch (Exception e){ - LOG.warn("Logout failed"); - LOG.debug("Exception details:", e); - } finally { getSessionHelper().destroy(sessionName); + } catch (Exception e) { + if ( e instanceof SessionLogoutException && !((SessionLogoutException)e).isDestroySession() ) { + throw e; + } else { + LOG.warn("Logout failed"); + LOG.debug("Exception details:", e); + getSessionHelper().destroy(sessionName); + } } } return result; diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/session/helper/SessionLogoutException.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/session/helper/SessionLogoutException.java new file mode 100644 index 0000000000..23ced07ea9 --- /dev/null +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/session/helper/SessionLogoutException.java @@ -0,0 +1,20 @@ +package com.fortify.cli.common.session.helper; + +import lombok.Getter; + +public class SessionLogoutException extends RuntimeException { + private static final long serialVersionUID = 1L; + @Getter private boolean destroySession; + public SessionLogoutException(String message, boolean destroySession) { + this(message, null, destroySession); + } + + public SessionLogoutException(Throwable cause, boolean destroySession) { + this(null, cause, destroySession); + } + + public SessionLogoutException(String message, Throwable cause, boolean destroySession) { + super(message, cause); + this.destroySession = destroySession; + } +} diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/EnvHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/EnvHelper.java index 9e461853d8..e826e69fe4 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/EnvHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/EnvHelper.java @@ -59,7 +59,11 @@ public static final String envName(String productEnvId, String suffix) { * other purposes. */ public static final String env(String name) { - return System.getProperty("fcli.env."+name, System.getenv(name)); + return System.getProperty(envSystemPropertyName(name), System.getenv(name)); + } + + public static String envSystemPropertyName(String envName) { + return "fcli.env."+envName; } public static final Boolean asBoolean(String s) { 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 121069314e..6048f2d814 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 @@ -13,7 +13,7 @@ usage.footer.0 = %nCommands/options marked as PREVIEW are subject to change; pip may need to be updated on any fcli upgrade. usage.footer.1 = %nFull command list: fcli util all-commands list usage.footer.2 = Documentation: https://fortify.github.io/fcli -usage.footer.3 = %n(c) Copyright 2021-2023 Open Text +usage.footer.3 = %n(c) Copyright 2021-2024 Open Text usage.parameterListHeading = %nCommand parameters:%n usage.optionListHeading = %nCommand options:%n diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDFileTransferHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDFileTransferHelper.java index c08c962af7..c8a88905db 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDFileTransferHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/rest/helper/FoDFileTransferHelper.java @@ -36,7 +36,9 @@ // chunked uploads for SC DAST as well, so consider refactoring into a // generic class in fcli-common. public final class FoDFileTransferHelper { - private static final int chunkSize = FoDConstants.DEFAULT_CHUNK_SIZE; + + private static int chunkSize = FoDConstants.DEFAULT_CHUNK_SIZE; + public static void setChunkSize(int chunkSize) { FoDFileTransferHelper.chunkSize = chunkSize; } @SneakyThrows public static final JsonNode upload(UnirestInstance unirest, HttpRequest baseRequest, File f) { @@ -64,7 +66,6 @@ public static final JsonNode uploadChunked(UnirestInstance unirest, HttpRequest< throw new IllegalArgumentException("Could not read file: " + f.getPath()); } long fileLen = f.length(); - String lastBody = null; try (var fs = new FileInputStream(f); var progressMonitor = new FoDProgressMonitor("Upload"); ) { byte[] readByteArray = new byte[chunkSize]; 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 e011f6feae..53b50e1f7d 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 @@ -13,11 +13,15 @@ package com.fortify.cli.fod._common.scan.cli.cmd; +import java.io.File; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.cli.mixin.CommonOptionMixins; import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; +import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin; +import com.fortify.cli.common.progress.helper.IProgressWriterI18n; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.rest.FoDUrls; @@ -28,26 +32,38 @@ import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; @CommandGroup("*-scan-import") public abstract class AbstractFoDScanImportCommand extends AbstractFoDJsonNodeOutputCommand implements IActionCommandResultSupplier { @Mixin private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins @Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver; + @Mixin private ProgressWriterFactoryMixin progressWriterFactory; @Mixin private CommonOptionMixins.RequiredFile scanFileMixin; - + + @Option(names="--chunk-size", descriptionKey = "fcli.fod.scan.chunk-size", required = false) + private Integer chunkSize; + @Override public final JsonNode getJsonNode(UnirestInstance unirest) { - var releaseDescriptor = releaseResolver.getReleaseDescriptor(unirest); - var releaseId = releaseDescriptor.getReleaseId(); - var importScanSessionId = getImportScanSessionId(unirest, releaseId); - HttpRequest baseRequest = getBaseRequest(unirest, releaseId) - .queryString("importScanSessionId", importScanSessionId) - .queryString("fileLength", scanFileMixin.getFile().length()); - FoDFileTransferHelper.uploadChunked(unirest, baseRequest, scanFileMixin.getFile()); - return releaseDescriptor.asObjectNode() - .put("importScanSessionId", importScanSessionId) - .put("scanType", getScanType().name()); + try ( var progressWriter = progressWriterFactory.create() ) { + + var releaseDescriptor = releaseResolver.getReleaseDescriptor(unirest); + var releaseId = releaseDescriptor.getReleaseId(); + File file = scanFileMixin.getFile(); + if (chunkSize != null && chunkSize > 0) FoDFileTransferHelper.setChunkSize(chunkSize); + preUpload(unirest, progressWriter, file); + var importScanSessionId = getImportScanSessionId(unirest, releaseId); + HttpRequest baseRequest = getBaseRequest(unirest, releaseId) + .queryString("importScanSessionId", importScanSessionId) + .queryString("fileLength", file.length()); + FoDFileTransferHelper.uploadChunked(unirest, baseRequest, file); + postUpload(unirest, progressWriter, file); + return releaseDescriptor.asObjectNode() + .put("importScanSessionId", importScanSessionId) + .put("scanType", getScanType().name()); + } } protected abstract HttpRequest getBaseRequest(UnirestInstance unirest, String releaseId); @@ -64,6 +80,10 @@ public final boolean isSingular() { return true; } + //protected File getFile() { return scanFileMixin.getFile(); }; + protected void preUpload(UnirestInstance unirest, IProgressWriterI18n progressWriter, File file) {} + protected void postUpload(UnirestInstance unirest, IProgressWriterI18n progressWriter, File file) {} + private static final String getImportScanSessionId(UnirestInstance unirest, String relId) { return unirest.get(FoDUrls.RELEASE_IMPORT_SCAN_SESSION) .routeParam("relId", relId) 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 26266c32eb..87cf88220e 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 @@ -57,7 +57,10 @@ public String getScanId(UnirestInstance unirest) { } public final String getDelimiter() { - return delimiterMixin.getDelimiter(); + if( delimiterMixin!=null ) { + return delimiterMixin.getDelimiter(); + } + return null; } } @@ -95,7 +98,7 @@ public static class RequiredOption extends AbstractFoDScanResolverMixin { } public static class RequiredOptionMulti extends AbstractFoDMultiScanResolverMixin { - @EnvSuffix("SCANS") @Option(names = {"--scans"}, required=true, split=",", descriptionKey = "fcli.fod.scan.scan-id") + @EnvSuffix("SCANS") @Option(names = {"--scans"}, required=true, split=",", descriptionKey = "fcli.fod.scan.scan-ids") @Getter private String[] releaseQualifiedScanOrIds; } @@ -105,7 +108,7 @@ public static class PositionalParameter extends AbstractFoDScanResolverMixin { } public static class PositionalParameterMulti extends AbstractFoDMultiScanResolverMixin { - @EnvSuffix("SCANS") @Parameters(index = "0", arity = "1..", paramLabel = "scan-id's", descriptionKey = "fcli.fod.scan.scan-id") + @EnvSuffix("SCANS") @Parameters(index = "0", arity = "1..", paramLabel = "scan-id's", descriptionKey = "fcli.fod.scan.scan-ids") @Getter private String[] releaseQualifiedScanOrIds; } 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 8b9ee3a72e..da64cf39bf 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 @@ -70,7 +70,7 @@ public static final JsonNode renameFields(JsonNode record, FoDScanType scanType) return obj; } public static final FoDScanDescriptor getScanDescriptor(UnirestInstance unirest, String releaseQualifiedScanOrId, String delimiter) { - String[] elts = releaseQualifiedScanOrId.split(delimiter); + String[] elts = (delimiter != null) ? releaseQualifiedScanOrId.split(delimiter) : new String[]{releaseQualifiedScanOrId}; switch (elts.length) { case 2: var pollingResult = unirest.get(FoDUrls.SCAN_POLLING_SUMMARY) 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 index ae15d3d104..a8e04c4837 100644 --- 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 @@ -42,6 +42,11 @@ protected final String getType() { return "FoD"; } + @Override + protected String getSessionName() { + return unirestInstanceSupplier.getSessionName(); + } + @Override protected void configure(ActionRunner templateRunner, SimpleEvaluationContext context) { templateRunner diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java index cd7fa22256..cf2afa8d18 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/cmd/FoDIssueListCommand.java @@ -20,6 +20,7 @@ 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.issue.cli.mixin.FoDIssueIncludeMixin; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import kong.unirest.HttpRequest; @@ -35,6 +36,7 @@ public class FoDIssueListCommand extends AbstractFoDBaseRequestOutputCommand imp @Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver; @Mixin private FoDFiltersParamMixin filterParamMixin; @Mixin private FoDIssueEmbedMixin embedMixin; + @Mixin private FoDIssueIncludeMixin includeMixin; @Getter private IServerSideQueryParamValueGenerator serverSideQueryParamGenerator = new FoDFiltersParamGenerator(); // .add("id","applicationId") // .add("name","applicationName") diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/mixin/FoDIssueIncludeMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/mixin/FoDIssueIncludeMixin.java new file mode 100644 index 0000000000..5254b91cf6 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/issue/cli/mixin/FoDIssueIncludeMixin.java @@ -0,0 +1,84 @@ +package com.fortify.cli.fod.issue.cli.mixin; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.output.transform.IRecordTransformer; +import com.fortify.cli.common.rest.unirest.IHttpRequestUpdater; +import com.fortify.cli.common.util.DisableTest; +import com.fortify.cli.common.util.DisableTest.TestType; + +import kong.unirest.HttpRequest; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import picocli.CommandLine.Option; + +public class FoDIssueIncludeMixin implements IHttpRequestUpdater, IRecordTransformer { + @DisableTest(TestType.MULTI_OPT_PLURAL_NAME) + @Option(names = {"--include", "-i"}, split = ",", defaultValue = "visible", descriptionKey = "fcli.fod.issue.list.includeIssue", paramLabel="") + private Set includes; + + public HttpRequest updateRequest(HttpRequest request) { + // TODO Potentially if 'includes' contains ONLY suppressed, we could also + // add filters=isSuppressed:true to perform server-side filtering + // (note that FoD doesn't allow for server-side filtering on closedStatus, + // so we can't do the same for fixed issues). + // However, we'd need to check how to integrate this with any + // FoDFiltersParamGenerator defined on the command, i.e., how FoD + // reacts if there are multiple 'filters' parameters, and/or do + // some refactoring to generate only a single 'filters' parameter + // from multiple sources. Alternatively, instead of directly generating + // a 'filters' parameter, we could potentially amend the client-side + // query, which would then be picked up by any FoDFiltersParamGenerator, + // and also remove the need for having an explicit transformRecord() + // method to perform client-side filtering. + if ( includes!=null ) { + for ( var include : includes) { + var queryParameterName = include.getRequestParameterName(); + if ( queryParameterName!=null ) { + request = request.queryString(queryParameterName, "true"); + } + } + } + return request; + } + + @Override + public JsonNode transformRecord(JsonNode record) { + // If includes doesn't include 'visible', we return null for any visible (non-suppressed + // & non-fixed) issues. We don't need explicit handling for other cases, as suppressed or + // fixed issues won't be returned by FoD if not explicitly specified through the --include + // option. + return !includes.contains(FoDIssueInclude.visible) + && JsonHelper.evaluateSpelExpression(record, "!isSuppressed && !closedStatus", Boolean.class) + ? null + : addVisibilityProperties((ObjectNode)record); + } + + private ObjectNode addVisibilityProperties(ObjectNode record) { + Map visibility = new LinkedHashMap<>(); + if ( getBoolean(record, "isSuppressed") ) { visibility.put("suppressed", "(S)"); } + if ( getBoolean(record, "closedStatus") ) { visibility.put("fixed", "(F)"); } + if ( visibility.isEmpty() ) { visibility.put("visible", " "); } + record.put("visibility", String.join(",", visibility.keySet())) + .put("visibilityMarker", String.join("", visibility.values())); + return record; + } + + private boolean getBoolean(ObjectNode record, String propertyName) { + var field = record.get(propertyName); + return field==null ? false : field.asBoolean(); + } + + @RequiredArgsConstructor + public static enum FoDIssueInclude { + visible(null), fixed("includeFixed"), suppressed("includeSuppressed"); + + @Getter + private final String requestParameterName; + } +} 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 c382394497..cce9a49e6f 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 @@ -24,6 +24,7 @@ FoDOssScanDownloadLatestCommand.class, FoDOssScanGetCommand.class, FoDOssScanImportCommand.class, + FoDOssScanImportDebrickedCommand.class, FoDOssScanListCommand.class, //FoDOssScanGetConfigCommand.class, //FoDOssScanSetupCommand.class, diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanImportDebrickedCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanImportDebrickedCommand.java new file mode 100644 index 0000000000..e2886ca2cc --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/oss_scan/cli/cmd/FoDOssScanImportDebrickedCommand.java @@ -0,0 +1,96 @@ +/** + * 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.oss_scan.cli.cmd; + +import java.io.File; +import java.util.function.BiFunction; + +import com.fortify.cli.common.cli.cmd.import_debricked.DebrickedHelper; +import com.fortify.cli.common.cli.cmd.import_debricked.DebrickedLoginOptions; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.progress.helper.IProgressWriterI18n; +import com.fortify.cli.common.rest.unirest.GenericUnirestFactory; +import com.fortify.cli.fod._common.rest.FoDUrls; +import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanImportCommand; +import com.fortify.cli.fod._common.scan.helper.FoDScanType; + +import kong.unirest.HttpRequest; +import kong.unirest.UnirestInstance; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command(name = "import-debricked") +public class FoDOssScanImportDebrickedCommand extends AbstractFoDScanImportCommand { + @Getter @Mixin private OutputHelperMixins.Import outputHelper; + @Mixin private DebrickedLoginOptions debrickedLoginOptions; + + //@Option(names = {"-f", "--save-sbom-as"}, required = false, defaultValue = "sbom.json") + //private String fileName; + + @Option(names = {"-r", "--repository"}, required = true) + private String repository; + + @Option(names = {"-b", "--branch"}, required = true) + private String branch; + + @Option(names="--type", required = true, defaultValue = "CycloneDX") + private FoDScanImportOpenSourceType type; + + @Override + protected HttpRequest getBaseRequest(UnirestInstance unirest, String releaseId) { + return type.getBaseRequest(unirest, releaseId); + } + + @Override + protected FoDScanType getScanType() { + return FoDScanType.OpenSource; + } + + @Override + protected void preUpload(UnirestInstance unirest, IProgressWriterI18n progressWriter, File file) { + DebrickedHelper debrickedHelper = new DebrickedHelper(debrickedLoginOptions, repository, branch); + progressWriter.writeProgress("Status: Generating & downloading SBOM"); + try ( var debrickedUnirest = GenericUnirestFactory.createUnirestInstance() ) { + debrickedHelper.downloadSbom(debrickedUnirest, file); + } + progressWriter.writeProgress("Status: Uploading SBOM to FoD"); + } + + @Override + protected void postUpload(UnirestInstance unirest, IProgressWriterI18n progressWriter, File file) { + //if ( StringUtils.isBlank(fileName) ) { + // file.delete(); + //} + progressWriter.writeProgress("Status: SBOM uploaded to FoD"); + progressWriter.clearProgress(); + } + + @RequiredArgsConstructor + public static enum FoDScanImportOpenSourceType { + + CycloneDX(FoDScanImportOpenSourceType::getBaseRequestCycloneDX); + + private final BiFunction> f; + + public HttpRequest getBaseRequest(UnirestInstance unirest, String releaseId) { + return f.apply(unirest, releaseId); + } + + private static final HttpRequest getBaseRequestCycloneDX(UnirestInstance unirest, String releaseId) { + return unirest.put(FoDUrls.RELEASE_IMPORT_CYCLONEDX_SBOM).routeParam("relId", releaseId); + } + } +} 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 index 1be7ae9e36..44ab2514af 100644 --- 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 @@ -30,6 +30,53 @@ steps: # Define output date format - name: dateFmt value: YYYY-MM-dd HH:mm + # Note: change/remove the following when Open Source counts are available on the release object + - progress: Loading Scans + - requests: + - name: scans + # we can't filter on scan type so we have to trawl through in to find "latest" open source scan :( + uri: /api/v3/releases/${r.releaseId}/scans?limit=50 + query: + orderBy: completedDateTime + orderByDirection: DESC + type: paged + forEach: + name: scan + breakIf: ${ossScanDate!=null} + if: ${scan.scanType=='OpenSource'} + do: + - set: + - name: scanType + value: ${scan.scanType} + - name: ossScanDate + value: ${scan.completedDateTime} + #- write: + # - to: stdout + # value: ${scanType} - ${ossScanDate} + - progress: Loading Vulnerabilities + - requests: + - name: issues + if: ${ossScanDate!=null} + uri: /api/v3/releases/${r.releaseId}/vulnerabilities?filters=category%3AOpen%20Source&limit=1 + onResponse: + - steps: + - set: + - name: ossTotal + value: ${issues_raw.totalCount} + - set: + - name: ossCritical + value: ${issues_raw.filters.?[#this.fieldName == 'severity'][0].fieldFilterValues.^[#this.value == "Critical"]?.count?:0} + - set: + - name: ossHigh + value: ${issues_raw.filters.?[#this.fieldName == 'severity'][0].fieldFilterValues.^[#this.value == "High"]?.count?:0} + - set: + - name: ossMedium + value: ${issues_raw.filters.?[#this.fieldName == 'severity'][0].fieldFilterValues.^[#this.value == "Medium"]?.count?:0} + - set: + - name: ossLow + value: ${issues_raw.filters.?[#this.fieldName == 'severity'][0].fieldFilterValues.^[#this.value == "Low"]?.count?:0} + # replace up to here + - write: - to: ${parameters.file} valueTemplate: summary-md @@ -38,6 +85,7 @@ steps: value: | Output written to ${parameters.file} +# Note: update references when Open Source counts are available on the release object, e.g. r.ossScanDate, r.ossCritical ... valueTemplates: - name: summary-md contents: | @@ -52,11 +100,12 @@ valueTemplates: **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)+' |'} + | 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) +' |'} + | **Open Source** | ${(#isBlank(ossScanDate)?#fmt('%-16s', 'N/A'):#formatDateTime(dateFmt, ossScanDate)) +' | '+#fmt('%8s', (ossCritical!=null?ossCritical:0)) +' | '+#fmt('%8s', (ossHigh!=null?ossHigh:0)) +' | '+#fmt('%8s', (ossMedium!=null?ossMedium:0)) +' | '+#fmt('%8s', (ossLow!=null?ossLow:0)) +' |'} + | **Total** | | ${#fmt('%8s', r.staticCritical+r.dynamicCritical+r.mobileCritical+(ossCritical!=null?ossCritical:0))+' | '+#fmt('%8s', r.staticHigh+r.dynamicHigh+r.mobileHigh+(ossHigh!=null?ossHigh:0))+' | '+#fmt('%8s', r.staticMedium+r.dynamicMedium+r.mobileMedium+(ossMedium!=null?ossMedium:0))+' | '+#fmt('%8s', r.staticLow+r.dynamicLow+r.mobileLow+(ossLow!=null?ossLow:0))+' |'} \ 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 5727294e73..5cb9607d72 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 @@ -10,7 +10,7 @@ usage.description = ### top level product command ### delim = Change the default delimiter character when using options that accept \ - "application[:microservice]:release" as an argument or parameter. + "application[:microservice]:release" as an argument or parameter. fcli.fod.usage.header = Interact with Fortify on Demand (FoD). fcli.fod.usage.description.0 = The commands in this module allow for interacting with \ Fortify on Demand (FoD). This includes functionality like managing applications, \ @@ -26,8 +26,9 @@ fcli.fod.app.app-name = Application name. fcli.fod.app.app-name-or-id = Application id or name. Note that numeric values are always interpreted \ as id's. If you have numeric application names, you will need to specify the application id. fcli.fod.app.app-type = Application type. Valid values: ${COMPLETION-CANDIDATES} -fcli.fod.app.release.microservice-and-release-name = Initial release to be created on the application, in the format : for a microservices application, or just for non-microservices applications. -fcli.fod.scan.scan-id = Scan id(s). +fcli.fod.app.release.microservice-and-release-name = Initial release to be created on the application, in the format : for a microservices application, or just for non-microservices applications. +fcli.fod.scan.scan-id = Scan id. +fcli.fod.scan.scan-ids = Scan id(s). fcli.fod.scan.entitlement-frequency = The entitlement frequency type to use. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.scan.analysis-status = Scan analysis status. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.scan.scan-type = Scan type. Valid values: ${COMPLETION-CANDIDATES}. @@ -120,7 +121,7 @@ fcli.fod.session.login.client-id.1 = Environment variable:%n \ fcli.fod.session.login.client-secret.0 = FoD client secret. fcli.fod.session.login.client-secret.1 = Environment variable:%n \ ${fcli.env.default.prefix}_FOD_CLIENT_SECRET -fcli.fod.session.login.scopes = FoD scopes to request. Default value: ${DEFAULT-VALUE} +fcli.fod.session.login.scopes = FoD scopes to request. Default value: ${DEFAULT-VALUE} fcli.fod.session.logout.usage.header = Terminate FoD session. fcli.fod.session.logout.usage.description = This command terminates an FoD session previously created \ @@ -137,7 +138,7 @@ fcli.fod.rest.usage.description = These commands allow for direct interaction wi (no need to manually specify Authorization header), rich output formatting options, and query functionality. fcli.fod.rest.call.usage.header = Call an individual FoD REST API endpoint. fcli.fod.rest.call.no-paging = By default, this command will load all pages of data from FoD (from the \ - given offset if specified as a request parameter). Use this option to return only a single page. + given offset if specified as a request parameter). Use this option to return only a single page. fcli.fod.rest.call.no-transform = By default, this command performs generic transformations on FoD REST \ responses, like only outputting the actual response data (contents of the 'items' property). Use this \ option to output the original response contents without transformations. @@ -150,7 +151,7 @@ fcli.fod.rest.lookup.usage.header = Retrieve FoD REST API lookup values. fcli.fod.rest.lookup.usage.description = Use this command to retrieve the values of lookup items (types) \ that are used with various commands and when using the FoD REST API directly. 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. + Leave empty to list all the valid lookup items of the REST API. # fcli fod action # Apart from the top-level usage header, which includes a FoD reference, all headers @@ -285,7 +286,7 @@ fcli.fod.app.output.header.microserviceName = Microservice fcli.fod.app.create.usage.header = Create a new application. fcli.fod.app.create.usage.description = This command allows a new application and its first release to be created. \ Please note some attributes might be mandatory depending on the configuration of your tenant. Please check the \ - Fortify on Demand web portal first. + Fortify on Demand web portal first. fcli.fod.app.create.application-name = The name of the application to create. fcli.fod.app.create.type = The type of the application. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.app.create.release-name = The name of the release to create for the application. @@ -378,7 +379,7 @@ 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}. +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}. # fcli fod entitlement fcli.fod.entitlement.usage.header = View FoD entitlements. @@ -394,7 +395,7 @@ 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 \ relevant 'fcli fod xxx-scan' entities, for example SAST scan setup and start can be found in \ - 'fcli fod sast-scan'. + 'fcli fod sast-scan'. fcli.fod.scan.output.header.scanId = Id fcli.fod.scan.output.header.scanType = Type fcli.fod.scan.output.header.analysisStatusType = Analysis Status @@ -419,10 +420,11 @@ fcli.fod.scan.wait-for.usage.description.2 = ${fcli.fod.scan.states:-See fcli he fcli.fod.scan.wait-for.until = Wait until either any or all scans match. If neither --until or --while are specified, default is to wait until all scans match. 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. +fcli.fod.scan.chunk-size = Size of each chunk (in bytes) for file uploads. Default is 1048576. # 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.description = The commands listed below allow for starting and managing SAST scans on FoD. fcli.fod.sast-scan.output.header.scanId = Id fcli.fod.sast-scan.output.header.analysisStatusType = Analysis Status fcli.fod.sast-scan.output.header.startedDateTime = Started @@ -492,7 +494,7 @@ fcli.fod.sast-scan.download-latest.file = File path and name where to save the F # 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.description = The commands listed below allow for starting and managing DAST scans on FoD. fcli.fod.dast-scan.output.header.scanId = Id fcli.fod.dast-scan.output.header.analysisStatusType = Analysis Status fcli.fod.dast-scan.output.header.startedDateTime = Started @@ -550,7 +552,7 @@ fcli.fod.dast-scan.start-legacy.start-date = ${fcli.fod.sast-scan.start.start-da fcli.fod.dast-scan.start-legacy.entitlement-id = ${fcli.fod.sast-scan.start.entitlement-id} fcli.fod.dast-scan.start-legacy.notes = ${fcli.fod.sast-scan.start.notes} fcli.fod.dast-scan.start-legacy.file = ${fcli.fod.sast-scan.start.file} -fcli.fod.dast-scan.start-legacy.chunk-size = ${fcli.fod.sast-scan.start.chunk-size} +fcli.fod.dast-scan.start-legacy.chunk-size = ${fcli.fod.scan.chunk-size} fcli.fod.dast-scan.start-legacy.timezone = The timezone to use for starting the scan - default is UTC. Use 'fod rest lookup TimeZones' to see the values. fcli.fod.dast-scan.import.usage.header = Import existing DAST scan results (from an FPR file). fcli.fod.dast-scan.import.usage.description = As FoD doesn't return a scan id for imported scans, the output of this command cannot be used with commands that expect a scan id, like the wait-for command. @@ -655,7 +657,7 @@ fcli.fod.dast-scan.setup-api.false-positive-removal = ${fcli.fod.dast-scan.setup # 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.description = The commands listed below allow for starting and managing MAST scans on FoD. fcli.fod.mast-scan.output.header.scanId = Id fcli.fod.mast-scan.output.header.analysisStatusType = Analysis Status fcli.fod.mast-scan.output.header.startedDateTime = Started @@ -713,7 +715,7 @@ fcli.fod.mast-scan.download-latest.file = File path and name where to save the F # 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.description = The commands listed below allow for starting and managing OSS scans on FoD. fcli.fod.oss-scan.output.header.scanId = Id fcli.fod.oss-scan.output.header.analysisStatusType = Analysis Status fcli.fod.oss-scan.output.header.startedDateTime = Started @@ -728,12 +730,25 @@ fcli.fod.oss-scan.import.usage.header = Import existing OSS scan results (from a fcli.fod.oss-scan.import.usage.description = As FoD doesn't return a scan id for imported scans, the output of this command cannot be used with commands that expect a scan id, like the wait-for command. fcli.fod.oss-scan.import.file = FPR file containing existing OSS scan results to be imported. fcli.fod.oss-scan.import.type = Open Source scan results file type. Valid values: ${COMPLETION-CANDIDATES} (default value is CycloneDX). +fcli.fod.oss-scan.import-debricked.usage.header = Import results from Debricked. +fcli.fod.oss-scan.import-debricked.file = Save a copy of the SBOM file downloaded from \ + Debricked to the given file. +fcli.fod.oss-scan.import-debricked.type = Open Source scan results file type. Valid values: ${COMPLETION-CANDIDATES} (default value is CycloneDX). +fcli.fod.oss-scan.import-debricked.debricked-url = Debricked URL. Default value: ${DEFAULT-VALUE}. +fcli.fod.oss-scan.import-debricked.debricked-user = Debricked user name / email address. +fcli.fod.oss-scan.import-debricked.debricked-password = Password for the given debricked user. +fcli.fod.oss-scan.import-debricked.debricked-access-token = Debricked long-lived access token. +fcli.fod.oss-scan.import-debricked.repository = Debricked repository name or id. +fcli.fod.oss-scan.import-debricked.branch = Branch in the given repository for which to retrieve the SBOM. +fcli.fod.oss-scan.import-debricked.insecure = Disable SSL checks when connecting to Debricked. +fcli.fod.oss-scan.import-debricked.connect-timeout = Debricked connection timeout, for example 30s (30 seconds), 5m (5 minutes). Default value: ${default-connect-timeout}. +fcli.fod.oss-scan.import-debricked.socket-timeout = Debricked socket timeout, for example 30s (30 seconds), 5m (5 minutes). Default value: ${default-socket-timeout}. fcli.fod.oss-scan.start.usage.header = (PREVIEW) Start a new OSS scan. fcli.fod.oss-scan.start.usage.description = This command is not fully implemented and is intended for preview only. \ Command name, options and behavior may change at any time, even between patch or minor releases, potentially affecting \ any workflows in which this command is being used. fcli.fod.oss-scan.start.file = ${fcli.fod.sast-scan.start.file} -fcli.fod.oss-scan.start.chunk-size = ${fcli.fod.sast-scan.start.chunk-size} +fcli.fod.oss-scan.start.chunk-size = ${fcli.fod.scan.chunk-size} fcli.fod.oss-scan.wait-for.usage.header = Wait for one or more OSS scans to reach or exit specified scan statuses. fcli.fod.oss-scan.wait-for.usage.description.0 = ${fcli.fod.scan.wait-for.usage.description.0} fcli.fod.oss-scan.wait-for.usage.description.1 = ${fcli.fod.scan.wait-for.usage.description.1} @@ -749,12 +764,31 @@ fcli.fod.oss-scan.download-latest.file = File path and name where to save the SB # fcli fod issue fcli.fod.issue.usage.header = Manage FoD issues (vulnerabilities) and related entities. fcli.fod.issue.list.usage.header = List vulnerabilities. +fcli.fod.issue.list.usage.description = This command allows for listing FoD vulnerability data \ + for a given release. By default, only visible issues will be returned; the --include option can \ + be used to (also) include suppressed or fixed issues. If any such issues are included, the \ + default table output will show (S) and/or (F) for respectively suppressed and fixed issues. \ + %n%nOptionally, additional details may be included in the output using the --embed option, but \ + please note that this may have a significant impact on performance as this will result in \ + additional HTTP requests to FoD for every individual issue, and FoD rate limits may apply \ + to those requests. \ + %n%nIn general, performance of this command is largely dependent on the number of issues \ + being retrieved from FoD. The default table output collects all data in memory until all \ + issues have been processed, so it may take a long time before you see any output, and there's \ + a small risk of running out of memory. Most other output formats (like json, yaml, or csv) \ + output data immediately after each page of issues has been loaded from FoD, resulting in \ + more immediate output. +fcli.fod.issue.list.output.header.visibilityMarker = # 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.issue.list.includeIssue = By default, only visible issues will be returned. This option \ + accepts a comma-separated list to allow (also) fixed and/or suppressed issues to be returned, \ + for example `--include visible,fixed` (to return both visible and fixed issues) or `--include \ + fixed` (to return only fixed issues). Allowed values: ${COMPLETION-CANDIDATES}. # fcli fod report fcli.fod.report.usage.header = Manage FoD reports. @@ -837,4 +871,4 @@ 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 +fcli.fod.issue.list.output.table.options = id,visibilityMarker,primaryLocation,lineNumber,category diff --git a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/session/cli/cmd/SCDastSessionLogoutCommand.java b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/session/cli/cmd/SCDastSessionLogoutCommand.java index 9987c97665..34bd3e3a2e 100644 --- a/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/session/cli/cmd/SCDastSessionLogoutCommand.java +++ b/fcli-core/fcli-sc-dast/src/main/java/com/fortify/cli/sc_dast/_common/session/cli/cmd/SCDastSessionLogoutCommand.java @@ -13,8 +13,11 @@ package com.fortify.cli.sc_dast._common.session.cli.cmd; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.common.session.cli.cmd.AbstractSessionLogoutCommand; +import com.fortify.cli.common.session.helper.SessionLogoutException; import com.fortify.cli.sc_dast._common.session.cli.mixin.SCDastSessionLogoutOptions; +import com.fortify.cli.sc_dast._common.session.cli.mixin.SCDastUserCredentialOptions; import com.fortify.cli.sc_dast._common.session.helper.SCDastSessionDescriptor; import com.fortify.cli.sc_dast._common.session.helper.SCDastSessionHelper; @@ -30,6 +33,17 @@ public class SCDastSessionLogoutCommand extends AbstractSessionLogoutCommand authEntitySpecs) { + if ( authEntitySpecs!=null && !authEntitySpecs.isEmpty() ) { + add(allowMultipleMatches, authEntitySpecs.toArray(String[]::new)); + } + return this; + } + public final SSCAppVersionUserUpdateBuilder remove(boolean allowMultipleMatches, String... authEntitySpecs) { this.allowMultipleMatchesForRemove |= allowMultipleMatches; if ( authEntitySpecs!=null && authEntitySpecs.length>0 ) { @@ -96,4 +104,6 @@ public final SSCAppVersionUserUpdateBuilder remove(boolean allowMultipleMatches, } return this; } + + } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenCreateResponse.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenGetOrCreateResponse.java similarity index 92% rename from fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenCreateResponse.java rename to fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenGetOrCreateResponse.java index ea6fa1b765..68ce9d2cdf 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenCreateResponse.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenGetOrCreateResponse.java @@ -23,8 +23,8 @@ @Data @Reflectable @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) -public final class SSCTokenCreateResponse { - private SSCTokenCreateResponse.SSCTokenData data; +public final class SSCTokenGetOrCreateResponse { + private SSCTokenGetOrCreateResponse.SSCTokenData data; @Data @Reflectable @NoArgsConstructor public static final class SSCTokenData { diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenHelper.java index fb26ad1825..e3bc6fec74 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenHelper.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/helper/SSCTokenHelper.java @@ -26,6 +26,7 @@ import com.fortify.cli.common.http.proxy.helper.ProxyHelper; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.rest.unirest.GenericUnirestFactory; +import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.common.rest.unirest.config.IUrlConfig; import com.fortify.cli.common.rest.unirest.config.IUserCredentialsConfig; import com.fortify.cli.common.rest.unirest.config.UnirestBasicAuthConfigurer; @@ -82,12 +83,24 @@ public static final JsonNode deleteTokensByValue(IUrlConfig urlConfig, IUserCred } } + public static final void revokeToken(IUrlConfig urlConfig, IUserCredentialsConfig uc, char[] token) { + try ( var unirest = GenericUnirestFactory.createUnirestInstance() ) { + revokeToken(unirest, urlConfig, uc, token); + } + } + public static final T createToken(IUrlConfig urlConfig, IUserCredentialsConfig uc, SSCTokenCreateRequest tokenCreateRequest, Class returnType) { try ( var unirest = GenericUnirestFactory.createUnirestInstance() ) { return createToken(unirest, urlConfig, uc, tokenCreateRequest, returnType); } } + public static final SSCTokenGetOrCreateResponse getTokenData(IUrlConfig urlConfig, char[] token) { + try ( var unirest = GenericUnirestFactory.createUnirestInstance() ) { + return getTokenData(unirest, urlConfig, token); + } + } + public static final R run(IUrlConfig urlConfig, char[] activeToken, Function f) { try ( var unirest = GenericUnirestFactory.createUnirestInstance() ) { configureUnirest(unirest, urlConfig, activeToken); @@ -128,6 +141,21 @@ private static final JsonNode deleteTokensByValue(UnirestInstance unirest, IUrlC .asObject(JsonNode.class).getBody(); } + private static final void revokeToken(UnirestInstance unirest, IUrlConfig urlConfig, IUserCredentialsConfig uc, char[] restToken) { + if ( uc!=null ) { + configureUnirest(unirest, urlConfig, uc); + } else { + // SSC 24.2+ allows for revoking tokens using token authentication + configureUnirest(unirest, urlConfig, restToken); + } + ObjectNode tokenRevokeRequest = new ObjectMapper().createObjectNode(); + ArrayNode tokenArray = JsonHelper.toArrayNode(new String(restToken)); + tokenRevokeRequest.set("tokens", tokenArray); + unirest.post(SSCUrls.TOKENS_ACTION_REVOKE) + .body(tokenRevokeRequest) + .asObject(JsonNode.class).getBody(); + } + private static final T createToken(UnirestInstance unirest, IUrlConfig urlConfig, IUserCredentialsConfig uc, SSCTokenCreateRequest tokenCreateRequest, Class returnType) { configureUnirest(unirest, urlConfig, uc); return unirest.post("/api/v1/tokens") @@ -136,6 +164,25 @@ private static final T createToken(UnirestInstance unirest, IUrlConfig urlCo .getBody(); } + private static SSCTokenGetOrCreateResponse getTokenData(UnirestInstance unirest, IUrlConfig urlConfig, char[] token) { + configureUnirest(unirest, urlConfig, token); + try { + var result = unirest.post("/api/v1/userSession/tokenData") + .body(JsonHelper.getObjectMapper().createObjectNode()) + .asObject(SSCTokenGetOrCreateResponse.class) + .getBody(); + result.getData().setToken(token); + return result; + } catch ( UnexpectedHttpResponseException e ) { + if ( e.getStatus()==404 ) { + // Older SSC versions don't support this endpoint, so we just return null + return null; + } else { + throw e; + } + } + } + private static void configureUnirest(UnirestInstance unirest, IUrlConfig urlConfig, IUserCredentialsConfig uc) { UnirestUnexpectedHttpResponseConfigurer.configure(unirest); UnirestUrlConfigConfigurer.configure(unirest, urlConfig); 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 index 0554be6e8e..d521f47ab2 100644 --- 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 @@ -68,6 +68,11 @@ protected final String getType() { return "SSC"; } + @Override + protected String getSessionName() { + return unirestInstanceSupplier.getSessionName(); + } + @Override protected void configure(ActionRunner templateRunner, SimpleEvaluationContext context) { templateRunner 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 1f32fc54e2..1549346d4c 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 @@ -12,7 +12,15 @@ *******************************************************************************/ package com.fortify.cli.ssc.appversion.cli.cmd; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -31,17 +39,15 @@ import com.fortify.cli.ssc.app.helper.SSCAppHelper; import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppAndVersionNameResolverMixin; import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionCopyFromMixin; -import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionRefreshOptions; +import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionCopyFromMixin.SSCAppVersionCopyFromDescriptor; +import com.fortify.cli.ssc.appversion.cli.mixin.SSCAppVersionCopyFromMixin.SSCAppVersionCopyOption; import com.fortify.cli.ssc.appversion.helper.SSCAppAndVersionNameDescriptor; -import com.fortify.cli.ssc.appversion.helper.SSCAppVersionCreateCopyFromBuilder; import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor; import com.fortify.cli.ssc.appversion.helper.SSCAppVersionHelper; import com.fortify.cli.ssc.attribute.cli.mixin.SSCAttributeUpdateMixin; 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 com.fortify.cli.ssc.issue.helper.SSCIssueTemplateHelper; import kong.unirest.HttpRequest; import kong.unirest.UnirestInstance; @@ -59,7 +65,6 @@ public class SSCAppVersionCreateCommand extends AbstractSSCJsonNodeOutputCommand @Mixin private SSCAttributeUpdateMixin.OptionalAttrOption attrUpdateMixin; @Mixin private SSCAppVersionUserMixin.OptionalUserAddOption userAddMixin; @Mixin private SSCAppVersionCopyFromMixin copyFromMixin; - @Mixin private SSCAppVersionRefreshOptions refreshOptions; @Option(names={"--description","-d"}, required = false) private String description; @Option(names={"--active"}, required = false, defaultValue="true", arity="1") @@ -72,22 +77,21 @@ public class SSCAppVersionCreateCommand extends AbstractSSCJsonNodeOutputCommand @Override public JsonNode getJsonNode(UnirestInstance unirest) { if ( skipIfExists ) { - SSCAppVersionDescriptor descriptor = SSCAppVersionHelper.getOptionalAppVersionFromAppAndVersionName(unirest, sscAppAndVersionNameResolver.getAppAndVersionNameDescriptor()); - if ( descriptor!=null ) { return descriptor.asObjectNode().put(IActionCommandResultSupplier.actionFieldName, "SKIPPED_EXISTING"); } + var existingDescriptor = SSCAppVersionHelper.getOptionalAppVersionFromAppAndVersionName(unirest, sscAppAndVersionNameResolver.getAppAndVersionNameDescriptor()); + if ( existingDescriptor!=null ) { return existingDescriptor.asObjectNode().put(IActionCommandResultSupplier.actionFieldName, "SKIPPED_EXISTING"); } } - SSCAttributeUpdateBuilder attrUpdateBuilder = getAttrUpdateBuilder(unirest); - SSCAppVersionUserUpdateBuilder authUpdateBuilder = getAuthUpdateBuilder(unirest); - - SSCAppVersionCreateCopyFromBuilder copyFromBuilder = getCopyFromBuilder(unirest); + var copyFromDescriptor = copyFromMixin.getCopyFromDescriptor(unirest); + var attrUpdateBuilder = getAttrUpdateBuilder(unirest, copyFromDescriptor); + var authUpdateBuilder = getAuthUpdateBuilder(unirest, copyFromDescriptor); - SSCAppVersionDescriptor descriptor = createUncommittedAppVersion(unirest); + var descriptor = createUncommittedAppVersion(unirest, copyFromDescriptor); SSCBulkResponse bulkResponse = new SSCBulkRequestBuilder() .request("attrUpdate", attrUpdateBuilder.buildRequest(descriptor.getVersionId())) .request("userUpdate", authUpdateBuilder.buildRequest(descriptor.getVersionId())) - .request("copyFrom", copyFromBuilder.buildCopyFromPartialRequest(descriptor.getVersionId())) + .request("copyFrom", buildCopyFromPartialRequest(unirest, descriptor, copyFromDescriptor)) .request("commit", getCommitRequest(unirest, descriptor)) - .request("copyState", copyFromBuilder.buildCopyStateRequest(descriptor.getVersionId())) + .request("copyState", buildCopyStateRequest(unirest, descriptor, copyFromDescriptor)) .request("updatedVersion", unirest.get(SSCUrls.PROJECT_VERSION(descriptor.getVersionId()))) .execute(unirest); return bulkResponse.body("updatedVersion"); @@ -108,68 +112,95 @@ public boolean isSingular() { return true; } - private final SSCAppVersionCreateCopyFromBuilder getCopyFromBuilder(UnirestInstance unirest) { - SSCAppVersionCreateCopyFromBuilder builder = new SSCAppVersionCreateCopyFromBuilder(unirest); - if(copyFromMixin.isCopyRequested()) { - SSCAppVersionDescriptor fromAppVersionDesc = SSCAppVersionHelper.getRequiredAppVersion(unirest, copyFromMixin.getAppVersionNameOrId(), sscAppAndVersionNameResolver.getDelimiter()); - - builder .setCopyRequested(true) - .setCopyFrom(fromAppVersionDesc) - .setCopyOptions(copyFromMixin.getCopyOptions()); - - // refreshMetrics if the source PV is required to fully copy the tags, audit or comments - if(builder.copyStateEnabled() - && refreshOptions.isRefresh() - && fromAppVersionDesc.isRefreshRequired()){ - SSCJobDescriptor refreshJobDesc = SSCAppVersionHelper.refreshMetrics(unirest, fromAppVersionDesc); - SSCJobHelper.waitForJob(unirest,refreshJobDesc); - } - } - - return builder; - } - - private final SSCAppVersionUserUpdateBuilder getAuthUpdateBuilder(UnirestInstance unirest) { + private final SSCAppVersionUserUpdateBuilder getAuthUpdateBuilder(UnirestInstance unirest, SSCAppVersionCopyFromDescriptor copyFromDescriptor) { return new SSCAppVersionUserUpdateBuilder(unirest) + .add(false, getUsersFromSource(unirest, copyFromDescriptor)) .add(false, userAddMixin.getAuthEntitySpecs()); } + + private Set getUsersFromSource(UnirestInstance unirest, SSCAppVersionCopyFromDescriptor copyFromDescriptor) { + if ( copyFromDescriptor.isCopyRequested() && copyFromDescriptor.getCopyOptions().contains(SSCAppVersionCopyOption.users) ) { + return getUsersSet(unirest, copyFromDescriptor.getAppVersionDescriptor()); + } + return null; + } + + public static final Set getUsersSet(UnirestInstance unirest, SSCAppVersionDescriptor descriptor) { + Set result = new LinkedHashSet<>(); + var users = SSCAppVersionHelper.getUsers(unirest, descriptor); + for (JsonNode user : users) { + result.add(user.get("id").asText()); + } + return result; + } - private final SSCAttributeUpdateBuilder getAttrUpdateBuilder(UnirestInstance unirest) { - Map attributes = attrUpdateMixin.getAttributes(); + private final SSCAttributeUpdateBuilder getAttrUpdateBuilder(UnirestInstance unirest, SSCAppVersionCopyFromDescriptor copyFromDescriptor) { return new SSCAttributeUpdateBuilder(unirest) - .add(attributes) + .add(getAttributesFromSource(unirest, copyFromDescriptor)) + .add(attrUpdateMixin.getAttributes()) .addRequiredAttrs(autoRequiredAttrs) .checkRequiredAttrs(true) .prepareAndCheckRequest(); } - private SSCAppVersionDescriptor createUncommittedAppVersion(UnirestInstance unirest) { - SSCIssueTemplateDescriptor issueTemplateDescriptor = issueTemplateResolver.getIssueTemplateDescriptorOrDefault(unirest); - SSCAppAndVersionNameDescriptor appAndVersionNameDescriptor = sscAppAndVersionNameResolver.getAppAndVersionNameDescriptor(); - - if ( issueTemplateDescriptor==null ) { - throw new IllegalArgumentException("--issue-template is required, as no default template is configured on SSC"); + private Map getAttributesFromSource(UnirestInstance unirest, SSCAppVersionCopyFromDescriptor copyFromDescriptor) { + if ( copyFromDescriptor.isCopyRequested() && copyFromDescriptor.getCopyOptions().contains(SSCAppVersionCopyOption.attributes) ) { + return getAttributesMap(unirest, copyFromDescriptor.getAppVersionDescriptor()); + } + return null; + } + + public static final Map getAttributesMap(UnirestInstance unirest, SSCAppVersionDescriptor descriptor) { + var result = new LinkedHashMap(); + var attributes = SSCAppVersionHelper.getAttributes(unirest, descriptor); + for (JsonNode attr : attributes) { + List values = new ArrayList<>(); + for (JsonNode value: attr.get("values")) { + values.add(value.get("guid").textValue()); + } + result.put(attr.get("attributeDefinitionId").toString(), String.join(";", values)); } + return result; + } + private SSCAppVersionDescriptor createUncommittedAppVersion(UnirestInstance unirest, SSCAppVersionCopyFromDescriptor copyFromDescriptor) { + String issueTemplateId = getIssueTemplateId(unirest, copyFromDescriptor); + SSCAppAndVersionNameDescriptor appAndVersionNameDescriptor = sscAppAndVersionNameResolver.getAppAndVersionNameDescriptor(); + var description = this.description; + if ( StringUtils.isBlank(description) && copyFromDescriptor.isCopyRequested() ) { + description = String.format("Copied from "+copyFromDescriptor.getAppVersionDescriptor().getAppAndVersionName()); + } ObjectNode body = objectMapper.createObjectNode(); body.put("name", appAndVersionNameDescriptor.getVersionName()) .put("description", description==null ? "" : description) .put("active", active) .put("committed", false) - .put("issueTemplateId", issueTemplateDescriptor.getId()) - .set("project", getProjectNode(unirest, appAndVersionNameDescriptor.getAppName(), issueTemplateDescriptor)); + .put("issueTemplateId", issueTemplateId) + .set("project", getProjectNode(unirest, appAndVersionNameDescriptor.getAppName(), issueTemplateId)); JsonNode response = unirest.post(SSCUrls.PROJECT_VERSIONS).body(body).asObject(JsonNode.class).getBody().get("data"); return JsonHelper.treeToValue(response, SSCAppVersionDescriptor.class); } - private JsonNode getProjectNode(UnirestInstance unirest, String appName, SSCIssueTemplateDescriptor issueTemplateDescriptor) { + private String getIssueTemplateId(UnirestInstance unirest, SSCAppVersionCopyFromDescriptor copyFromDescriptor) { + var issueTemplateNameOrId = issueTemplateResolver.getIssueTemplateNameOrId(); + if ( StringUtils.isBlank(issueTemplateNameOrId) && copyFromDescriptor.isCopyRequested() ) { + issueTemplateNameOrId = copyFromDescriptor.getAppVersionDescriptor().getIssueTemplateId(); + } + var issueTemplateDescriptor = new SSCIssueTemplateHelper(unirest).getIssueTemplateDescriptorOrDefault(issueTemplateNameOrId); + if ( issueTemplateDescriptor==null ) { + throw new IllegalArgumentException("--issue-template is required, as no default template is configured on SSC"); + } + return issueTemplateDescriptor.getId(); + } + + private JsonNode getProjectNode(UnirestInstance unirest, String appName, String issueTemplateId) { SSCAppDescriptor appDescriptor = SSCAppHelper.getApp(unirest, appName, false, "id"); if ( appDescriptor!=null ) { return appDescriptor.asJsonNode(); } else { ObjectNode appNode = new ObjectMapper().createObjectNode(); appNode.put("name", appName); - appNode.put("issueTemplateId", issueTemplateDescriptor.getId()); + appNode.put("issueTemplateId", issueTemplateId); return appNode; } } @@ -178,4 +209,34 @@ private final HttpRequest getCommitRequest(UnirestInstance unirest, SSCAppVer ObjectNode body = objectMapper.createObjectNode().put("committed", true); return unirest.put(SSCUrls.PROJECT_VERSION(descriptor.getVersionId())).body(body); } + + private HttpRequest buildCopyFromPartialRequest(UnirestInstance unirest, SSCAppVersionDescriptor copyTo, SSCAppVersionCopyFromDescriptor copyFromDescriptor) { + if ( !copyFromDescriptor.isCopyRequested() ) { return null; } + var properties = copyFromDescriptor.getCopyOptions().stream() + .map(SSCAppVersionCopyOption::getCopyFromPartialProperty) + .filter(Objects::nonNull) + .toList(); + if ( properties.isEmpty() ) { return null; } + var body = buildCopyFromAppVersionIdsBody(copyTo, copyFromDescriptor); + properties.forEach(p->body.put(p, true)); + return unirest.post(SSCUrls.PROJECT_VERSIONS_ACTION_COPY_FROM_PARTIAL) + .body(body); + } + + private HttpRequest buildCopyStateRequest(UnirestInstance unirest, SSCAppVersionDescriptor copyTo, SSCAppVersionCopyFromDescriptor copyFromDescriptor) { + if ( !copyFromDescriptor.isCopyRequested() + || !copyFromDescriptor.getCopyOptions().contains(SSCAppVersionCopyOption.state)) { + return null; + } + return unirest.post(SSCUrls.PROJECT_VERSIONS_ACTION_COPY_CURRENT_STATE) + .body(buildCopyFromAppVersionIdsBody(copyTo, copyFromDescriptor)); + } + + private ObjectNode buildCopyFromAppVersionIdsBody(SSCAppVersionDescriptor copyTo, SSCAppVersionCopyFromDescriptor copyFromDescriptor) { + var copyFrom = copyFromDescriptor.getAppVersionDescriptor(); + return JsonHelper.getObjectMapper().createObjectNode() + .put("projectVersionId", copyTo.getVersionId()) + .put("previousProjectVersionId", copyFrom.getVersionId()); + } + } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/ISSCDelimiterMixinAware.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/ISSCDelimiterMixinAware.java new file mode 100644 index 0000000000..752063b41d --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/ISSCDelimiterMixinAware.java @@ -0,0 +1,17 @@ +/** + * 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.appversion.cli.mixin; + +public interface ISSCDelimiterMixinAware { + void setDelimiterMixin(SSCDelimiterMixin delimiterMixin); +} 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 90f9036241..f7a2e244a9 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 @@ -12,33 +12,49 @@ *******************************************************************************/ package com.fortify.cli.ssc.appversion.cli.mixin; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + 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.common.util.StringUtils; import com.fortify.cli.ssc.appversion.helper.SSCAppVersionDescriptor; import com.fortify.cli.ssc.appversion.helper.SSCAppVersionHelper; +import com.fortify.cli.ssc.system_state.helper.SSCJobHelper; import kong.unirest.UnirestInstance; +import lombok.Data; import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; -public class SSCAppVersionCopyFromMixin { - +public class SSCAppVersionCopyFromMixin implements ISSCDelimiterMixinAware { + @Setter private SSCDelimiterMixin delimiterMixin; @ArgGroup(exclusive=false, multiplicity = "0..1") - private SSCAppVersionCopyFromArgGroup argGroup; + private SSCAppVersionCopyFromArgGroup copyOptionsArgGroup = new SSCAppVersionCopyFromArgGroup(); + @Mixin private SSCAppVersionRefreshOptions refreshOptions; - public boolean isCopyRequested() { return argGroup!=null; } - public String getAppVersionNameOrId() { - return argGroup==null ? null : argGroup.getAppVersionNameOrId(); + public final SSCAppVersionCopyFromDescriptor getCopyFromDescriptor(UnirestInstance unirest) { + var appVersionDescriptor = refresh(unirest, + copyOptionsArgGroup.getCopyFromAppVersionDescriptor(unirest, delimiterMixin.getDelimiter())); + var copyOptions = copyOptionsArgGroup.getCopyOptionsOrDefault(); + return new SSCAppVersionCopyFromDescriptor(appVersionDescriptor, copyOptions); } - - public final SSCAppVersionCopyType[] getCopyOptions(){ - return this.argGroup.getCopyOptions(); - } - - public SSCAppVersionDescriptor getAppVersionDescriptor(UnirestInstance unirest,String delimiter, String... fields){ - return SSCAppVersionHelper.getRequiredAppVersion(unirest, getAppVersionNameOrId(), delimiter, fields); + + private SSCAppVersionDescriptor refresh(UnirestInstance unirest, SSCAppVersionDescriptor copyFrom) { + if (copyFrom!=null && copyFrom.isRefreshRequired() && refreshOptions.isRefresh() ) { + var jobDescriptor = SSCAppVersionHelper.refreshMetrics(unirest, copyFrom); + SSCJobHelper.waitForJob(unirest, jobDescriptor); + } + return copyFrom; } private static class SSCAppVersionCopyFromArgGroup { @@ -46,6 +62,72 @@ private static class SSCAppVersionCopyFromArgGroup { @Getter private String appVersionNameOrId; @DisableTest(TestType.MULTI_OPT_PLURAL_NAME) @Option(names = {"--copy"}, required = false, split = ",", descriptionKey = "fcli.ssc.appversion.create.copy-options") - @Getter private SSCAppVersionCopyType[] copyOptions; + @Getter private Set copyOptions; + @Getter(lazy = true) private final Set copyOptionsOrDefault = + SSCAppVersionCopyOption.getCopyOptionsOrDefaultStream(copyOptions).collect(Collectors.toSet()); + + private SSCAppVersionDescriptor getCopyFromAppVersionDescriptor(UnirestInstance unirest, String delimiter, String... fields) { + return StringUtils.isBlank(appVersionNameOrId) + ? null + : SSCAppVersionHelper.getRequiredAppVersion(unirest, appVersionNameOrId, delimiter, fields); + } + } + + @Data + public static final class SSCAppVersionCopyFromDescriptor { + private final SSCAppVersionDescriptor appVersionDescriptor; + private final Set copyOptions; + + public boolean isCopyRequested() { + return appVersionDescriptor!=null; + } + } + + /** + * This enumeration defines the items that can be copied from an existing application version + */ + @RequiredArgsConstructor @Getter + public static enum SSCAppVersionCopyOption { + custom_tags("copyCustomTags", null), + bugtracker("copyBugTrackerConfiguration", null), + BugTrackerConfiguration("copyBugTrackerConfiguration", bugtracker), // Deprecated + processing_rules("copyAnalysisProcessingRules", null), + AnalysisProcessingRules("copyAnalysisProcessingRules", processing_rules), // Deprecated + // Contrary to what's sent by SSC UI, attributes are not supported on COPY_FROM_PARTIAL + attributes(null, null), + // Contrary to what's sent by SSC UI, auth entities are not supported on COPY_FROM_PARTIAL + users(null, null), + // Requires separate call to COPY_CURRENT_STATE action + state(null, null); + + @Override + public String toString() { + // Show and accept dashes instead of underscores when this + // enum is used in picocli options. + return name().replace('_', '-'); + } + + private static final Logger LOG = LoggerFactory.getLogger(SSCAppVersionCopyOption.class); + private final String copyFromPartialProperty; + private final SSCAppVersionCopyOption deprecatedReplacement; + + public static final Stream getCopyOptionsOrDefaultStream(Collection copyOptions) { + return isCopyAll(copyOptions) + ? Stream.of(SSCAppVersionCopyOption.values()) + .filter(o->o.getDeprecatedReplacement()==null) + : copyOptions.stream() + .peek(SSCAppVersionCopyOption::warnDeprecated); + } + + public static final boolean isCopyAll(Collection copyOptions) { + return copyOptions==null || copyOptions.isEmpty(); + } + + public static final void warnDeprecated(SSCAppVersionCopyOption o) { + var replacement = o.getDeprecatedReplacement(); + if ( replacement!=null ) { + LOG.warn(String.format("WARN: %s is deprecated, please use %s", o.name(), replacement.name())); + } + } } } diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCDelimiterMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCDelimiterMixin.java index 76fc293587..474e46c22c 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCDelimiterMixin.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/cli/mixin/SSCDelimiterMixin.java @@ -12,10 +12,25 @@ *******************************************************************************/ package com.fortify.cli.ssc.appversion.cli.mixin; +import com.fortify.cli.common.cli.mixin.ICommandAware; + import lombok.Getter; +import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; -public final class SSCDelimiterMixin { +public final class SSCDelimiterMixin implements ICommandAware { @Option(names = {"--delim"}, defaultValue = ":") @Getter private String delimiter; + + @Override + public void setCommandSpec(CommandSpec commandSpec) { + commandSpec.mixins().values().forEach(this::injectThis); + } + + private void injectThis(CommandSpec spec) { + var mixin = spec.userObject(); + if ( mixin instanceof ISSCDelimiterMixinAware ) { + ((ISSCDelimiterMixinAware)mixin).setDelimiterMixin(this); + } + } } \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCopyType.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCopyType.java deleted file mode 100644 index 43d47d1cc2..0000000000 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCopyType.java +++ /dev/null @@ -1,44 +0,0 @@ -/******************************************************************************* - * 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.appversion.helper; - -import java.util.stream.Stream; - -/** - * This enumeration defines the items that can be copied from an existing application version - */ -public enum SSCAppVersionCopyType { - AnalysisProcessingRules("copyAnalysisProcessingRules"), - BugTrackerConfiguration("copyBugTrackerConfiguration"), - CustomTags("copyCustomTags"), - State("copyState"); - - private final String sscValue; - - private SSCAppVersionCopyType(String sscValue) { - this.sscValue = sscValue; - } - - public String getSscValue() { - return this.sscValue; - } - - public static final SSCAppVersionCopyType fromSscValue(String sscValue) { - return Stream.of(SSCAppVersionCopyType.values()) - .filter(v->v.getSscValue().equals(sscValue)) - .findFirst() - .orElseThrow(()->new IllegalStateException("Unknown SSC copyFrom option: "+sscValue)); - } - -} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCreateCopyFromBuilder.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCreateCopyFromBuilder.java deleted file mode 100644 index 374edd0072..0000000000 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionCreateCopyFromBuilder.java +++ /dev/null @@ -1,96 +0,0 @@ -/******************************************************************************* - * 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.appversion.helper; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fortify.cli.common.json.JsonHelper; -import com.fortify.cli.ssc._common.rest.SSCUrls; - -import kong.unirest.HttpRequest; -import kong.unirest.UnirestInstance; - -public final class SSCAppVersionCreateCopyFromBuilder { - private final UnirestInstance unirest; - - private ObjectNode copyFromPartialOptions = JsonHelper.getObjectMapper().createObjectNode(); - private ObjectNode copyStateOptions = JsonHelper.getObjectMapper().createObjectNode(); - private SSCAppVersionDescriptor previousProjectVersion; - - private boolean copyRequested = false; - - private boolean copyState = false; - - public SSCAppVersionCreateCopyFromBuilder(UnirestInstance unirest) { - this.unirest = unirest; - } - - public final HttpRequest buildCopyFromPartialRequest(String projectVersionId) { - if(!copyRequested){ - return null; - } - - this.copyFromPartialOptions.put("projectVersionId", projectVersionId); - return unirest - .post(SSCUrls.PROJECT_VERSIONS_ACTION_COPY_FROM_PARTIAL) - .body(copyFromPartialOptions); - } - - - public final HttpRequest buildCopyStateRequest(String projectVersionId) { - if(!copyState || !copyRequested){ - return null; - } - - this.copyStateOptions.put("projectVersionId", Integer.parseInt(projectVersionId)); - - ObjectNode body = JsonHelper.getObjectMapper().createObjectNode(); - body .put("type", "copy_current_state") - .set("values", copyStateOptions); - - return unirest - .post(SSCUrls.PROJECT_VERSIONS_ACTION(projectVersionId)) - .body(body); - } - - public final SSCAppVersionCreateCopyFromBuilder setCopyFrom(SSCAppVersionDescriptor previousProjectVersionDescriptor) { - this.previousProjectVersion = previousProjectVersionDescriptor; - this.copyFromPartialOptions.put("previousProjectVersionId", previousProjectVersionDescriptor.getVersionId()); - this.copyStateOptions.put("previousProjectVersionId", previousProjectVersionDescriptor.getIntVersionId()); - return this; - } - - public final SSCAppVersionCreateCopyFromBuilder setCopyOptions(SSCAppVersionCopyType[] copyOptions) { - if(copyOptions == null) { - copyOptions = SSCAppVersionCopyType.values(); - } - for (SSCAppVersionCopyType option : copyOptions) { - if(option.getSscValue() == "copyState") { - this.copyState = true; - } else { - this.copyFromPartialOptions.put(option.getSscValue(), "true"); - } - } - - return this; - } - - public final SSCAppVersionCreateCopyFromBuilder setCopyRequested(boolean status){ - this.copyRequested = status; - - return this; - } - - public boolean copyStateEnabled() { - return this.copyState; - } -} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionDescriptor.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionDescriptor.java index 75c24d2c6a..4d538a9ce5 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionDescriptor.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionDescriptor.java @@ -31,6 +31,7 @@ public class SSCAppVersionDescriptor extends JsonNodeHolder { @JsonProperty("id") private String versionId; @JsonProperty("name") private String versionName; private boolean refreshRequired; + private String issueTemplateId; @JsonProperty("project") public void unpackProject(Map project) { diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionHelper.java index ce4cc1193a..dc5af3fa9b 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionHelper.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/appversion/helper/SSCAppVersionHelper.java @@ -75,6 +75,20 @@ private static final SSCAppVersionDescriptor getOptionalDescriptor(GetRequest re return versions.size()==0 ? null : JsonHelper.treeToValue(versions.get(0), SSCAppVersionDescriptor.class); } + public static final JsonNode getAttributes(UnirestInstance unirest, SSCAppVersionDescriptor descriptor) { + return unirest.get(SSCUrls.PROJECT_VERSION_ATTRIBUTES(descriptor.getVersionId())) + .asObject(ObjectNode.class) + .getBody() + .get("data"); + } + + public static final JsonNode getUsers(UnirestInstance unirest, SSCAppVersionDescriptor descriptor) { + return unirest.get(SSCUrls.PROJECT_VERSION_AUTH_ENTITIES(descriptor.getVersionId())) + .asObject(ObjectNode.class) + .getBody() + .get("data"); + } + public static final SSCJobDescriptor refreshMetrics(UnirestInstance unirest, SSCAppVersionDescriptor descriptor) { if ( !descriptor.isRefreshRequired() ) { return null; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactCommands.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactCommands.java index 0bace39b68..22fff54f29 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactCommands.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactCommands.java @@ -14,7 +14,6 @@ import com.fortify.cli.common.cli.cmd.AbstractContainerCommand; import com.fortify.cli.common.variable.DefaultVariablePropertyName; -import com.fortify.cli.ssc.artifact.cli.cmd.import_debricked.SSCArtifactImportDebrickedCommand; import picocli.CommandLine.Command; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactImportDebrickedCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactImportDebrickedCommand.java new file mode 100644 index 0000000000..ec7618c18e --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/artifact/cli/cmd/SSCArtifactImportDebrickedCommand.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * 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.artifact.cli.cmd; + +import java.io.File; + +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.progress.helper.IProgressWriterI18n; +import com.fortify.cli.common.rest.unirest.GenericUnirestFactory; +import com.fortify.cli.common.util.StringUtils; +import com.fortify.cli.common.cli.cmd.import_debricked.DebrickedHelper; +import com.fortify.cli.common.cli.cmd.import_debricked.DebrickedLoginOptions; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import lombok.SneakyThrows; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command(name = "import-debricked") +public class SSCArtifactImportDebrickedCommand extends AbstractSSCArtifactUploadCommand { + @Mixin @Getter private OutputHelperMixins.TableNoQuery outputHelper; + @Mixin private DebrickedLoginOptions debrickedLoginOptions; + + @Option(names = {"-e", "--engine-type"}, required = true, defaultValue = "DEBRICKED") + @Getter private String engineType; + + @Option(names = {"-f", "--save-sbom-as"}, required = false) + private String fileName; + + @Option(names = {"-r", "--repository"}, required = true) + private String repository; + + @Option(names = {"-b", "--branch"}, required = true) + private String branch; + + @Override + public boolean isSingular() { + return true; + } + + @Override @SneakyThrows + protected File getFile() { + File sbomFile = null; + if ( StringUtils.isNotBlank(fileName) ) { + sbomFile = new File(fileName); + } else { + sbomFile = File.createTempFile("debricked", ".json"); + sbomFile.deleteOnExit(); + } + return sbomFile; + } + + @Override + protected void preUpload(UnirestInstance unirest, IProgressWriterI18n progressWriter, File file) { + DebrickedHelper debrickedHelper = new DebrickedHelper(debrickedLoginOptions, repository, branch); + progressWriter.writeProgress("Status: Generating & downloading SBOM"); + try ( var debrickedUnirest = GenericUnirestFactory.createUnirestInstance() ) { + debrickedHelper.downloadSbom(debrickedUnirest, file); + } + progressWriter.writeProgress("Status: Uploading SBOM to SSC"); + } + + @Override + protected void postUpload(UnirestInstance unirest, IProgressWriterI18n progressWriter, File file) { + if ( StringUtils.isBlank(fileName) ) { + file.delete(); + } + progressWriter.writeProgress("Status: SBOM uploaded to SSC"); + progressWriter.clearProgress(); + } + +} 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 index 1aa690e062..19995ec80d 100644 --- 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 @@ -22,6 +22,7 @@ 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.cli.mixin.SSCIssueIncludeMixin; import com.fortify.cli.ssc.issue.helper.SSCIssueFilterHelper; import com.fortify.cli.ssc.issue.helper.SSCIssueFilterSetDescriptor; @@ -41,6 +42,7 @@ public class SSCIssueListCommand extends AbstractSSCBaseRequestOutputCommand imp @Mixin private SSCQParamMixin qParamMixin; @Mixin private SSCIssueBulkEmbedMixin bulkEmbedMixin; @Option(names="--filter", required=false) private String filter; + @Mixin private SSCIssueIncludeMixin includeMixin; // 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? diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueIncludeMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueIncludeMixin.java new file mode 100644 index 0000000000..a2fb7bff26 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueIncludeMixin.java @@ -0,0 +1,76 @@ +package com.fortify.cli.ssc.issue.cli.mixin; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.output.transform.IRecordTransformer; +import com.fortify.cli.common.rest.unirest.IHttpRequestUpdater; +import com.fortify.cli.common.util.DisableTest; +import com.fortify.cli.common.util.DisableTest.TestType; + +import kong.unirest.HttpRequest; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import picocli.CommandLine.Option; + +public class SSCIssueIncludeMixin implements IHttpRequestUpdater, IRecordTransformer { + @DisableTest(TestType.MULTI_OPT_PLURAL_NAME) + @Option(names = {"--include", "-i"}, split = ",", defaultValue = "visible", descriptionKey = "fcli.ssc.issue.list.includeIssue", paramLabel="") + private Set includes; + + public HttpRequest updateRequest(HttpRequest request) { + // TODO Check whether we can potentially perform any server-side filtering + // to retrieve ONLY suppressed/fixed/hidden issues, although SSC doesn't + // seem to allow server-side filtering on the respective boolean fields. + // See FoDIssueIncludeMixin for additional details. + if ( includes!=null ) { + for ( var include : includes) { + var queryParameterName = include.getRequestParameterName(); + if ( queryParameterName!=null ) { + request = request.queryString(queryParameterName, "true"); + } + } + } + return request; + } + + @Override + public JsonNode transformRecord(JsonNode record) { + // If includes doesn't include 'visible', we return null for any visible (non-suppressed + // & non-fixed) issues. We don't need explicit handling for other cases, as suppressed or + // fixed issues won't be returned by FoD if not explicitly specified through the --include + // option. + return !includes.contains(SSCIssueInclude.visible) + && JsonHelper.evaluateSpelExpression(record, "!hidden && !removed && !suppressed", Boolean.class) + ? null + : addVisibilityProperties((ObjectNode)record); + } + + private ObjectNode addVisibilityProperties(ObjectNode record) { + Map visibility = new LinkedHashMap<>(); + if ( getBoolean(record, "hidden") ) { visibility.put("hidden", "(H)"); } + if ( getBoolean(record, "removed") ) { visibility.put("removed", "(R)"); } + if ( getBoolean(record, "suppressed") ) { visibility.put("suppressed", "(S)"); } + if ( visibility.isEmpty() ) { visibility.put("visible", " "); } + record.put("visibility", String.join(",", visibility.keySet())) + .put("visibilityMarker", String.join("", visibility.values())); + return record; + } + + private boolean getBoolean(ObjectNode record, String propertyName) { + var field = record.get(propertyName); + return field==null ? false : field.asBoolean(); + } + + @RequiredArgsConstructor + public static enum SSCIssueInclude { + visible(null), hidden("showhidden"), removed("showremoved"), suppressed("showsuppressed"); + + @Getter + private final String requestParameterName; + } +} diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueTemplateResolverMixin.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueTemplateResolverMixin.java index 43311d0c4c..dde5e223ef 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueTemplateResolverMixin.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/cli/mixin/SSCIssueTemplateResolverMixin.java @@ -32,11 +32,6 @@ public SSCIssueTemplateDescriptor getIssueTemplateDescriptor(UnirestInstance uni ? null : new SSCIssueTemplateHelper(unirest).getDescriptorByNameOrId(issueTemplateNameOrId, true); } - - public SSCIssueTemplateDescriptor getIssueTemplateDescriptorOrDefault(UnirestInstance unirest) { - SSCIssueTemplateDescriptor descriptor = getIssueTemplateDescriptor(unirest); - return descriptor!=null ? descriptor : SSCIssueTemplateHelper.getDefaultIssueTemplateDescriptor(unirest); - } } public static class OptionalOption extends AbstractSSCIssueTemplateResolverMixin { diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueTemplateHelper.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueTemplateHelper.java index e4b3a4b055..a4b2b01257 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueTemplateHelper.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue/helper/SSCIssueTemplateHelper.java @@ -15,6 +15,8 @@ import java.util.HashMap; import java.util.Map; +import org.apache.commons.lang3.StringUtils; + import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.ssc._common.rest.SSCUrls; @@ -56,6 +58,12 @@ public SSCIssueTemplateDescriptor getDescriptorByNameOrId(String issueTemplateNa return descriptor; } + public SSCIssueTemplateDescriptor getIssueTemplateDescriptorOrDefault(String issueTemplateNameOrId) { + return StringUtils.isBlank(issueTemplateNameOrId) + ? getDefaultIssueTemplateDescriptor() + : getDescriptorByNameOrId(issueTemplateNameOrId, true); + } + /** * If only the default issue template is needed, then this method will be more performant than * new SSCIssueTemplateHelper(unirest).getDefaultIssueTemplateDescriptor(). 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 index 93d5914025..35b4bfe8e0 100644 --- 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 @@ -238,6 +238,12 @@ valueTemplates: Summary generated on: ${#formatDateTime(dateFmt)} + The tables below display summaries for all analysis types available in this application + version, which may include analysis types that were not updated by the current scan job, + for example because this scan job didn't run that type of analysis, or because the analysis + failed. You can verify this by matching the displayed 'Last Scan Date' against the summary + date listed above. + ### Issue Counts ${#join('\n', issueCountsOutput)} 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 index 4bbec2055e..bfb916332a 100644 --- 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 @@ -86,6 +86,7 @@ steps: valueTemplate: results - progress: Processing rule data - forEach: + if: ${ruleCategories!=null} processor: ${#ssc.ruleDescriptionsProcessor(parameters.appversion.id)} name: rule do: 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 index bf60642c70..a1b3b3c663 100644 --- 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 @@ -85,6 +85,7 @@ steps: valueTemplate: results - progress: Processing rule data - forEach: + if: ${ruleCategories!=null} processor: ${#ssc.ruleDescriptionsProcessor(parameters.appversion.id)} name: rule do: 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 e97d36e8a9..a8de12b496 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 @@ -64,12 +64,10 @@ fcli.ssc.session.login.usage.description.0 = This command stores the SSC URL and line or specified through environment variables (common for CI/CD pipeline use) as listed below. fcli.ssc.session.login.usage.description.1 = %nWhen logging in with username and password, this \ command will connect to SSC to generate a UnifiedLoginToken; the user password is not stored \ - by fcli. When logging in with a pre-generated token, this token will be stored as-is. Note \ - that due to SSC limitations, fcli cannot validate whether the given token is a valid SSC token, \ - and doesn't known when it will expire. If an invalid or expired token is given, the 'login' \ - command may still succeed but subsequent 'fcli ssc' commands will fail. Also note that depending \ - on token type, some 'fcli ssc' commands may fail due to the token not being allowed to access \ - the necessary SSC API endpoints. + by fcli. When logging in with a pre-generated token, this token will be stored as-is after \ + checking its validity. Note that depending on token type, some 'fcli ssc' commands may fail \ + due to the token not being allowed to access the necessary SSC API endpoints. Also note that \ + on SSC 23.2 or below, fcli cannot determine when a pre-generated token will expire. fcli.ssc.session.login.usage.description.2 = %nFor interactive use, you may choose to keep the session \ open until it expires. For CI/CD integrations and other automations, you should always issue a \ logout command once work is complete. On shared/non-containerized systems, consider setting \ @@ -89,17 +87,26 @@ fcli.ssc.session.login.token.1 = Environment variables:%n \ ${fcli.env.default.prefix}_SSC_TOKEN fcli.ssc.session.logout.usage.header = Terminate Fortify SSC session. -fcli.ssc.session.logout.usage.description = This command terminates an SSC session previously created \ - through the 'login' command. This command will try to revoke the token passed to or generated by the \ - 'login' command (unless the --no-revoke-token option is specified), and removes the session data from \ - the fcli state data directory. -fcli.ssc.session.logout.no-revoke-token = Don't revoke the token passed to or generated by the 'login' \ - command. If user name and password were specified during login, it is recommended to have the 'logout' \ - command revoke this token, to avoid exceeding SSCs maximum token limit. Note that due to SSC limitations, \ - user credentials will need to be provided to allow for token revocation. If a pre-generated token was \ - specified during login, usually you'll want to pass this option to avoid the pre-generated token from \ - being revoked. - +fcli.ssc.session.logout.usage.description.0 = This command terminates an SSC session previously created \ + through the 'login' command. +fcli.ssc.session.logout.usage.description.1 = %nIf the session was created with user credentials, this command \ + will revoke the automatically generated SSC token unless the --no-revoke-token option is specified. \ + On SSC 24.2 or above, the automatically generated token can be revoked without providing user \ + credentials. For SSC 23.2 or below, user credentials are required to revoke the automatically \ + generated token; the logout command will throw an error if neither user credentials nor --no-revoke-token \ + option is specified, and the session will not be terminated. +fcli.ssc.session.logout.usage.description.2 = %nIf the session was created with a pre-generated token, the \ + session will always be terminated without revoking the pre-generated token. As such, no user credentials need \ + to be provided, and the --no-revoke-token option will have no effect. +fcli.ssc.session.logout.usage.description.3 = %nTo summarize: No user credentials nor --no-revoke-token option \ + should be specified if the session was created with a pre-generated token or if the session is connected to \ + SSC 24.2 or above. Either user credentials or --no-revoke-token option must be specified if the session was \ + created with user credentials AND the session is connected to SSC 23.2 or below. + fcli.ssc.session.logout.no-revoke-token = It is highly recommended to have fcli revoke the token that was \ + automatically generated if the session was created using user credentials to avoid exceeding SSCs maximum \ + token limit. This option is provided for convenience only, to allow the session to be terminated without \ + having to specify user credentials on SSC 23.2 or below. Once most users have upgraded to SSC 24.2 or above, \ + this option will be deprecated or removed. fcli.ssc.session.list.usage.header = List active and expired Fortify SSC sessions. fcli.ssc.session.list.usage.description.0 = This commands lists all SSC sessions created through the `login` \ command, as long as they haven't been explicitly terminated through the 'logout' command. %n @@ -296,6 +303,14 @@ fcli.ssc.app.resolver.nameOrId = Application name or id. Note that numeric value # fcli ssc appversion fcli.ssc.appversion.usage.header = Manage SSC application versions. fcli.ssc.appversion.create.usage.header = Create an application version. +fcli.ssc.appversion.create.usage.description = This command allows for creating a new SSC application \ + version, optionally skipping creation if the application version already exists (see --skip-if-exists \ + option), and/or copying configuration and state of an existing application version (see --copy and \ + --copy-from options). \ + %n%nNote that depending on user and token permissions, SSC may reject some of the application version \ + configuration settings. In particular, user access permissions as specified through the --add-users \ + option or when copying an existing application version may not be applied if the fcli session was \ + created using a CIToken. fcli.ssc.appversion.create.auto-required-attrs = Automatically set a default value for required application \ version attributes. fcli.ssc.appversion.create.description = Application version description. @@ -306,7 +321,8 @@ fcli.ssc.appversion.create.skip-if-exists = Skip application version creation if fcli.ssc.appversion.create.refresh = By default, this command will refresh the source application version's metrics when copying from it. \ Note that for large applications this can lead to an error if the timeout expires. fcli.ssc.appversion.create.copy-options = Comma separated list of elements to copy (Requires --copy-from). By default, all are copied. \ - Allowed values: ${COMPLETION-CANDIDATES}. + Allowed values: ${COMPLETION-CANDIDATES}. The BugTrackerConfiguration and AnalysisProcessingRules values are deprecated and will \ + be removed in a future fcli version. fcli.ssc.appversion.delete.usage.header = Delete an application version. fcli.ssc.appversion.copy-from.usage.header = Copy from options fcli.ssc.appversion.copy-state.usage.header = (PREVIEW) Copy application version state. @@ -343,6 +359,11 @@ fcli.ssc.appversion.refresh-metrics.usage.description = This command allows for version metrics, similar to the little 'refresh' button shown on the SSC application version page \ in case a refresh is required/pending. fcli.ssc.appversion.update.usage.header = Update an application version. +fcli.ssc.appversion.update.usage.description = This command allows for updating an existing SSC \ + application version. \ + %n%nNote that depending on user and token permissions, SSC may reject some of the application version \ + configuration settings. In particular, user access permissions as specified through the --add-users \ + or --rm-users option may not be applied if the fcli session was created using a CIToken. fcli.ssc.appversion.update.name = Update application version name. fcli.ssc.appversion.update.description = Update application version description. fcli.ssc.appversion.resolver.name = Application and version name. @@ -416,12 +437,31 @@ fcli.ssc.issue.count.by = Vulnerability grouping type. See 'fcli ssc issue list- 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.usage.description = This command allows for listing SSC vulnerability data \ + for a given application version. By default, only visible issues will be returned; the --include \ + option can be used to (also) include hidden, suppressed or removed issues. If any such issues are \ + included, the default table output will show (H), (S), and/or (R) for respectively hidden, \ + suppressed and removed issues. \ + %n%nOptionally, additional details may be included in the output using the --embed option, but \ + please note that this may have some impact on performance as this will result in additional \ + HTTP requests to SSC for every page of issues. \ + %n%nIn general, performance of this command is largely dependent on the number of issues \ + being retrieved from SSC. The default table output collects all data in memory until all \ + issues have been processed, so it may take a long time before you see any output, and there's \ + a small risk of running out of memory. Most other output formats (like json, yaml, or csv) \ + output data immediately after each page of issues has been loaded from FoD, resulting in \ + more immediate output. +fcli.ssc.issue.list.output.header.visibilityMarker = 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.list.includeIssue = By default, only visible issues will be returned. This option \ + accepts a comma-separated list to allow (also) removed, suppressed and/or hidden issues to be returned, \ + for example `--include visible,removed` (to return both visible and removed issues) or `--include \ + removed` (to return only removed issues). Allowed values: ${COMPLETION-CANDIDATES}. 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. @@ -571,7 +611,7 @@ 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.list.output.table.options = id,visibilityMarker,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 diff --git a/fcli-other/fcli-doc/src/docs/asciidoc/static/index.adoc b/fcli-other/fcli-doc/src/docs/asciidoc/static/index.adoc index 9bf56caf25..2a32ed7f7a 100644 --- a/fcli-other/fcli-doc/src/docs/asciidoc/static/index.adoc +++ b/fcli-other/fcli-doc/src/docs/asciidoc/static/index.adoc @@ -8,6 +8,8 @@ Release documentation can be accessed through the drop-down menu on the top-righ === Releases Versions +Note that in general, maintenance is performed on the latest release version only; older versions may not receive bug fixes or compatibility updates to support the latest Fortify product releases. Fcli upgrades between minor and patch releases should be straightforward as new releases are mostly backward-compatible with older versions within the same major version range. If you are still on an older major release like v1.x, it is highly recommended to upgrade to fcli v2.x to avoid potential compatibility issues with new Fortify product releases. + ++++
    {% for version in site.data.versions.release %}