diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java index 3094ffb3180..a3531083fe6 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java @@ -67,7 +67,7 @@ public static final FoDScanDescriptor startScanAdvanced(UnirestInstance unirest, .queryString("remdiationScanPreferenceType", (req.getRemdiationScanPreferenceType() != null ? FoDEnums.RemediationScanPreferenceType.valueOf(req.getRemdiationScanPreferenceType()) : FoDEnums.RemediationScanPreferenceType.NonRemediationScanOnly)) .queryString("inProgressScanActionType", (req.getInProgressScanActionType() != null ? - FoDEnums.InProgressScanActionType.valueOf(req.getInProgressScanActionType()) : FoDEnums.InProgressScanActionType.DoNotStartScan)) + req.getInProgressScanActionType() : FoDEnums.InProgressScanActionType.DoNotStartScan.toString())) .queryString("scanTool", req.getScanTool()) .queryString("scanToolVersion", req.getScanToolVersion()) .queryString("scanMethodType", req.getScanMethodType()); diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanStartCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanStartCommand.java index 85df65310af..dd714626b6c 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanStartCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/sast_scan/cli/cmd/FoDSastScanStartCommand.java @@ -19,6 +19,7 @@ import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.progress.cli.mixin.ProgressWriterFactoryMixin; import com.fortify.cli.common.progress.helper.IProgressWriter; +import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.common.util.FcliBuildProperties; import com.fortify.cli.fod._common.scan.cli.cmd.AbstractFoDScanStartCommand; import com.fortify.cli.fod._common.scan.cli.mixin.FoDRemediationScanPreferenceTypeMixins; @@ -41,6 +42,10 @@ public class FoDSastScanStartCommand extends AbstractFoDScanStartCommand { @Option(names = {"--notes"}) private String notes; + @Option(names = {"--in-progress-action"}, descriptionKey = "fcli.fod.sast-scan.start.in-progress-action") + private FoDEnums.InProgressScanActionType inProgressScanActionType; + @Option(names = {"--entitlement-preference"}, descriptionKey = "fcli.fod.scan.entitlement-preference") + private FoDEnums.EntitlementPreferenceType entitlementPreferenceType; @Mixin private CommonOptionMixins.RequiredFile scanFileMixin; @Mixin private FoDRemediationScanPreferenceTypeMixins.OptionalOption remediationScanType; @@ -49,29 +54,63 @@ public class FoDSastScanStartCommand extends AbstractFoDScanStartCommand { @Override protected FoDScanDescriptor startScan(UnirestInstance unirest, FoDReleaseDescriptor releaseDescriptor) { String relId = releaseDescriptor.getReleaseId(); - Boolean isRemediation = false; - - // if we have requested remediation scan use it to find appropriate assessment type - if (remediationScanType != null && remediationScanType.getRemediationScanPreferenceType() != null) { - if (remediationScanType.getRemediationScanPreferenceType().equals(FoDEnums.RemediationScanPreferenceType.RemediationScanIfAvailable) || - remediationScanType.getRemediationScanPreferenceType().equals(FoDEnums.RemediationScanPreferenceType.RemediationScanOnly)) { - isRemediation = true; - } - } validateScanSetup(unirest, relId); - FoDScanSastStartRequest startScanRequest = FoDScanSastStartRequest.builder() - .isRemediationScan(isRemediation) + FoDEnums.RemediationScanPreferenceType remediationPref = remediationScanType != null + ? remediationScanType.getRemediationScanPreferenceType() : null; + + boolean useAdvanced = entitlementPreferenceType != null || inProgressScanActionType != null; + + FoDScanSastStartRequest.FoDScanSastStartRequestBuilder requestBuilder = FoDScanSastStartRequest.builder() .scanMethodType("Other") .notes(notes != null && !notes.isEmpty() ? notes : "") .scanTool(FcliBuildProperties.INSTANCE.getFcliProjectName()) - .scanToolVersion(FcliBuildProperties.INSTANCE.getFcliVersion()) - .build(); + .scanToolVersion(FcliBuildProperties.INSTANCE.getFcliVersion()); try (IProgressWriter progressWriter = progressWriterFactory.create()) { + if (useAdvanced) { + FoDEnums.InProgressScanActionType inProgressAction = inProgressScanActionType != null + ? inProgressScanActionType : FoDEnums.InProgressScanActionType.Queue; + // FoD's start-scan-advanced expects 'CancelInProgressScan' rather than the enum's 'CancelScanInProgress' + String inProgressApiValue = inProgressAction == FoDEnums.InProgressScanActionType.CancelScanInProgress + ? "CancelInProgressScan" : inProgressAction.name(); + FoDScanSastStartRequest startScanRequest = requestBuilder + .entitlementPreferenceType(entitlementPreferenceType != null ? entitlementPreferenceType.name() : null) + .purchaseEntitlement(false) + .remdiationScanPreferenceType(remediationPref != null ? remediationPref.name() : null) + .inProgressScanActionType(inProgressApiValue) + .build(); + return FoDScanSastHelper.startScanAdvanced(unirest, releaseDescriptor, startScanRequest, scanFileMixin.getFile(), progressWriter); + } + boolean isRemediation = remediationPref != null + && (remediationPref.equals(FoDEnums.RemediationScanPreferenceType.RemediationScanIfAvailable) + || remediationPref.equals(FoDEnums.RemediationScanPreferenceType.RemediationScanOnly)); + FoDScanSastStartRequest startScanRequest = requestBuilder + .isRemediationScan(isRemediation) + .build(); return FoDScanSastHelper.startScanWithDefaults(unirest, releaseDescriptor, startScanRequest, scanFileMixin.getFile(), progressWriter); + } catch (Exception e) { + throw translateScanInProgressException(e); + } + } + + // FoD returns HTTP 422 (errorCode 2001) when a scan is already in progress and the + // in-progress action prevents starting a new one. Translate that into a concise, + // actionable message instead of surfacing the raw upload/HTTP exception. + private RuntimeException translateScanInProgressException(Exception e) { + for (Throwable t = e; t != null; t = t.getCause()) { + if (t instanceof UnexpectedHttpResponseException) { + UnexpectedHttpResponseException httpException = (UnexpectedHttpResponseException) t; + if (httpException.getStatus() == 422 && httpException.getMessage() != null + && httpException.getMessage().toLowerCase().contains("another scan is in progress")) { + return new FcliSimpleException("Cannot start scan: another scan is already in progress for this release. " + + "Use '--in-progress-action=Queue' to queue this scan, or " + + "'--in-progress-action=CancelScanInProgress' to cancel the running scan and start a new one."); + } + } } + return e instanceof RuntimeException ? (RuntimeException) e : new FcliSimpleException(e); } private void validateScanSetup(UnirestInstance unirest, String relId) { 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 411e3cfbd51..326322e9ba5 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 @@ -542,6 +542,7 @@ fcli.fod.sast-scan.start.remediation = Identify this scan as a remediation scan. fcli.fod.sast-scan.start.skip-if-running = Check to see if static scan is already running before starting. fcli.fod.sast-scan.start.entitlement-id = The Id of the entitlement to use for the scan. fcli.fod.sast-scan.start.purchase-entitlement = Purchase an entitlement if one is not currently allocated or available. +fcli.fod.sast-scan.start.in-progress-action = The action to use if a scan is already in progress. Valid values: ${COMPLETION-CANDIDATES}. Defaults to 'Queue' when this or '--entitlement-preference' is specified; otherwise the FoD-side default applies. fcli.fod.sast-scan.start.notes = Scan notes. fcli.fod.sast-scan.start.file = Absolute path of the ScanCentral package (.Zip) file to upload. fcli.fod.sast-scan.start.validate-entitlement = Validate if an entitlement has been set and is still valid. diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanSpec.groovy index e684432d5c2..f465c3c1461 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/fod/FoDScanSpec.groovy @@ -337,6 +337,42 @@ class FoDScanSpec extends FcliBaseSpec { } } + def "start.sast-scan-help-shows-new-options"() { + def args = "fod sast-scan start --help" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + it.any { it.contains("--in-progress-action") } + it.any { it.contains("--entitlement-preference") } + it.any { it.contains("DoNotStartScan") } + it.any { it.contains("CancelScanInProgress") } + it.any { it.contains("Queue") } + } + } + + def "start.sast-scan-in-progress-do-not-start"() { + // A scan was started above, so DoNotStartScan must be rejected with a friendly message + def args = "fod sast-scan start --release=fcli-1698140484524:v2 --file=$sastPackage --in-progress-action=DoNotStartScan" + when: + Fcli.run(args) + then: + def e = thrown(UnexpectedFcliResultException) + e.result.stderr.any { it.contains("another scan is already in progress") } + } + + def "start.sast-scan-advanced-queue"() { + // Exercises the start-scan-advanced path; queues behind the in-progress scan + def args = "fod sast-scan start --release=fcli-1698140484524:v2 --file=$sastPackage --in-progress-action=Queue" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()>=2 + it.last().contains("STARTED") + } + } + def "wait-for-sast"() { def args = "fod sast-scan wait-for ::sastScan:: -i 2s --until=all-match --any-state=Completed,In_Progress,Queued" when: