diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FileUtils.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FileUtils.java index 9f8da26310..b7702b6087 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FileUtils.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/util/FileUtils.java @@ -25,7 +25,11 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.Comparator; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; @@ -39,6 +43,7 @@ // TODO For now, methods provided in this class are only used by the tools module, // but potentially some methods or the full class could be moved to the common module. public final class FileUtils { + public static final Set execPermissions = PosixFilePermissions.fromString("rwxr-xr-x"); private FileUtils() {} @SneakyThrows @@ -105,15 +110,55 @@ public static final void move(Path source, Path target) { } } + @SneakyThrows + public static final void setAllFilePermissions(Path path, Set permissions, boolean recursive) { + if ( path!=null && Files.exists(path) ) { + if ( Files.isDirectory(path) ) { + try (Stream walk = Files.walk(path)) { + walk.forEach(p->{ + var isDir = Files.isDirectory(p); + if ( isDir && recursive ) { + setAllFilePermissions(p, permissions, recursive); + } else if ( !isDir ) { + setSinglePathPermissions(p, permissions); + } + }); + } + } + } + } + + @SneakyThrows + public static final void setSinglePathPermissions(Path p, Set permissions) { + try { + Files.setPosixFilePermissions(p, permissions); + } catch ( UnsupportedOperationException e ) { + // Log warning? + } + } + + public static final Function defaultExtractPathResolver(Path targetPath, Function sourcePathRewriter) { + return sourcePath->{ + var newSourcePath = sourcePathRewriter==null ? sourcePath : sourcePathRewriter.apply(sourcePath); + var resolvedPath = targetPath.resolve(newSourcePath); + if (!resolvedPath.startsWith(targetPath.normalize())) { + // see: https://snyk.io/research/zip-slip-vulnerability + throw new RuntimeException("Entry with an illegal path: " + sourcePath); + } + return resolvedPath; + }; + } + @SneakyThrows public static final void extractZip(File zipFile, Path targetDir) { + extractZip(zipFile, defaultExtractPathResolver(targetDir, null)); + } + + @SneakyThrows + public static final void extractZip(File zipFile, Function extractPathResolver) { try (FileInputStream fis = new FileInputStream(zipFile); ZipInputStream zipIn = new ZipInputStream(fis)) { for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) { - Path resolvedPath = targetDir.resolve(ze.getName()).normalize(); - if (!resolvedPath.startsWith(targetDir.normalize())) { - // see: https://snyk.io/research/zip-slip-vulnerability - throw new RuntimeException("Entry with an illegal path: " + ze.getName()); - } + Path resolvedPath = extractPathResolver.apply(Path.of(ze.getName())).normalize(); if (ze.isDirectory()) { Files.createDirectories(resolvedPath); } else { @@ -126,13 +171,18 @@ public static final void extractZip(File zipFile, Path targetDir) { @SneakyThrows public static final void extractTarGZ(File tgzFile, Path targetDir) { + extractTarGZ(tgzFile, defaultExtractPathResolver(targetDir, null)); + } + + @SneakyThrows + public static final void extractTarGZ(File tgzFile, Function extractPathResolver) { try (InputStream source = Files.newInputStream(tgzFile.toPath()); GZIPInputStream gzip = new GZIPInputStream(source); TarArchiveInputStream tar = new TarArchiveInputStream(gzip)) { TarArchiveEntry entry; while ((entry = tar.getNextEntry()) != null) { - Path extractTo = targetDir.resolve(entry.getName()); + Path extractTo = extractPathResolver.apply(Path.of(entry.getName())); if(entry.isDirectory()) { Files.createDirectories(extractTo); } else { diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupBaseRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupBaseRequest.java index 16189414ab..84b644a283 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupBaseRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/dast/FoDScanDastAutomatedSetupBaseRequest.java @@ -51,5 +51,5 @@ public static class NetworkAuthenticationType { private Integer timeBoxInHours; @Builder.Default private Boolean requestFalsePositiveRemoval = false; - + private String networkName; // FoDConnect Network Name } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/mixin/FoDSdlcStatusTypeOptions.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/mixin/FoDSdlcStatusTypeOptions.java index 32a609538f..644784928c 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/mixin/FoDSdlcStatusTypeOptions.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/mixin/FoDSdlcStatusTypeOptions.java @@ -21,7 +21,7 @@ //TODO Enum case? See comments in FoDAppTypeOptions public class FoDSdlcStatusTypeOptions { - public enum FoDSdlcStatusType {Development, QA, Production} + public enum FoDSdlcStatusType {Development, QA, Production, Retired} public static final class FoDSdlcStatusTypeIterable extends ArrayList { private static final long serialVersionUID = 1L; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupApiCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupApiCommand.java index 2e63473ad5..6efb0e184d 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupApiCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupApiCommand.java @@ -67,6 +67,8 @@ public class FoDDastAutomatedScanSetupApiCommand extends AbstractFoDScanSetupCom private String password; @Option(names = {"--false-positive-removal"}) private Boolean requestFalsePositiveRemoval; + @Option(names = {"--vpn"}) + private String fodConnectNetwork; @Override protected String getScanType() { @@ -94,11 +96,14 @@ protected HttpRequest getBaseRequest(UnirestInstance unirest, String releaseI networkAuthenticationSettings = new FoDScanDastAutomatedSetupBaseRequest.NetworkAuthenticationType(networkAuthenticationType, username, password); } String timeZoneToUse = FoDScanHelper.validateTimezone(unirest, timezone); - + if (fodConnectNetwork != null) { + // if Fortify Connect network site override environmentFacingType to Internal + environmentFacingType = FoDEnums.DynamicScanEnvironmentFacingType.Internal; + } + FoDScanAssessmentTypeDescriptor assessmentTypeDescriptor = FoDScanHelper.getEntitlementToUse(unirest, releaseId, FoDScanType.Dynamic, assessmentType, entitlementFrequencyTypeMixin.getEntitlementFrequencyType(), entitlementId); entitlementId = assessmentTypeDescriptor.getEntitlementId(); - FoDScanDastAutomatedSetupBaseRequest setupBaseRequest = FoDScanDastAutomatedSetupBaseRequest.builder() .dynamicScanEnvironmentFacingType(environmentFacingType != null ? environmentFacingType : @@ -111,6 +116,7 @@ protected HttpRequest getBaseRequest(UnirestInstance unirest, String releaseI .assessmentTypeId(assessmentTypeDescriptor.getAssessmentTypeId()) .entitlementId(entitlementId) .entitlementFrequencyType(FoDEnums.EntitlementFrequencyType.valueOf(assessmentTypeDescriptor.getFrequencyType())) + .networkName(fodConnectNetwork != null ? fodConnectNetwork : "") .build(); if (apiType.equals(FoDEnums.DastAutomatedApiTypes.Postman)) { diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWebsiteCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWebsiteCommand.java index 0e1402ceff..f435d773b7 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWebsiteCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWebsiteCommand.java @@ -73,6 +73,8 @@ public class FoDDastAutomatedScanSetupWebsiteCommand extends AbstractFoDScanSetu private String macroSecondaryUsername; @Option(names = {"--macro-secondary-password"}) private String macroSecondaryPassword; + @Option(names = {"--vpn"}) + private String fodConnectNetwork; @Override protected String getScanType() { @@ -116,6 +118,10 @@ protected HttpRequest getBaseRequest(UnirestInstance unirest, String releaseI loginMacroFileCreationSettings = new FoDScanDastAutomatedSetupWebsiteRequest.LoginMacroFileCreationType(macroPrimaryUsername, macroPrimaryPassword, macroSecondaryUsername, macroSecondaryPassword); } + if (fodConnectNetwork != null) { + // if Fortify Connect network site override environmentFacingType to Internal + environmentFacingType = FoDEnums.DynamicScanEnvironmentFacingType.Internal; + } FoDScanAssessmentTypeDescriptor assessmentTypeDescriptor = FoDScanHelper.getEntitlementToUse(unirest, releaseId, FoDScanType.Dynamic, assessmentType, entitlementFrequencyTypeMixin.getEntitlementFrequencyType(), entitlementId); @@ -129,7 +135,7 @@ protected HttpRequest getBaseRequest(UnirestInstance unirest, String releaseI .restrictToDirectoryAndSubdirectories(restrictToDirectoryAndSubdirectories != null ? restrictToDirectoryAndSubdirectories : false) .policy(scanPolicy) .timeBoxInHours(timebox) - .dynamicScanEnvironmentFacingType(environmentFacingType != null ? environmentFacingType : FoDEnums.DynamicScanEnvironmentFacingType.Internal) + .dynamicScanEnvironmentFacingType(environmentFacingType != null ? environmentFacingType : FoDEnums.DynamicScanEnvironmentFacingType.External) .timeZone(timeZoneToUse) .requiresNetworkAuthentication(requiresNetworkAuthentication) .networkAuthenticationSettings(networkAuthenticationSettings) @@ -139,6 +145,7 @@ protected HttpRequest getBaseRequest(UnirestInstance unirest, String releaseI .requestFalsePositiveRemoval(requestFalsePositiveRemoval != null ? requestFalsePositiveRemoval : false) .requestLoginMacroFileCreation(requiresLoginMacroCreation) .loginMacroFileCreationDetails(loginMacroFileCreationSettings) + .networkName(fodConnectNetwork != null ? fodConnectNetwork : "") .build(); return unirest.put(FoDUrls.DAST_AUTOMATED_SCANS + "/website-scan-setup") diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWorkflowCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWorkflowCommand.java index 55fcb13874..1d47e919cb 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWorkflowCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/dast_scan/cli/cmd/FoDDastAutomatedScanSetupWorkflowCommand.java @@ -55,6 +55,8 @@ public class FoDDastAutomatedScanSetupWorkflowCommand extends AbstractFoDScanSet private String password; @Option(names = {"--false-positive-removal"}) private Boolean requestFalsePositiveRemoval; + @Option(names = {"--vpn"}) + private String fodConnectNetwork; @Override protected String getScanType() { @@ -85,7 +87,11 @@ protected HttpRequest getBaseRequest(UnirestInstance unirest, String releaseI networkAuthenticationSettings = new FoDScanDastAutomatedSetupWorkflowRequest.NetworkAuthenticationType(networkAuthenticationType, username, password); } String timeZoneToUse = FoDScanHelper.validateTimezone(unirest, timezone); - + if (fodConnectNetwork != null) { + // if Fortify Connect network site override environmentFacingType to Internal + environmentFacingType = FoDEnums.DynamicScanEnvironmentFacingType.Internal; + } + FoDScanAssessmentTypeDescriptor assessmentTypeDescriptor = FoDScanHelper.getEntitlementToUse(unirest, releaseId, FoDScanType.Dynamic, assessmentType, entitlementFrequencyTypeMixin.getEntitlementFrequencyType(), entitlementId); entitlementId = assessmentTypeDescriptor.getEntitlementId(); @@ -100,6 +106,7 @@ protected HttpRequest getBaseRequest(UnirestInstance unirest, String releaseI .entitlementId(entitlementId) .entitlementFrequencyType(FoDEnums.EntitlementFrequencyType.valueOf(assessmentTypeDescriptor.getFrequencyType())) .requestFalsePositiveRemoval(requestFalsePositiveRemoval != null ? requestFalsePositiveRemoval : false) + .networkName(fodConnectNetwork != null ? fodConnectNetwork : "") .build(); return unirest.put(FoDUrls.DAST_AUTOMATED_SCANS + "/workflow-scan-setup") diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanCommands.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanCommands.java index 908de6d1c4..a789109431 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanCommands.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanCommands.java @@ -27,7 +27,7 @@ FoDMastScanGetConfigCommand.class, FoDMastScanImportCommand.class, FoDMastScanListCommand.class, - //FoDMastScanSetupCommand.class, + FoDMastScanSetupCommand.class, FoDMastScanStartCommand.class, FoDMastScanWaitForCommand.class, } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanSetupCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanSetupCommand.java new file mode 100644 index 0000000000..123ac7c29c --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/cli/cmd/FoDMastScanSetupCommand.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright 2021, 2023 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + *******************************************************************************/ + +package com.fortify.cli.fod.mast_scan.cli.cmd; + +import java.time.format.DateTimeFormatter; +import java.util.Objects; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fortify.cli.common.cli.util.CommandGroup; +import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; +import com.fortify.cli.common.output.transform.IRecordTransformer; +import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin; +import com.fortify.cli.common.util.DisableTest; +import com.fortify.cli.common.util.DisableTest.TestType; +import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; +import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; +import com.fortify.cli.fod._common.scan.cli.mixin.FoDEntitlementFrequencyTypeMixins; +import com.fortify.cli.fod._common.scan.helper.FoDScanType; +import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; +import com.fortify.cli.fod.release.helper.FoDReleaseAssessmentTypeDescriptor; +import com.fortify.cli.fod.release.helper.FoDReleaseAssessmentTypeHelper; +import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; +import com.fortify.cli.fod.mast_scan.helper.FoDScanConfigMobileDescriptor; +import com.fortify.cli.fod.mast_scan.helper.FoDScanConfigMobileHelper; +import com.fortify.cli.fod.mast_scan.helper.FoDScanConfigMobileSetupRequest; + +import kong.unirest.UnirestInstance; +import lombok.Getter; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command(name = OutputHelperMixins.Setup.CMD_NAME, hidden = false) @CommandGroup("*-scan-setup") +@DisableTest(TestType.CMD_DEFAULT_TABLE_OPTIONS_PRESENT) +public class FoDMastScanSetupCommand extends AbstractFoDJsonNodeOutputCommand implements IRecordTransformer, IActionCommandResultSupplier { + private static final Log LOG = LogFactory.getLog(FoDMastScanStartCommand.class); + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm"); + @Getter @Mixin private OutputHelperMixins.Start outputHelper; + + @Mixin + private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins + @Mixin + private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver; + + @Option(names = {"--assessment-type"}, required = true) + private String mobileAssessmentType; // Plain text name as custom assessment types can be created + @Option(names = {"--entitlement-id"}) + private Integer entitlementId; + @Mixin private FoDEntitlementFrequencyTypeMixins.RequiredOption entitlementFrequencyTypeMixin; + private enum MobileFrameworks { iOS, Android } + @Option(names = {"--framework"}, required = true) + private MobileFrameworks mobileFramework; + @Option(names = {"--timezone"}, defaultValue = "UTC") + private String timezone; + private enum MobileAuditPreferenceTypes { Manual, None } + @Option(names = {"--audit-preference"}, defaultValue = "None") + private MobileAuditPreferenceTypes auditPreferenceType; + private enum MobilePlatforms { Phone, Tablet, Both } + @Option(names = {"--platform"}, required = true) + private MobilePlatforms mobilePlatform; + @Option(names={"--skip-if-exists"}) + private Boolean skipIfExists = false; + + @Mixin private ProgressWriterFactoryMixin progressWriterFactory; + + @Override + public JsonNode getJsonNode(UnirestInstance unirest) { + try (var progressWriter = progressWriterFactory.create()) { + var releaseDescriptor = releaseResolver.getReleaseDescriptor(unirest); + var setupDescriptor = FoDScanConfigMobileHelper.getSetupDescriptor(unirest, releaseDescriptor.getReleaseId()); + if ( skipIfExists && setupDescriptor.getAssessmentTypeId()!=0 ) { + return setupDescriptor.asObjectNode().put("__action__", "SKIPPED_EXISTING"); + } else { + return setup(unirest, releaseDescriptor, setupDescriptor).asObjectNode(); + } + } + } + + private FoDScanConfigMobileDescriptor setup(UnirestInstance unirest, FoDReleaseDescriptor releaseDescriptor, FoDScanConfigMobileDescriptor currentSetup) { + var relId = releaseDescriptor.getReleaseId(); + + LOG.info("Finding appropriate entitlement to use."); + + var atd = FoDReleaseAssessmentTypeHelper.getAssessmentTypeDescriptor(unirest, relId, FoDScanType.Mobile, + entitlementFrequencyTypeMixin.getEntitlementFrequencyType(), mobileAssessmentType); + Integer assessmentTypeId = atd.getAssessmentTypeId(); + Integer entitlementIdToUse = atd.getEntitlementId(); + + validateEntitlement(currentSetup, entitlementIdToUse, relId, atd); + LOG.info("Release will be usig entitlement " + entitlementIdToUse); + + FoDScanConfigMobileSetupRequest setupMastScanRequest = FoDScanConfigMobileSetupRequest.builder() + .assessmentTypeId(assessmentTypeId) + .frameworkType(mobileFramework.name()) + .platformType(mobilePlatform.name()) + .auditPreferenceType(auditPreferenceType.name()) + .timeZone(timezone).build(); + + return FoDScanConfigMobileHelper.setupScan(unirest, releaseDescriptor, setupMastScanRequest); + } + + private void validateEntitlement(FoDScanConfigMobileDescriptor currentSetup, Integer entitlementIdToUse, String relId, FoDReleaseAssessmentTypeDescriptor atd) { + // validate entitlement specified or currently in use against assessment type found + if (entitlementId != null && entitlementId > 0) { + // check if "entitlement id" explicitly matches what has been found + if (!Objects.equals(entitlementIdToUse, entitlementId)) { + throw new IllegalArgumentException("Cannot find appropriate assessment type for use with entitlement: " + entitlementId + "=" + entitlementIdToUse); + } + } else { + if (currentSetup.getEntitlementId() != null && currentSetup.getEntitlementId() > 0) { + // check if "entitlement id" is already configured + if (!Objects.equals(entitlementIdToUse, currentSetup.getEntitlementId())) { + LOG.warn("Changing current release entitlement from " + currentSetup.getEntitlementId()); + } + } + } + // check if the entitlement is still valid + FoDReleaseAssessmentTypeHelper.validateEntitlement(relId, atd); + } + + @Override + public JsonNode transformRecord(JsonNode record) { + FoDReleaseDescriptor releaseDescriptor = releaseResolver.getReleaseDescriptor(getUnirestInstance()); + return ((ObjectNode)record) + .put("scanType", mobileAssessmentType) + .put("setupType", auditPreferenceType.name()) + .put("applicationName", releaseDescriptor.getApplicationName()) + .put("releaseName", releaseDescriptor.getReleaseName()) + .put("microserviceName", releaseDescriptor.getMicroserviceName()); + } + + @Override + public String getActionCommandResult() { + return "SETUP"; + } + + @Override + public boolean isSingular() { + return true; + } + +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/helper/FoDScanConfigMobileHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/helper/FoDScanConfigMobileHelper.java index c68906c194..c2942ed3cc 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/helper/FoDScanConfigMobileHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/helper/FoDScanConfigMobileHelper.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.fod._common.rest.FoDUrls; +import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; import kong.unirest.UnirestInstance; @@ -27,4 +28,13 @@ public static final FoDScanConfigMobileDescriptor getSetupDescriptor(UnirestInst .getBody(); return JsonHelper.treeToValue(body, FoDScanConfigMobileDescriptor.class); } + + public static final FoDScanConfigMobileDescriptor setupScan(UnirestInstance unirest, FoDReleaseDescriptor releaseDescriptor, FoDScanConfigMobileSetupRequest setupMobileScanRequest) { + var releaseId = releaseDescriptor.getReleaseId(); + unirest.put(FoDUrls.MOBILE_SCANS + "/scan-setup") + .routeParam("relId", releaseId) + .body(setupMobileScanRequest) + .asString().getBody(); + return getSetupDescriptor(unirest, releaseId); + } } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/helper/FoDScanConfigMobileSetupRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/helper/FoDScanConfigMobileSetupRequest.java new file mode 100644 index 0000000000..f467f3697a --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/mast_scan/helper/FoDScanConfigMobileSetupRequest.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright 2021, 2023 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + *******************************************************************************/ + +package com.fortify.cli.fod.mast_scan.helper; + +import com.formkiq.graalvm.annotations.Reflectable; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Reflectable @NoArgsConstructor @AllArgsConstructor +@Data @Builder +public class FoDScanConfigMobileSetupRequest { + private Integer assessmentTypeId; + private String auditPreferenceType; + private String frameworkType; + private String platformType; + private String timeZone; +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/helper/FoDReleaseAssessmentTypeHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/helper/FoDReleaseAssessmentTypeHelper.java index c1de4d1e1c..e49fdd6308 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/helper/FoDReleaseAssessmentTypeHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/helper/FoDReleaseAssessmentTypeHelper.java @@ -13,7 +13,9 @@ package com.fortify.cli.fod.release.helper; import java.time.Instant; +import java.util.Arrays; import java.util.Date; +import java.util.Optional; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -25,6 +27,7 @@ import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.scan.helper.FoDScanType; import com.fortify.cli.fod._common.util.FoDEnums; +import com.fortify.cli.fod._common.util.FoDEnums.EntitlementFrequencyType; import kong.unirest.GetRequest; import kong.unirest.UnirestInstance; @@ -55,6 +58,18 @@ public static final FoDReleaseAssessmentTypeDescriptor[] getAssessmentTypes(Unir return JsonHelper.treeToValue(assessmentTypes, FoDReleaseAssessmentTypeDescriptor[].class); } + public static final FoDReleaseAssessmentTypeDescriptor getAssessmentTypeDescriptor(UnirestInstance unirest, String relId, + FoDScanType scanType, EntitlementFrequencyType entFreqType, String assessmentType) { + // find an appropriate assessment type to use + Optional atd = Arrays.stream( + FoDReleaseAssessmentTypeHelper.getAssessmentTypes(unirest, + relId, scanType, entFreqType, + false, true) + ).filter(n -> n.getName().equals(assessmentType)) + .findFirst(); + return atd.orElseThrow(()->new IllegalArgumentException("Cannot find appropriate assessment type for specified options.")); + } + public final static void validateEntitlement(String relId, FoDReleaseAssessmentTypeDescriptor atd) { if (atd == null || atd.getAssessmentTypeId() == null || atd.getAssessmentTypeId() <= 0) { diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanSetupCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanSetupCommand.java index 4bf5d20da3..16ae64b315 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanSetupCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanSetupCommand.java @@ -13,10 +13,7 @@ package com.fortify.cli.fod.sast_scan.cli.cmd; -import java.util.Arrays; import java.util.Objects; -import java.util.Optional; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -112,7 +109,8 @@ private FoDScanConfigSastDescriptor setup(UnirestInstance unirest, FoDReleaseDes LOG.info("Finding appropriate entitlement to use."); - var atd = getAssessmentTypeDescriptor(unirest, relId); + var atd = FoDReleaseAssessmentTypeHelper.getAssessmentTypeDescriptor(unirest, relId, FoDScanType.Static, + entitlementFrequencyTypeMixin.getEntitlementFrequencyType(), staticAssessmentType); var assessmentTypeId = atd.getAssessmentTypeId(); var entitlementIdToUse = atd.getEntitlementId(); @@ -182,18 +180,6 @@ private void validateEntitlement(FoDScanConfigSastDescriptor currentSetup, Integ FoDReleaseAssessmentTypeHelper.validateEntitlement(relId, atd); } - private FoDReleaseAssessmentTypeDescriptor getAssessmentTypeDescriptor(UnirestInstance unirest, String relId) { - // find an appropriate assessment type to use - Optional atd = Arrays.stream( - FoDReleaseAssessmentTypeHelper.getAssessmentTypes(unirest, - relId, FoDScanType.Static, - entitlementFrequencyTypeMixin.getEntitlementFrequencyType(), - false, true) - ).filter(n -> n.getName().equals(staticAssessmentType)) - .findFirst(); - return atd.orElseThrow(()->new IllegalArgumentException("Cannot find appropriate assessment type for specified options.")); - } - @Override public JsonNode transformRecord(JsonNode record) { FoDReleaseDescriptor releaseDescriptor = releaseResolver.getReleaseDescriptor(getUnirestInstance()); 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 bf16abb75d..dd2cd8b05a 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,11 +30,11 @@ 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 + # Update Note: although open source counts are now available on the release object, the date of last open source scan is not so still need below - 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 :( + # we can't filter on scan type so we have to trawl through scans to find "latest" open source scan :( uri: /api/v3/releases/${r.releaseId}/scans?limit=50 query: orderBy: completedDateTime @@ -50,32 +50,6 @@ steps: 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']?.fieldFilterValues?.^[#this.value == "Critical"]?.count?:0} - - set: - - name: ossHigh - value: ${issues_raw.filters.^[#this.fieldName == 'severity']?.fieldFilterValues?.^[#this.value == "High"]?.count?:0} - - set: - - name: ossMedium - value: ${issues_raw.filters.^[#this.fieldName == 'severity']?.fieldFilterValues?.^[#this.value == "Medium"]?.count?:0} - - set: - - name: ossLow - value: ${issues_raw.filters.^[#this.fieldName == 'severity']?.fieldFilterValues?.^[#this.value == "Low"]?.count?:0} - # replace up to here - write: - to: ${parameters.file} @@ -85,7 +59,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 ... +# Note: update ossScanDate when it is available on release object ... valueTemplates: - name: summary-md contents: | @@ -105,7 +79,7 @@ valueTemplates: | **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))+' |'} + | **Open Source** | ${(#isBlank(ossScanDate)?#fmt('%-16s', 'N/A'):#formatDateTime(dateFmt, ossScanDate)) +' | '+#fmt('%8s', r.openSourceCritical) +' | '+#fmt('%8s', r.openSourceHigh) +' | '+#fmt('%8s', r.openSourceMedium) +' | '+#fmt('%8s', r.openSourceLow) +' |'} + | **Total** | | ${#fmt('%8s', r.staticCritical+r.dynamicCritical+r.mobileCritical+r.openSourceCritical)+' | '+#fmt('%8s', r.staticHigh+r.dynamicHigh+r.mobileHigh+r.openSourceHigh)+' | '+#fmt('%8s', r.staticMedium+r.dynamicMedium+r.mobileMedium+r.openSourceMedium)+' | '+#fmt('%8s', r.staticLow+r.dynamicLow+r.mobileLow+r.openSourceLow)+' |'} \ 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 1a4a0078de..cfb26308c2 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 @@ -36,7 +36,7 @@ fcli.fod.app.app-criticality = The business criticality of the application. Vali fcli.fod.release.sdlc-status = The SDLC lifecycle status of the release. Valid values: ${COMPLETION-CANDIDATES} fcli.fod.scan.time-period = Time period to retrieve results over. Valid values: ${COMPLETION-CANDIDATES}. Default value is Last30. fcli.fod.scan.entitlement-preference = The entitlement preference to use. Valid values: ${COMPLETION-CANDIDATES}. Default is SubscriptionFirstThenSingleScan. -fcli.fod.scan.assessment-type = The assessment type to use. Use 'release list-assessment-types' to find valid values. +fcli.fod.scan.assessment-type = The assessment type to use. Use 'fod release lsat' to find valid values. fcli.fod.scan.in-progress-action = The action to use if a scan is already in progress. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.scan.remediation-preference = The remediation preference to use. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.microservice.resolver.name = Microservice name in the format :. @@ -529,7 +529,7 @@ fcli.fod.sast-scan.setup.usage.description.3 = For the '--technology-stack' and fcli.fod.sast-scan.setup.entitlement-frequency = The Entitlement Frequency to use. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.sast-scan.setup.entitlement-id = Entitlement Id to use. If not specified Frequency and Assessment Type will be used to find one. fcli.fod.sast-scan.setup.validate-entitlement = Check if the entitlement assigned is still valid, e.g. it has not expired. -fcli.fod.sast-scan.setup.assessment-type = The type of Static assessment to carry out. Use 'release list-assessment-types' to find valid values. +fcli.fod.sast-scan.setup.assessment-type = The type of Static assessment to carry out. Use 'fod release lsat' to find valid values. fcli.fod.sast-scan.setup.technology-stack = The technology stack of the application. Default value: ${DEFAULT-VALUE}. fcli.fod.sast-scan.setup.language-level = The language level of the technology stack (if needed). fcli.fod.sast-scan.setup.oss = Perform Open Source Analysis scan. @@ -672,6 +672,7 @@ fcli.fod.dast-scan.setup-website.macro-primary-username = Login macro username f fcli.fod.dast-scan.setup-website.macro-primary-password = Login macro password for the primary user. fcli.fod.dast-scan.setup-website.macro-secondary-username = Login macro username for the secondary user. fcli.fod.dast-scan.setup-website.macro-secondary-password = Login macro password for the secondary user. +fcli.fod.dast-scan.setup-website.vpn = Fortify Connect network name to use for site-to-site VPN. If specified, environment will be set to Internal. fcli.fod.dast-scan.setup-workflow.file = A Workflow file to upload and use for authentication in the website scan. fcli.fod.dast-scan.setup-workflow.allowed-hosts = The FQDN and port of any hosts that that you want to be scanned, e.g. "test.mysite.com:443". @@ -688,6 +689,7 @@ fcli.fod.dast-scan.setup-workflow.network-auth-type = The Network Authentication fcli.fod.dast-scan.setup-workflow.network-username = ${fcli.fod.dast-scan.setup-website.network-username} fcli.fod.dast-scan.setup-workflow.network-password = ${fcli.fod.dast-scan.setup-website.network-password} fcli.fod.dast-scan.setup-workflow.false-positive-removal = ${fcli.fod.dast-scan.setup-website.false-positive-removal} +fcli.fod.dast-scan.setup-workflow.vpn = Fortify Connect network name to use for site-to-site VPN. If specified, environment will be set to Internal. fcli.fod.dast-scan.setup-api.type = The type of API to scan. Valid Values: ${COMPLETION-CANDIDATES} fcli.fod.dast-scan.setup-api.file = An OpenAPI specification, Postman collection, GraphQL schema file or GRPC proto file. @@ -710,6 +712,7 @@ fcli.fod.dast-scan.setup-api.network-auth-type = The Network Authentication type fcli.fod.dast-scan.setup-api.network-username = ${fcli.fod.dast-scan.setup-website.network-username} fcli.fod.dast-scan.setup-api.network-password = ${fcli.fod.dast-scan.setup-website.network-password} fcli.fod.dast-scan.setup-api.false-positive-removal = ${fcli.fod.dast-scan.setup-website.false-positive-removal} +fcli.fod.dast-scan.setup-api.vpn = Fortify Connect network name to use for site-to-site VPN. If specified, environment will be set to Internal. # fcli fod mast-scan fcli.fod.mast-scan.usage.header = Manage FoD MAST scans. @@ -724,11 +727,10 @@ fcli.fod.mast-scan.output.header.microserviceName = Microservice fcli.fod.mast-scan.output.header.releaseName = Release fcli.fod.mast-scan.cancel.usage.header = Cancel a MAST scan. fcli.fod.mast-scan.get.usage.header = Get MAST scan details. -fcli.fod.mast-scan.get-config.usage.header = (PREVIEW) Get current MAST scan configuration. -fcli.fod.mast-scan.get-config.usage.description = This command 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. \ - Please note: there is currently no API endpoint for this command and so it will fail with HTTP 405 error until this endpoint is available. +fcli.fod.mast-scan.get-config.usage.header = Get current MAST scan configuration. +fcli.fod.mast-scan.get-config.usage.description = This command will retrieve the current MAST scan configuration. \ + The API endpoint used by this command currently only returns a subset of information for Mobile+ Assessments. \ + It is therefore recommended to use this command for Mobile Assessments only. fcli.fod.mast-scan.list.usage.header = List MAST scans. fcli.fod.mast-scan.list.status = ${fcli.fod.scan.list.status} fcli.fod.mast-scan.list.type = ${fcli.fod.scan.list.type} @@ -740,22 +742,18 @@ fcli.fod.mast-scan.wait-for.usage.description.3 = ${fcli.fod.scan.wait-for.usage fcli.fod.mast-scan.wait-for.until = ${fcli.fod.scan.wait-for.until} fcli.fod.mast-scan.wait-for.while = ${fcli.fod.scan.wait-for.while} fcli.fod.mast-scan.wait-for.any-state = ${fcli.fod.scan.wait-for.any-state} -fcli.fod.mast-scan.start.usage.header = (PREVIEW) Start a new MAST scan. -fcli.fod.mast-scan.start.usage.description.0 = 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.mast-scan.start.usage.description.1 = The scan will need to have been previously setup using the FoD UI or the \ - 'fod mast-scan setup' command. -fcli.fod.mast-scan.start.usage.description.2 = To correctly start a scan you will need to provide the name of the \ - assessment type using the '--assessment-type=xxx' option. Since assessment types can potentially be configured \ - differently for each tenant, you can find the correct name using the 'fod rest lookup AssessmentTypes' command. -fcli.fod.mast-scan.start.usage.description.3 = The scan will need to have been previously setup using the FoD UI or the \ - 'fod dast-scan setup' command. -fcli.fod.mast-scan.start.usage.description.4 = If you know the Id of an entitlement that you want to use then you \ +fcli.fod.mast-scan.start.usage.header = Start a new MAST scan. +fcli.fod.mast-scan.start.usage.description.0 = To correctly start a scan you will need to provide the name of the \ + assessment type using the '--assessment-type=xxx' option. This will usually be "Mobile Assessment" but since assessment \ + types can potentially be configured differently for each tenant, you can find the correct name using the 'fod release lsat' \ + command for the release. +fcli.fod.mast-scan.start.usage.description.1 = If you know the Id of an entitlement that you want to use then you \ can supply it to the '--entitlement-id=xxx' option. If not, you can supply both '--assessment-type' and \ '--entitlement-frequency' options and the command will try to find an appropriate entitlement. +fcli.fod.mast-scan.start.usage.description.2 = The scan will need to have been previously setup using the FoD UI or the \ + 'fod mast-scan setup' command. fcli.fod.mast-scan.start.start-date = ${fcli.fod.sast-scan.start.start-date} -fcli.fod.mast-scan.start.assessment-type = The type of MAST assessment to carry out. Use 'fod rest lookup AssessmentTypes' to find valid values. +fcli.fod.mast-scan.start.assessment-type = The type of MAST assessment to carry out. Use 'fod release lsat' to find valid values. fcli.fod.mast-scan.start.entitlement-id = ${fcli.fod.sast-scan.start.entitlement-id} fcli.fod.mast-scan.start.entitlement-frequency = The Entitlement Frequency to use. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.mast-scan.start.notes = ${fcli.fod.sast-scan.start.notes} @@ -763,6 +761,26 @@ fcli.fod.mast-scan.start.file = Absolute path of the mobile application file to fcli.fod.mast-scan.start.framework = The Mobile Framework to use. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.mast-scan.start.platform = The Mobile Platform to use. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.mast-scan.start.timezone = The timezone to use for starting the scan - default is UTC. Use 'fod rest lookup TimeZones' to see the values. +fcli.fod.mast-scan.setup.usage.header = (PREVIEW) Configure MAST scan details for the release. +fcli.fod.mast-scan.setup.usage.description.0 = 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.mast-scan.setup.usage.description.1 = To correctly setup a scan you will need to provide the name of the \ + assessment type using the '--assessment-type=xxx' option. Since assessment types can potentially be configured \ + differently for each tenant, you can find the correct name using the 'fod release lsat' command. +fcli.fod.mast-scan.setup.usage.description.2 = If you know the Id of an entitlement that you want to use then you \ + can supply it to the '--entitlement-id=xxx' option. If not, you can supply both '--assessment-type' and \ + '--entitlement-frequency' options and the command will try to find an appropriate entitlement. +fcli.fod.mast-scan.setup.entitlement-frequency = The Entitlement Frequency to use. Valid values: ${COMPLETION-CANDIDATES}. +fcli.fod.mast-scan.setup.entitlement-id = Entitlement Id to use. If not specified Frequency and Assessment Type will be used to find one. +fcli.fod.mast-scan.setup.validate-entitlement = Check if the entitlement assigned is still valid, e.g. it has not expired. +fcli.fod.mast-scan.setup.assessment-type = The type of Static assessment to carry out. Use 'fod release lsat' to find valid values. +fcli.fod.mast-scan.setup.framework = The Mobile Framework to use. Valid values: ${COMPLETION-CANDIDATES}. +fcli.fod.mast-scan.setup.platform = The Mobile Platform to use. Valid values: ${COMPLETION-CANDIDATES}. +fcli.fod.mast-scan.setup.timezone = The timezone to use for starting the scan - default is UTC. Use 'fod rest lookup TimeZones' to see the values. +fcli.fod.mast-scan.setup.audit-preference = Audit preference, e.g. Manual or None. Default value: None. +fcli.fod.mast-scan.setup.skip-if-exists = Skip setup if a scan has already been set up. If not specified, any existing scan \ + setup will be replaced based on the given setup options. fcli.fod.mast-scan.import.usage.header = Import existing MAST scan results (from an FPR file). fcli.fod.mast-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.mast-scan.import.file = FPR file containing existing MAST scan results to be imported. diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstaller.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstaller.java index 8e102bacb0..427929b7a4 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstaller.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/_common/helper/ToolInstaller.java @@ -18,17 +18,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Stream; import com.fortify.cli.common.crypto.helper.SignatureHelper; import com.fortify.cli.common.progress.helper.IProgressWriterI18n; @@ -47,7 +43,6 @@ @Builder public final class ToolInstaller { - private static final Set binPermissions = PosixFilePermissions.fromString("rwxr-xr-x"); @Getter private final String toolName; @Getter private final String requestedVersion; @Getter private final String defaultPlatform; @@ -104,7 +99,7 @@ public final Path getBinPath() { } public final Path getGlobalBinPath() { - return _globalBinPath.get(()->globalBinPathProvider.apply(this)); + return _globalBinPath.get(()->globalBinPathProvider==null?null:globalBinPathProvider.apply(this)); } public final String getToolVersion() { @@ -187,7 +182,7 @@ public final void installGlobalBinScript(BinScriptType type, String globalBinScr if ( Files.exists(scriptTargetFilePath) ) { var replacements = getResourceReplacementsMap(globalBinPath.getParent(), scriptTargetFilePath); installResource(resourceFile, globalBinScriptPath, true, replacements); - updateFilePermissions(globalBinScriptPath); + FileUtils.setSinglePathPermissions(globalBinScriptPath, FileUtils.execPermissions); } } } @@ -195,7 +190,7 @@ public final void installGlobalBinScript(BinScriptType type, String globalBinScr private final ToolInstallationResult install(ToolDefinitionArtifactDescriptor artifactDescriptor) { try { - preInstallAction.accept(this); + if ( preInstallAction!=null ) { preInstallAction.accept(this); } var versionDescriptor = getVersionDescriptor(); warnIfDifferentTargetPath(); if ( !hasMatchingTargetPath(getVersionDescriptor()) ) { @@ -203,9 +198,11 @@ private final ToolInstallationResult install(ToolDefinitionArtifactDescriptor ar downloadAndExtract(artifactDescriptor); } var result = new ToolInstallationResult(toolName, versionDescriptor, artifactDescriptor, createAndSaveInstallationDescriptor()); - progressWriter.writeProgress("Running post-install actions"); - postInstallAction.accept(this, result); - updateBinPermissions(result.getInstallationDescriptor().getBinPath()); + if ( postInstallAction!=null ) { + progressWriter.writeProgress("Running post-install actions"); + postInstallAction.accept(this, result); + } + FileUtils.setAllFilePermissions(result.getInstallationDescriptor().getBinPath(), FileUtils.execPermissions, false); writeInstallationInfo(result); return result; } catch ( IOException e ) { @@ -288,24 +285,6 @@ private final void checkEmptyTargetPath() throws IOException { } } - private static final void updateBinPermissions(Path binPath) throws IOException { - if ( binPath!=null ) { - try (Stream walk = Files.walk(binPath)) { - walk.forEach(ToolInstaller::updateFilePermissions); - } - } - } - - // TODO Move this method to FileUtils or similar, as it's also used by AbstractToolInstallCommand - @SneakyThrows - public static final void updateFilePermissions(Path p) { - try { - Files.setPosixFilePermissions(p, binPermissions); - } catch ( UnsupportedOperationException e ) { - // Log warning? - } - } - // TODO Is there a standard Java class for this? private static final class LazyObject { private T value = null; diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionVersionDescriptor.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionVersionDescriptor.java index cf86c4eca5..6962d3d6ab 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionVersionDescriptor.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/definitions/helper/ToolDefinitionVersionDescriptor.java @@ -32,4 +32,5 @@ public final class ToolDefinitionVersionDescriptor { private String[] aliases; private boolean stable; private Map binaries; + private Map extraProperties; } \ No newline at end of file diff --git a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sc_client/cli/cmd/ToolSCClientInstallCommand.java b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sc_client/cli/cmd/ToolSCClientInstallCommand.java index 8c26e74bb9..0b51d2b017 100644 --- a/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sc_client/cli/cmd/ToolSCClientInstallCommand.java +++ b/fcli-core/fcli-tool/src/main/java/com/fortify/cli/tool/sc_client/cli/cmd/ToolSCClientInstallCommand.java @@ -12,20 +12,29 @@ *******************************************************************************/ package com.fortify.cli.tool.sc_client.cli.cmd; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import com.fortify.cli.common.crypto.helper.SignatureHelper; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.rest.unirest.UnirestHelper; +import com.fortify.cli.common.util.FileUtils; import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.tool._common.cli.cmd.AbstractToolInstallCommand; import com.fortify.cli.tool._common.helper.ToolInstaller; import com.fortify.cli.tool._common.helper.ToolInstaller.BinScriptType; +import com.fortify.cli.tool._common.helper.ToolInstaller.DigestMismatchAction; import com.fortify.cli.tool._common.helper.ToolInstaller.ToolInstallationResult; +import com.fortify.cli.tool._common.helper.ToolPlatformHelper; +import com.fortify.cli.tool.definitions.helper.ToolDefinitionArtifactDescriptor; +import com.fortify.cli.tool.definitions.helper.ToolDefinitionsHelper; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; @@ -36,6 +45,8 @@ public class ToolSCClientInstallCommand extends AbstractToolInstallCommand { @Getter @Mixin private OutputHelperMixins.Install outputHelper; @Getter private String toolName = ToolSCClientCommands.TOOL_NAME; @Option(names= {"-t", "--client-auth-token"}) private String clientAuthToken; + @Option(names= {"--with-jre"}) private boolean withJre; + @Option(names= {"--jre-platform"}) private String jrePlatform; @Override protected String getDefaultArtifactType() { @@ -45,6 +56,7 @@ protected String getDefaultArtifactType() { @Override @SneakyThrows protected void postInstall(ToolInstaller installer, ToolInstallationResult installationResult) { updateClientAuthToken(installer.getTargetPath()); + if ( withJre || StringUtils.isNotBlank(jrePlatform) ) { installJre(installer); } installer.installGlobalBinScript(BinScriptType.bash, "scancentral", "bin/scancentral"); installer.installGlobalBinScript(BinScriptType.bat, "scancentral.bat", "bin/scancentral.bat"); installer.installGlobalBinScript(BinScriptType.bash, "pwtool", "bin/pwtool"); @@ -61,4 +73,88 @@ private void updateClientAuthToken(Path installPath) throws IOException { StandardOpenOption.TRUNCATE_EXISTING); } } + + private void installJre(ToolInstaller scClientInstaller) throws IOException { + var platform = StringUtils.isNotBlank(jrePlatform) ? jrePlatform : ToolPlatformHelper.getPlatform(); + new SCClientJREInstaller(scClientInstaller).installJre(platform); + } + + @RequiredArgsConstructor // TODO Remove code duplication between this class and ToolInstaller + private static final class SCClientJREInstaller { + private final ToolInstaller scClientInstaller; + + public void installJre(String platform) throws IOException { + var jreTargetPath = getJreTargetPath(); + if ( !Files.exists(jreTargetPath) || !Files.list(jreTargetPath).findFirst().isPresent() ) { + var jreVersion = getJreVersion(); + var jreBinaryDescriptor = getJreArtifactDescriptor(jreVersion, platform); + downloadAndExtractJre(jreBinaryDescriptor); + updateExecPermissions(jreTargetPath); + } + } + + private void updateExecPermissions(Path jreTargetPath) { + FileUtils.setAllFilePermissions(jreTargetPath.resolve("bin"), FileUtils.execPermissions, false); + var jspawnhelper = jreTargetPath.resolve("lib/jspawnhelper"); + if ( Files.exists(jspawnhelper) ) { + FileUtils.setSinglePathPermissions(jspawnhelper, FileUtils.execPermissions); + } + } + + private void downloadAndExtractJre(ToolDefinitionArtifactDescriptor jreArtifactDescriptor) throws IOException { + scClientInstaller.getProgressWriter().writeProgress("Downloading ScanCentral Client JRE"); + File downloadedFile = downloadJre(jreArtifactDescriptor); + scClientInstaller.getProgressWriter().writeProgress("Verifying JRE signature"); + SignatureHelper.fortifySignatureVerifier() + .verify(downloadedFile, jreArtifactDescriptor.getRsa_sha256()) + .throwIfNotValid(scClientInstaller.getOnDigestMismatch() == DigestMismatchAction.fail); + scClientInstaller.getProgressWriter().writeProgress("Installing JRE binaries"); + extractJre(jreArtifactDescriptor, downloadedFile); + } + + private static final File downloadJre(ToolDefinitionArtifactDescriptor jreArtifactDescriptor) throws IOException { + File tempDownloadFile = File.createTempFile("fcli-tool-download", null); + tempDownloadFile.deleteOnExit(); + UnirestHelper.download("tool", jreArtifactDescriptor.getDownloadUrl(), tempDownloadFile); + return tempDownloadFile; + } + + private final void extractJre(ToolDefinitionArtifactDescriptor jreArtifactDescriptor, File downloadedFile) throws IOException { + Path targetPath = getJreTargetPath(); + Files.createDirectories(targetPath); + var artifactName = jreArtifactDescriptor.getName(); + if (artifactName.endsWith("gz") || artifactName.endsWith(".tar.gz")) { + FileUtils.extractTarGZ(downloadedFile, FileUtils.defaultExtractPathResolver(targetPath, this::rewriteExtractSourcePath)); + } else if (artifactName.endsWith("zip")) { + FileUtils.extractZip(downloadedFile, FileUtils.defaultExtractPathResolver(targetPath, this::rewriteExtractSourcePath)); + } + downloadedFile.delete(); + } + + private final Path rewriteExtractSourcePath(Path p) { + return Path.of(p.toString().replaceAll("^jdk-.*-jre[/\\\\]", "")); + } + + private ToolDefinitionArtifactDescriptor getJreArtifactDescriptor(String jreVersion, String platform) { + var toolDefinitions = ToolDefinitionsHelper.getToolDefinitionRootDescriptor("jre"); + var jreVersionDescriptor = toolDefinitions.getVersion(jreVersion); + var jreBinaryDescriptor = jreVersionDescriptor.getBinaries().get(platform); + if ( jreBinaryDescriptor==null ) { throw new IllegalStateException("No JRE found for platform "+platform); } + return jreBinaryDescriptor; + } + + private String getJreVersion() { + var versionDescriptor = scClientInstaller.getVersionDescriptor(); + var extraProperties = versionDescriptor.getExtraProperties(); + var jreVersion = extraProperties==null ? null : extraProperties.get("jre"); + if ( StringUtils.isBlank(jreVersion) ) { + throw new IllegalStateException("Tool definitions don't list JRE version for this ScanCentral Client version; cannot install JRE as requested"); + } + return jreVersion; + } + + private Path getJreTargetPath() { + return scClientInstaller.getTargetPath().resolve("jre"); + } + } } diff --git a/fcli-core/fcli-tool/src/main/resources/com/fortify/cli/tool/i18n/ToolMessages.properties b/fcli-core/fcli-tool/src/main/resources/com/fortify/cli/tool/i18n/ToolMessages.properties index dbc2808c4f..06afb8b6d0 100644 --- a/fcli-core/fcli-tool/src/main/resources/com/fortify/cli/tool/i18n/ToolMessages.properties +++ b/fcli-core/fcli-tool/src/main/resources/com/fortify/cli/tool/i18n/ToolMessages.properties @@ -142,6 +142,9 @@ fcli.tool.sc-client.install.usage.header = Download and install ScanCentral SAST fcli.tool.sc-client.install.usage.description = ${fcli.tool.install.generic-description} fcli.tool.sc-client.install.confirm = Automatically confirm all prompts (cleaning the target directory, uninstalling other versions). fcli.tool.sc-client.install.client-auth-token = ScanCentral SAST client_auth_token used for authenticating with ScanCentral SAST Controller. +fcli.tool.sc-client.install.with-jre = (PREVIEW) Install compatible JRE into the ScanCentral Client JRE directory, allowing ScanCentral Client to run even if no (compatible) JRE is installed elsewhere on the system. +fcli.tool.sc-client.install.jre-platform = (PREVIEW) Specify the platform for which to install the JRE, implies --with-jre. See \ + https://github.com/fortify/tool-definitions/blob/main/v1/jre.yaml for available platforms. Default value: current platform. fcli.tool.sc-client.list.usage.header = List available and installed ScanCentral SAST Client versions. fcli.tool.sc-client.list.usage.description = Use the 'fcli tool definitions update' command to update the list of available versions. fcli.tool.sc-client.list-platforms.usage.header = List available platforms for ScanCentral SAST Client. diff --git a/fcli-other/fcli-doc/src/docs/asciidoc/versioned/index.adoc b/fcli-other/fcli-doc/src/docs/asciidoc/versioned/index.adoc index e8bd085972..330f519bb7 100644 --- a/fcli-other/fcli-doc/src/docs/asciidoc/versioned/index.adoc +++ b/fcli-other/fcli-doc/src/docs/asciidoc/versioned/index.adoc @@ -304,6 +304,10 @@ When evaluating expressions, fcli configures SpEL for 'data binding and conditio `+fcli ... list -q 'nestedObject!=null && get("nestedObject").has("stringValue") && nestedObject.stringValue=="nestedObjectValue1"'+` * Nested array properties: + `+fcli ... list -q 'nestedObjectArray !=null && !(nestedObjectArray.isEmpty()) && get("nestedObjectArray").get(0).has("stringValue") && nestedObjectArray.get(0).stringValue=="nestedArrayValue1"'+` +* Variable nested array property as command argument: + + `+fcli ... get ::yourVar::get(0).id+` +* Variable nested array property in query and output expression: + + `+fcli ssc av ls -q "application.id == #var('yourVar').get(9).id" -o "expr={name} : {#var('yourVar').get(9).name} : {id}\n"+` Some SpEL operators like `matches` may throw an exception if any of the operands is `null`. For example, the following will result in an error if the `prop1` propery is `null` for any of the records returned by the `list` command: +