Skip to content

Commit

Permalink
feat: fcli tool sc-client install: Add options to install compatibl…
Browse files Browse the repository at this point in the history
…e JRE
  • Loading branch information
rsenden committed Dec 23, 2024
1 parent 8110f20 commit 85bc662
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<PosixFilePermission> execPermissions = PosixFilePermissions.fromString("rwxr-xr-x");
private FileUtils() {}

@SneakyThrows
Expand Down Expand Up @@ -105,15 +110,55 @@ public static final void move(Path source, Path target) {
}
}

@SneakyThrows
public static final void setAllFilePermissions(Path path, Set<PosixFilePermission> permissions, boolean recursive) {
if ( path!=null && Files.exists(path) ) {
if ( Files.isDirectory(path) ) {
try (Stream<Path> 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<PosixFilePermission> permissions) {
try {
Files.setPosixFilePermissions(p, permissions);
} catch ( UnsupportedOperationException e ) {
// Log warning?
}
}

public static final Function<Path,Path> defaultExtractPathResolver(Path targetPath, Function<Path,Path> 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<Path, Path> 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 {
Expand All @@ -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<Path,Path> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,7 +43,6 @@

@Builder
public final class ToolInstaller {
private static final Set<PosixFilePermission> binPermissions = PosixFilePermissions.fromString("rwxr-xr-x");
@Getter private final String toolName;
@Getter private final String requestedVersion;
@Getter private final String defaultPlatform;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -187,25 +182,27 @@ 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);
}
}
}


private final ToolInstallationResult install(ToolDefinitionArtifactDescriptor artifactDescriptor) {
try {
preInstallAction.accept(this);
if ( preInstallAction!=null ) { preInstallAction.accept(this); }
var versionDescriptor = getVersionDescriptor();
warnIfDifferentTargetPath();
if ( !hasMatchingTargetPath(getVersionDescriptor()) ) {
checkEmptyTargetPath();
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 ) {
Expand Down Expand Up @@ -288,24 +285,6 @@ private final void checkEmptyTargetPath() throws IOException {
}
}

private static final void updateBinPermissions(Path binPath) throws IOException {
if ( binPath!=null ) {
try (Stream<Path> 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<T> {
private T value = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ public final class ToolDefinitionVersionDescriptor {
private String[] aliases;
private boolean stable;
private Map<String, ToolDefinitionArtifactDescriptor> binaries;
private Map<String, String> extraProperties;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand All @@ -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");
Expand All @@ -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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 85bc662

Please sign in to comment.