From f49993071f6f00d0f1d1000784f7ee097e3e85f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:36:56 +0000 Subject: [PATCH 1/3] Initial plan From 35e3981706891ffc94db64bdcfa2d05555d913a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:46:36 +0000 Subject: [PATCH 2/3] Fix NPE in FoDSastScanStartCommand and add new --in-progress-action/--entitlement-preference options with ftests --- .../scan/helper/sast/FoDScanSastHelper.java | 2 +- .../cli/cmd/FoDSastScanStartCommand.java | 47 +++++++++++++------ .../cli/fod/i18n/FoDMessages.properties | 1 + .../fortify/cli/ftest/fod/FoDScanSpec.groovy | 22 +++++++++ 4 files changed, 57 insertions(+), 15 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 85df65310af..2f0db465469 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,35 +41,54 @@ 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; @Mixin private ProgressWriterFactoryMixin progressWriterFactory; - + @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) + // Guard remediationScanType against null; Picocli @Mixin fields may be null if not injected + 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); } } 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 9d9e2fc1649..970563babbe 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 @@ -554,6 +554,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..47704b2c39d 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,28 @@ class FoDScanSpec extends FcliBaseSpec { } } + def "start.sast-scan-advanced-queue"() { + def args = "fod sast-scan start --release=fcli-1698140484524:v2 --file=$sastPackage --in-progress-action=Queue --store sastScanAdvQueue" + when: + def result = Fcli.run(args) + then: + verifyAll(result.stdout) { + size()>=2 + it.last().contains("STARTED") + } + } + + def "start.sast-scan-advanced-entitlement-preference"() { + def args = "fod sast-scan start --release=fcli-1698140484524:v2 --file=$sastPackage --entitlement-preference=SubscriptionOnly --store sastScanAdvEntPref" + 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: From fb559b689c487e89337e7a28cd0ef1ddbcac407e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:50:29 +0000 Subject: [PATCH 3/3] Add clarifying comment for inProgressScanActionType raw string handling in startScanAdvanced --- .../cli/fod/_common/scan/helper/sast/FoDScanSastHelper.java | 2 ++ 1 file changed, 2 insertions(+) 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 a3531083fe6..f1cea9ee5ec 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 @@ -66,6 +66,8 @@ public static final FoDScanDescriptor startScanAdvanced(UnirestInstance unirest, .queryString("purchaseEntitlement", Boolean.toString(req.getPurchaseEntitlement())) .queryString("remdiationScanPreferenceType", (req.getRemdiationScanPreferenceType() != null ? FoDEnums.RemediationScanPreferenceType.valueOf(req.getRemdiationScanPreferenceType()) : FoDEnums.RemediationScanPreferenceType.NonRemediationScanOnly)) + // Raw string is passed (not valueOf) because the caller may supply "CancelInProgressScan", + // which differs from the enum name "CancelScanInProgress" used by DAST. .queryString("inProgressScanActionType", (req.getInProgressScanActionType() != null ? req.getInProgressScanActionType() : FoDEnums.InProgressScanActionType.DoNotStartScan.toString())) .queryString("scanTool", req.getScanTool())