From 85bc662c3a6c1214fd37f92165b40390f01c83ec Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Mon, 23 Dec 2024 13:06:02 +0100 Subject: [PATCH] feat: `fcli tool sc-client install`: Add options to install compatible JRE --- .../fortify/cli/common/util/FileUtils.java | 62 ++++++++++-- .../tool/_common/helper/ToolInstaller.java | 37 ++----- .../ToolDefinitionVersionDescriptor.java | 1 + .../cli/cmd/ToolSCClientInstallCommand.java | 96 +++++++++++++++++++ .../cli/tool/i18n/ToolMessages.properties | 3 + 5 files changed, 164 insertions(+), 35 deletions(-) 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-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.