From ed29d321ad30cba8a0b9379ca8758f3dd0448469 Mon Sep 17 00:00:00 2001 From: gilseara <> Date: Wed, 20 May 2026 13:44:09 +0200 Subject: [PATCH 1/3] feat: `fcli fod sast-scan start`: Add `--in-progress-action` and `--entitlement-preference` options Routes the scan start through the FoD `start-scan-advanced` endpoint when either option is specified, allowing control over the in-progress scan action and entitlement preference. Existing callers that pass neither option continue to use `start-scan-with-defaults`, preserving previous behavior. When the advanced path is used and `--in-progress-action` is not explicitly set, fcli defaults it to `Queue` rather than FoD's `DoNotStartScan`. --- .../cli/cmd/FoDSastScanStartCommand.java | 40 +++++++++++++------ .../cli/fod/i18n/FoDMessages.properties | 1 + 2 files changed, 28 insertions(+), 13 deletions(-) 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..140da467cbb 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 @@ -41,6 +41,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,27 +53,37 @@ 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.getRemediationScanPreferenceType(); + + 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; + FoDScanSastStartRequest startScanRequest = requestBuilder + .entitlementPreferenceType(entitlementPreferenceType != null ? entitlementPreferenceType.name() : null) + .purchaseEntitlement(false) + .remdiationScanPreferenceType(remediationPref != null ? remediationPref.name() : null) + .inProgressScanActionType(inProgressAction.name()) + .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); } } 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. From a26fbd1d8a534e85defb5953cef3d994d9b7aec5 Mon Sep 17 00:00:00 2001 From: gilseara <> Date: Fri, 22 May 2026 15:14:10 +0200 Subject: [PATCH 2/3] fix: `fcli fod sast-scan start`: Send `CancelInProgressScan` to FoD API for cancel action FoD's `start-scan-advanced` endpoint expects `CancelInProgressScan` rather than the value used by the shared `InProgressScanActionType` enum (`CancelScanInProgress`). Translate at the SAST command boundary; the helper now forwards the request's string value directly so a wire-specific value can be supplied. DAST behavior is unchanged. --- .../cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java | 2 +- .../cli/fod/sast_scan/cli/cmd/FoDSastScanStartCommand.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 140da467cbb..b78db8e2fa5 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 @@ -70,11 +70,14 @@ protected FoDScanDescriptor startScan(UnirestInstance unirest, FoDReleaseDescrip 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(inProgressAction.name()) + .inProgressScanActionType(inProgressApiValue) .build(); return FoDScanSastHelper.startScanAdvanced(unirest, releaseDescriptor, startScanRequest, scanFileMixin.getFile(), progressWriter); } From d1db882471471ec2e95ac7e74e4a9d377b05eccc Mon Sep 17 00:00:00 2001 From: gilseara <> Date: Fri, 12 Jun 2026 18:35:08 +0200 Subject: [PATCH 3/3] fix: `fcli fod sast-scan start`: Show friendly message when a scan is already in progress Intercept FoD's HTTP 422 response and surface a concise, actionable message instead of the raw upload/HTTP stack trace. Also restore the remediationScanType null-guard and add ftest coverage for the new --in-progress-action / --entitlement-preference options. --- .../cli/cmd/FoDSastScanStartCommand.java | 24 ++++++++++++- .../fortify/cli/ftest/fod/FoDScanSpec.groovy | 36 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) 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 b78db8e2fa5..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; @@ -56,7 +57,8 @@ protected FoDScanDescriptor startScan(UnirestInstance unirest, FoDReleaseDescrip validateScanSetup(unirest, relId); - FoDEnums.RemediationScanPreferenceType remediationPref = remediationScanType.getRemediationScanPreferenceType(); + FoDEnums.RemediationScanPreferenceType remediationPref = remediationScanType != null + ? remediationScanType.getRemediationScanPreferenceType() : null; boolean useAdvanced = entitlementPreferenceType != null || inProgressScanActionType != null; @@ -88,9 +90,29 @@ protected FoDScanDescriptor startScan(UnirestInstance unirest, FoDReleaseDescrip .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) { // get current setup and check if its valid FoDScanConfigSastDescriptor currentSetup = FoDScanSastHelper.getSetupDescriptor(unirest, relId); 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: