diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanStartCommand.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanStartCommand.java index 910c8c7e81..5b71e55035 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanStartCommand.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/cmd/SCSastControllerScanStartCommand.java @@ -16,11 +16,11 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -40,6 +40,7 @@ import kong.unirest.MultipartBody; import kong.unirest.UnirestInstance; import lombok.Getter; +import lombok.RequiredArgsConstructor; import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; @@ -55,29 +56,21 @@ public final class SCSastControllerScanStartCommand extends AbstractSCSastContro @Mixin private SCSastSensorPoolResolverMixin.OptionalOption sensorPoolResolver; @Mixin private PublishToAppVersionResolverMixin sscAppVersionResolver; @Option(names = "--ssc-ci-token") private String ciToken; - @Option(names = { "--sargs", "--scan-args" }, description = "Runtime scan arguments to Source Analyzer.") + @Option(names = { "--sargs", "--scan-args" }) private String scanArguments = ""; - - private Map> scanFileArgs = new HashMap>(); - private Map> compressedFilesMap = new HashMap>(); - private Set scanArgumentsSet; - - // TODO Add options for specifying (custom) rules file(s), filter file(s) and project template - // TODO Add options for pool selection @Override public final JsonNode getJsonNode(UnirestInstance unirest) { String sensorVersion = normalizeSensorVersion(optionsProvider.getScanStartOptions().getSensorVersion()); - - processScanArguments(); + var scanArgsHelper = ScanArgsHelper.parse(scanArguments); MultipartBody body = unirest.post("/rest/v2/job") .multiPartContent() - .field("zipFile", createZipFile(), "application/zip") + .field("zipFile", createZipFile(scanArgsHelper.getInputFileToZipEntryMap()), "application/zip") .field("username", userName, "text/plain") .field("scaVersion", sensorVersion, "text/plain") .field("clientVersion", sensorVersion, "text/plain") .field("jobType", optionsProvider.getScanStartOptions().getJobType().name(), "text/plain") - .field("scaRuntimeArgs", constructSCAArgs(), "text/plain"); + .field("scaRuntimeArgs", scanArgsHelper.getScanArgs(), "text/plain"); body = updateBody(body, "email", email); body = updateBody(body, "buildId", optionsProvider.getScanStartOptions().getBuildId()); @@ -95,79 +88,6 @@ public final JsonNode getJsonNode(UnirestInstance unirest) { return SCSastControllerScanJobHelper.getScanJobDescriptor(unirest, scanJobToken, StatusEndpointVersion.v1).asJsonNode(); } - private String constructSCAArgs() { - StringBuffer buffer = new StringBuffer(); - for (ScanArgument scanArgument : scanArgumentsSet) { - String argKey = scanArgument.getArgKey(); - String argValue = scanArgument.getArgValue(); - boolean fileArgument = scanArgument.isFileArgument(); - if (fileArgument) { - Set scanArgFiles = compressedFilesMap.get(argKey); - if (null != scanArgFiles) { - int size = scanArgFiles.size(); - for (String scanArgumentFileName : scanArgFiles) { - buffer.append(argKey); - buffer.append(" \'"); - buffer.append(scanArgumentFileName); - buffer.append("\'"); - if (size > 1) { - buffer.append(" "); - --size; - } - } - } - compressedFilesMap.remove(argKey); - } else { - buffer.append(argKey); - if (argValue!=null) { - buffer.append(" "); - buffer.append(argValue); - } - } - buffer.append(" "); - } - return buffer.toString().trim(); - } - - private void processScanArguments() { - scanArgumentsSet = processScanRuntimeArgs(); - for (ScanArgument scanArgument : scanArgumentsSet) { - if (scanArgument.isFileArgument()) { - String key = scanArgument.getArgKey(); - String value = scanArgument.getArgValue(); - scanFileArgs.computeIfAbsent(key, k -> new HashSet<>()).add(value); - } - } - updateFileNamesToUniqueName(); - } - - private Set processScanRuntimeArgs() { - Set scanArgsSet = new HashSet(); - String[] parts = scanArguments.split(" (?=(?:[^\']*\'[^\']*\')*[^\']*$)"); - - ScanArgument scanArgument = null; - for (String part : parts) { - String key = null; - String value = null; - boolean isFileArg = false; - - if (part.startsWith("-")) { - key = part.trim(); - scanArgument = new ScanArgument(); - scanArgument.setArgKey(key); - } else { - if (part.startsWith("file:")) { - isFileArg = true; - } - value = part.replace("file:", "").replace("'", ""); - scanArgument.setArgValue(value); - scanArgument.setFileArgument(isFileArg); - } - scanArgsSet.add(scanArgument); - } - return scanArgsSet; - } - @Override public final String getActionCommandResult() { return "SCAN_REQUESTED"; @@ -225,7 +145,7 @@ private final MultipartBody updateBody(MultipartBody body, String field, String return StringUtils.isBlank(value) ? body : body.field(field, value, "text/plain"); } - private File createZipFile() { + private File createZipFile(Map extraFiles) { try { File zipFile = File.createTempFile("zip", ".zip"); zipFile.deleteOnExit(); @@ -233,11 +153,8 @@ private File createZipFile() { final String fileName = (optionsProvider.getScanStartOptions().getJobType() == SCSastControllerJobType.TRANSLATION_AND_SCAN_JOB) ? "translation.zip" : "session.mbs"; addFile( zout, fileName, optionsProvider.getScanStartOptions().getPayloadFile()); - for (Entry> fileArgsMap : compressedFilesMap.entrySet()) { - Set files = fileArgsMap.getValue(); - for (String file : files) { - addFile(zout, file, optionsProvider.getScanStartOptions().getPayloadFile()); - } + for (var extraFile : extraFiles.entrySet() ) { + addFile(zout, extraFile.getValue(), extraFile.getKey()); } } return zipFile; @@ -246,24 +163,6 @@ private File createZipFile() { } } - private void updateFileNamesToUniqueName() { - for (Entry> fileArg : scanFileArgs.entrySet()) { - String argName = fileArg.getKey(); - Set argValues = fileArg.getValue(); - Set compressedFileNames = new HashSet(); - for (String argValue : argValues) { - String uniqueFileName = constructUniqueFileName(argValue); - compressedFileNames.add(uniqueFileName); - - } - compressedFilesMap.put(argName, compressedFileNames); - } - } - - private String constructUniqueFileName(String argValue) { - return argValue.replaceAll("[^A-Za-z0-9.]", "_"); - } - private void addFile(ZipOutputStream zout, String fileName, File file) throws IOException { try ( FileInputStream in = new FileInputStream(file)) { zout.putNextEntry(new ZipEntry(fileName)); @@ -281,9 +180,49 @@ private static final class PublishToAppVersionResolverMixin extends AbstractSSCA @Getter private String appVersionNameOrId; public final boolean hasValue() { return StringUtils.isNotBlank(appVersionNameOrId); } } + + @RequiredArgsConstructor + private static final class ScanArgsHelper { + @Getter private final String scanArgs; + @Getter private final Map inputFileToZipEntryMap; + + public static final ScanArgsHelper parse(String scanArgs) { + List newArgs = new ArrayList<>(); + Map inputFileToZipEntryMap = new LinkedHashMap<>(); + String[] parts = scanArgs.split(" (?=(?:[^\']*\'[^\']*\')*[^\']*$)"); + for ( var part: parts ) { + var inputFileName = getInputFileName(part); + if ( inputFileName==null ) { + newArgs.add(part.replace("'", "\"")); + } else { + var inputFile = new File(inputFileName); + if ( !inputFile.canRead() ) { + throw new IllegalArgumentException("Can't read file "+inputFileName+" as specified in --sargs"); + } + // Re-use existing zip entry name if same file was processed before + var zipEntryFileName = inputFileToZipEntryMap.getOrDefault(inputFile, getZipEntryFileName(inputFileName)); + newArgs.add("\""+zipEntryFileName+"\""); + inputFileToZipEntryMap.put(inputFile, zipEntryFileName); + } + } + return new ScanArgsHelper(String.join(" ", newArgs), inputFileToZipEntryMap); + } + + private static final String getInputFileName(String part) { + var pattern = Pattern.compile("^'?file:'?([^\']*)'?$"); + var matcher = pattern.matcher(part); + return matcher.matches() ? matcher.group(1) : null; + } + + private static final String getZipEntryFileName(String orgFileName) { + return orgFileName.replaceAll("[^A-Za-z0-9.]", "_"); + } + } } + + class ScanArgument { private boolean isFileArgument; private String argKey; diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/ISCSastScanStartOptions.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/ISCSastScanStartOptions.java index 30777072e0..2bdd47fd39 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/ISCSastScanStartOptions.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/ISCSastScanStartOptions.java @@ -18,7 +18,6 @@ public interface ISCSastScanStartOptions { String getBuildId(); -// String getScaRuntimeArgs(); boolean isDotNetRequired(); String getDotNetVersion(); File getPayloadFile(); diff --git a/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties b/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties index 1f51ce9775..1f7ed00324 100644 --- a/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties +++ b/fcli-core/fcli-sc-sast/src/main/resources/com/fortify/cli/sc_sast/i18n/SCSastMessages.properties @@ -123,6 +123,11 @@ fcli.sc-sast.scan.start.package-file = Package file to scan. fcli.sc-sast.scan.start.notify = Email address to which to send a scan completion notification. fcli.sc-sast.scan.start.sensor-version = Version of the sensor on which the package should be scanned. Officially, you should select the same sensor version as the version of the ScanCentral Client used to create the package. fcli.sc-sast.scan.start.publish-to = Publish scan results to the given SSC application version once the scan has completed. +fcli.sc-sast.scan.start.sargs = Fortify Static Code Analyzer scan arguments, see ScanCentral SAST documentation for supported \ + scan arguments for your ScanCentral SAST version. Multiple scan arguments must be provided as a single option argument, \ + arguments containing spaces must be embedded in single quotes, and local files must be referenced through the 'file:' prefix. \ + Note that contrary to fcli, scan arguments usually start with a single dash, not double dashes. For example: \ + %n --sargs "-quick -filter 'file:./my filters.txt'" fcli.sc-sast.scan.status.usage.header = Get status for a previously submitted scan request. fcli.sc-sast.scan.wait-for.usage.header = Wait for one or more scans to reach or exit specified scan statuses. fcli.sc-sast.scan.wait-for.usage.description.0 = Although this command offers a lot of options to cover many \ @@ -141,7 +146,6 @@ fcli.sc-sast.scan.wait-for.any-scan-state=One or more scan states against which fcli.sc-sast.scan.wait-for.any-publish-state=One or more scan publishing states against which to match the given scans. fcli.sc-sast.scan.wait-for.any-ssc-state=One or more SSC artifact processing states against which to match the given scans. fcli.sc-sast.scan-job.resolver.jobToken = Scan job token. -fcli.sc-sast.scan.start.sargs = Fortify Source Analyzer runtime scan arguments. # fcli sc-sast sensor fcli.sc-sast.sensor.usage.header = Manage ScanCentral SAST sensors.