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 1dfe2e0fb6..910c8c7e81 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,6 +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.Map; +import java.util.Map.Entry; +import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -50,21 +55,30 @@ 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.") + 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(); MultipartBody body = unirest.post("/rest/v2/job") .multiPartContent() .field("zipFile", createZipFile(), "application/zip") .field("username", userName, "text/plain") .field("scaVersion", sensorVersion, "text/plain") .field("clientVersion", sensorVersion, "text/plain") - .field("scaRuntimeArgs", optionsProvider.getScanStartOptions().getScaRuntimeArgs(), "text/plain") - .field("jobType", optionsProvider.getScanStartOptions().getJobType().name(), "text/plain"); + .field("jobType", optionsProvider.getScanStartOptions().getJobType().name(), "text/plain") + .field("scaRuntimeArgs", constructSCAArgs(), "text/plain"); + body = updateBody(body, "email", email); body = updateBody(body, "buildId", optionsProvider.getScanStartOptions().getBuildId()); body = updateBody(body, "pvId", getAppVersionId()); @@ -72,6 +86,7 @@ public final JsonNode getJsonNode(UnirestInstance unirest) { body = updateBody(body, "uploadToken", getUploadToken()); body = updateBody(body, "dotNetRequired", String.valueOf(optionsProvider.getScanStartOptions().isDotNetRequired())); body = updateBody(body, "dotNetFrameworkRequiredVersion", optionsProvider.getScanStartOptions().getDotNetVersion()); + JsonNode response = body.asObject(JsonNode.class).getBody(); if ( !response.has("token") ) { throw new IllegalStateException("Unexpected response when submitting scan job: "+response); @@ -80,7 +95,80 @@ public final JsonNode getJsonNode(UnirestInstance unirest) { return SCSastControllerScanJobHelper.getScanJobDescriptor(unirest, scanJobToken, StatusEndpointVersion.v1).asJsonNode(); } - @Override + 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"; } @@ -144,7 +232,13 @@ private File createZipFile() { try (FileOutputStream fout = new FileOutputStream(zipFile); ZipOutputStream zout = new ZipOutputStream(fout)) { final String fileName = (optionsProvider.getScanStartOptions().getJobType() == SCSastControllerJobType.TRANSLATION_AND_SCAN_JOB) ? "translation.zip" : "session.mbs"; addFile( zout, fileName, optionsProvider.getScanStartOptions().getPayloadFile()); - // TODO Add rule files, filter files, issue template + + for (Entry> fileArgsMap : compressedFilesMap.entrySet()) { + Set files = fileArgsMap.getValue(); + for (String file : files) { + addFile(zout, file, optionsProvider.getScanStartOptions().getPayloadFile()); + } + } } return zipFile; } catch (IOException e) { @@ -152,7 +246,25 @@ private File createZipFile() { } } - private void addFile(ZipOutputStream zout, String fileName, File file) throws IOException { + 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)); byte[] buffer = new byte[1024]; @@ -170,3 +282,39 @@ private static final class PublishToAppVersionResolverMixin extends AbstractSSCA public final boolean hasValue() { return StringUtils.isNotBlank(appVersionNameOrId); } } } + + +class ScanArgument { + private boolean isFileArgument; + private String argKey; + private String argValue; + + public void setFileArgument(boolean isFileArgument) { + this.isFileArgument = isFileArgument; + } + + public void setArgKey(String argKey) { + this.argKey = argKey; + } + + public void setArgValue(String argValue) { + this.argValue = argValue; + } + + public boolean isFileArgument() { + return isFileArgument; + } + + public String getArgKey() { + return argKey; + } + + public String getArgValue() { + return argValue; + } + + @Override + public String toString() { + return "Argument " + argKey + (isFileArgument? " is a file argument with value " : " is not a file argument with value ") + argValue; + } +} \ No newline at end of file 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 060e8f5127..30777072e0 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,7 @@ public interface ISCSastScanStartOptions { String getBuildId(); - String getScaRuntimeArgs(); +// String getScaRuntimeArgs(); boolean isDotNetRequired(); String getDotNetVersion(); File getPayloadFile(); diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/SCSastScanStartMbsOptions.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/SCSastScanStartMbsOptions.java index 5a56359393..1c381f6795 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/SCSastScanStartMbsOptions.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/SCSastScanStartMbsOptions.java @@ -32,7 +32,6 @@ public class SCSastScanStartMbsOptions implements ISCSastScanStartOptions { @Getter private String buildId; @Getter private final boolean dotNetRequired = false; @Getter private final String dotNetVersion = null; - @Getter private final String scaRuntimeArgs = ""; // TODO Provide options @Getter private SCSastControllerJobType jobType = SCSastControllerJobType.SCAN_JOB; @Option(names = {"-m", "--mbs-file"}, required= true) @@ -40,7 +39,7 @@ public void setMbsFile(File mbsFile) { this.payloadFile = mbsFile; setMbsProperties(mbsFile); } - + private void setMbsProperties(File mbsFile) { try ( FileSystem fs = FileSystems.newFileSystem(mbsFile.toPath()) ) { Path mbsManifest = fs.getPath("MobileBuildSession.manifest"); diff --git a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/SCSastScanStartPackageOptions.java b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/SCSastScanStartPackageOptions.java index ceb9314517..19dcf43923 100644 --- a/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/SCSastScanStartPackageOptions.java +++ b/fcli-core/fcli-sc-sast/src/main/java/com/fortify/cli/sc_sast/scan/cli/mixin/SCSastScanStartPackageOptions.java @@ -33,7 +33,6 @@ public class SCSastScanStartPackageOptions implements ISCSastScanStartOptions { @Getter private final String buildId = null; // TODO ScanCentral Client doesn't allow for specifying build id; should we provide a CLI option for this? @Getter private boolean dotNetRequired; @Getter private String dotNetVersion; - @Getter private final String scaRuntimeArgs = ""; @Getter private SCSastControllerJobType jobType = SCSastControllerJobType.TRANSLATION_AND_SCAN_JOB; @Option(names = {"-p", "--package-file"}, required = true) 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 58b29cf819..1f51ce9775 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 @@ -141,6 +141,7 @@ 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.