diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionAsciidocCommand.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionAsciidocCommand.java index cffb960ee2..02ceecd9ab 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionAsciidocCommand.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/cli/cmd/AbstractActionAsciidocCommand.java @@ -18,7 +18,6 @@ import java.nio.file.StandardOpenOption; import java.util.HashSet; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -116,14 +115,28 @@ private final String generateOptionDescription(IOptionDescriptor descriptor) { private final String addLinks(String contents) { if ( manpageDir==null ) { return contents; } + // TODO Do we want to automatically insert fcli links (which could potentially lead to bugs as seen with + // https://github.com/fortify/fcli/issues/622), or should we allow Markdown syntax in action descriptions? + // We could either add support for new markdownDescription properties, or allow Markdown in existing + // description properties and clean this up in the 'action help' command. var manPages = listDir(manpageDir).stream().filter(s->s.matches("fcli-[\\w-]+-[\\w-]+-[\\w-]+.adoc")) .map(s->s.replaceAll("\\.adoc", "")) + .sorted((a,b)->Integer.compare(a.length(), b.length())) // In case of overlapping names, we need to replace longest matching name .collect(Collectors.toSet()); for ( var manPage : manPages ) { var pattern = manPage.replace("-", "[ -]"); var replacement = String.format("link:manpage/%s.html[$1]", manPage); - contents = contents.replaceAll("(? action run' references in synopsis + contents = contents.replaceAll("("+pattern+")", replacement); + } else { + // Replace literal 'fcli *' references embedded in backticks, if not preceded by '[' + // as that (likely) means we already generated a link for a longer command name. + // See https://github.com/fortify/fcli/issues/622 for example. The backticks need to + // go into the link text (as otherwise link:... would be rendered literally), so we + // need to include the full text between the backticks in the link text. + contents = contents.replaceAll("(?FoDReleaseHelper.getReleaseDescriptor(unirest, nameOrId, delimiterMixin.getDelimiter(), true, fields)).toArray(FoDReleaseDescriptor[]::new); + } + + public Collection getReleaseDescriptorJsonNodes(UnirestInstance unirest, String... fields) { + return Stream.of(getReleaseDescriptors(unirest, fields)).map(FoDReleaseDescriptor::asJsonNode).collect(Collectors.toList()); + } + + public Integer[] getReleaseIds(UnirestInstance unirest) { + return Stream.of(getReleaseDescriptors(unirest, "releaseId")).map(FoDReleaseDescriptor::getReleaseId).toArray(Integer[]::new); + } + } public static class RequiredOption extends AbstractFoDQualifiedReleaseNameOrIdResolverMixin { @Option(names = {"--release", "--rel"}, required = true, paramLabel = "id|app[:ms]:rel", descriptionKey = "fcli.fod.release.resolver.name-or-id") @@ -63,4 +85,9 @@ public static class OptionalCopyFromOption extends AbstractFoDQualifiedReleaseNa @Option(names = {"--copy-from"}, required = false, paramLabel = "id|app[:ms]:rel", descriptionKey = "fcli.fod.release.resolver.copy-from.nameOrId") @Getter private String qualifiedReleaseNameOrId; } + + public static class PositionalParameterMulti extends AbstractFoDMultiQualifiedReleaseNameOrIdResolverMixin { + @Parameters(index = "0", arity = "1..", paramLabel="id|app[:ms]:rel", descriptionKey = "fcli.fod.release.resolver.multi-name-or-id") + @Getter private String[] qualifiedReleaseNamesOrIds; + } } diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-pr-comment.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-pr-comment.yaml index e01a8de039..5fe8133ad5 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-pr-comment.yaml +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/github-pr-comment.yaml @@ -134,7 +134,7 @@ valueTemplates: - name: reviewBody contents: | - ## Fortify vulnerability summary (PREVIEW) + ## Fortify vulnerability summary Any issues listed below are based on comparing the latest scan results against the previous scan results in FoD release [${parameters.release.applicationName}${#isNotBlank(parameters.release.microserviceNae)?'- '+parameters.release.microserviceName:''} - ${parameters.release.releaseName}](${#fod.releaseBrowserUrl(parameters.release)}). This is for informational purposes only and, depending on workflow, may not be an accurate representation of what issues will be introduced into or removed from the target branch when merging this PR. diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml index e95182346d..af52e9c260 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml @@ -53,6 +53,7 @@ parameters: name: sdlc-status cliAliases: status description: "See `fcli fod release create`" + defaultValue: Development - group: sast_setup_opts name: assessment-type required: false @@ -95,6 +96,9 @@ steps: - to: stdout value: | Create application release ${parameters.release} (id ${createRelease[0].releaseId}): ${createRelease[0].__action__} + - progress: "Waiting for release to leave suspended state" + - fcli: + - args: fod release wait-for "${parameters.release}" --progress=none - if: ${parameters["scan-types"].contains("sast")} steps: - if: ${parameters.profile=="default"} diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index 2ec1371237..12a81a0332 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -41,6 +41,7 @@ fcli.fod.scan.in-progress-action = The action to use if a scan is already in pro fcli.fod.scan.remediation-preference = The remediation preference to use. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.microservice.resolver.name = Microservice name in the format :. fcli.fod.release.resolver.name-or-id = Release id or [:]: name. +fcli.fod.release.resolver.multi-name-or-id = One or more release id's or [:]: names. fcli.fod.release.resolver.name = Release name in the format [:]:. fcli.fod.import.sbom-format = The SBOM format to import. If empty Cyclone DX format is assumed. fcli.fod.user.user-name-or-id = User id or username. Note that numeric values are always interpreted \ @@ -363,7 +364,11 @@ fcli.fod.release.create.usage.header = Create a new application release. \ fcli.fod.release.create.application-name = The id or name of the application to create the release on. fcli.fod.release.create.release-name = The name of the release to create for the application. fcli.fod.release.create.description = Description of the release. -fcli.fod.release.create.copy-from = The id or name of a release to copy existing state from. +fcli.fod.release.create.copy-from = The id or name of a release to copy existing state from. Note that \ + FoD will put the release in 'suspended' state until copying is complete, during which time scan \ + requests and other operations may be rejected. If you want to run any other operations on the release, \ + it is recommended to first invoke the `fcli fod release wait-for` command to wait until the release \ + leaves suspended state. fcli.fod.release.create.microservice = The id or name of the microservice to create the release on. fcli.fod.release.create.status = SDLC lifecycle status of the release. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.release.create.attr = Attribute id or name and its value to set on the release. \ @@ -387,6 +392,19 @@ fcli.fod.release.update.attr = Attribute id or name and its value to set on the fcli.fod.release.list-assessment-types.usage.header = List assessment types for a given release. fcli.fod.release.list-assessment-types.scan-types = Comma-separated list of scan types for which to list assessment types. Default value: ${DEFAULT-VALUE}. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.release.list-scans.usage.header = List scans for a given release. +fcli.fod.release.wait-for.usage.header = Wait for one or more scans to reach or exit suspended state. +fcli.fod.release.wait-for.usage.description.0 = Although this command offers a lot of options to \ + cover many different use cases, you can simply pass one or more release names or id's to wait until \ + those releases leave 'suspended' state. \ + %n%nMost common use case is to invoke this wait-for command after creating a new release through the \ + `fcli fod release create` command with the `--copy-from` option; FoD will put the newly created \ + release in 'suspended' state until copying is completed, during which time scan requests and other \ + operations may be rejected. \ + %n%nNote that contrary to other fcli wait-for commands, any options related to unknown or failure state \ + handling are not applicable to this wait-for command and will be ignored. +fcli.fod.release.wait-for.until = Wait until either any or all releases match. If neither --until or --while are specified, default is to wait until all releases match. +fcli.fod.release.wait-for.while = Wait while either any or all releases match. +fcli.fod.release.wait-for.suspended = Suspended state against which to match the given releases; may be `false` (default) or `true`. # fcli fod assessment-type fcli.fod.assessment-type.usage.header = Manage FoD assessment types. @@ -884,6 +902,7 @@ fcli.fod.app.output.table.options = applicationId,applicationName,fcliApplicatio fcli.fod.app.scan.output.table.options = scanId,scanType,analysisStatusType,applicationName,microserviceName,releaseName,startedDateTime,completedDateTime,scanMethodTypeName fcli.fod.microservice.output.table.options = microserviceId,microserviceName,applicationName fcli.fod.release.output.table.options = releaseId,releaseName,microserviceName,applicationName,sdlcStatusType +fcli.fod.release.wait-for.output.table.options = releaseId,releaseName,microserviceName,applicationName,suspended fcli.fod.release.scan.output.table.options = scanId,scanType,analysisStatusType,applicationName,microserviceName,releaseName,startedDateTime,completedDateTime,scanMethodTypeName fcli.fod.release.assessment-type.output.table.options = assessmentTypeId,name,scanType,frequencyType,unitInfo,entitlementId,entitlementDescription fcli.fod.entitlement.output.table.options = entitlementId,entitlementDescription,startDate,endDate,unitInfo 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..c2198aba98 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.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; @@ -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; @@ -50,21 +56,22 @@ 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()); @@ -72,6 +79,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 +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"; } @@ -137,14 +145,17 @@ 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(); 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) { @@ -152,7 +163,7 @@ private File createZipFile() { } } - 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]; @@ -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 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.]", "_"); + } + } +} \ 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..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/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..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 \ diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml index db5d72005a..8320cc4bd9 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/github-pr-comment.yaml @@ -139,7 +139,7 @@ valueTemplates: - name: reviewBody contents: | - ## Fortify vulnerability summary (PREVIEW) + ## Fortify vulnerability summary Any issues listed below are based on comparing the latest scan results against the previous scan results in SSC application version [${parameters.appversion.project.name} - ${parameters.appversion.name}](${#ssc.appversionBrowserUrl(parameters.appversion,null)}). This is for informational purposes only and, depending on workflow, may not be an accurate representation of what issues will be introduced into or removed from the target branch when merging this PR.