Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fcli sc-sast scan start: Add support for passing scan arguments through --sargs option (resolves #449) #627

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

Expand All @@ -35,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;
Expand All @@ -50,28 +56,30 @@ public final class SCSastControllerScanStartCommand extends AbstractSCSastContro
@Mixin private SCSastSensorPoolResolverMixin.OptionalOption sensorPoolResolver;
@Mixin private PublishToAppVersionResolverMixin sscAppVersionResolver;
@Option(names = "--ssc-ci-token") private String ciToken;

// TODO Add options for specifying (custom) rules file(s), filter file(s) and project template
// TODO Add options for pool selection
@Option(names = { "--sargs", "--scan-args" })
private String scanArguments = "";

@Override
public final JsonNode getJsonNode(UnirestInstance unirest) {
String sensorVersion = normalizeSensorVersion(optionsProvider.getScanStartOptions().getSensorVersion());
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("scaRuntimeArgs", optionsProvider.getScanStartOptions().getScaRuntimeArgs(), "text/plain")
.field("jobType", optionsProvider.getScanStartOptions().getJobType().name(), "text/plain");
.field("jobType", optionsProvider.getScanStartOptions().getJobType().name(), "text/plain")
.field("scaRuntimeArgs", scanArgsHelper.getScanArgs(), "text/plain");

body = updateBody(body, "email", email);
body = updateBody(body, "buildId", optionsProvider.getScanStartOptions().getBuildId());
body = updateBody(body, "pvId", getAppVersionId());
body = updateBody(body, "poolUuid", getSensorPoolUuid());
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);
Expand All @@ -80,7 +88,7 @@ public final JsonNode getJsonNode(UnirestInstance unirest) {
return SCSastControllerScanJobHelper.getScanJobDescriptor(unirest, scanJobToken, StatusEndpointVersion.v1).asJsonNode();
}

@Override
@Override
public final String getActionCommandResult() {
return "SCAN_REQUESTED";
}
Expand Down Expand Up @@ -137,22 +145,25 @@ 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<File, String> extraFiles) {
try {
File zipFile = File.createTempFile("zip", ".zip");
zipFile.deleteOnExit();
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 (var extraFile : extraFiles.entrySet() ) {
addFile(zout, extraFile.getValue(), extraFile.getKey());
}
}
return zipFile;
} catch (IOException e) {
throw new RuntimeException("Error creating job file", e);
}
}

private void addFile(ZipOutputStream zout, String fileName, File file) throws IOException {
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];
Expand All @@ -169,4 +180,42 @@ 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<File, String> inputFileToZipEntryMap;

public static final ScanArgsHelper parse(String scanArgs) {
List<String> newArgs = new ArrayList<>();
Map<File, String> 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.]", "_");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

public interface ISCSastScanStartOptions {
String getBuildId();
String getScaRuntimeArgs();
boolean isDotNetRequired();
String getDotNetVersion();
File getPayloadFile();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ 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)
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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down