From 06f7ae83e6d1fde624655567a687a7f671777dcd Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 10 Sep 2026 13:49:38 +0530 Subject: [PATCH 1/3] feat: add bulk DAST audit action --- .../AviatorSSCCorrelateSastDastCommand.java | 4 +- .../cli/cmd/AviatorSSCDastAuditCommand.java | 7 + ...va => AviatorSSCAttributeDefinitions.java} | 26 +- ...er.java => AviatorSSCAttributeHelper.java} | 46 +- .../ssc/helper/AviatorSSCPrepareHelper.java | 4 +- .../helper/AviatorSSCAttributeHelperTest.java | 123 +++++ .../cli/ssc/actions/zip/bulkaudit-dast.yaml | 490 ++++++++++++++++++ .../cli/ssc/actions/zip/bulkcorrelate.yaml | 22 +- .../ssc/SSCAviatorAuditValidationSpec.groovy | 28 + 9 files changed, 723 insertions(+), 27 deletions(-) rename fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/{AviatorSSCCorrelationAttributeDefs.java => AviatorSSCAttributeDefinitions.java} (70%) rename fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/{AviatorSSCCorrelationAttributeHelper.java => AviatorSSCAttributeHelper.java} (73%) create mode 100644 fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeHelperTest.java create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-dast.yaml diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java index 78dd93b330..eaae72f1d4 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCCorrelateSastDastCommand.java @@ -37,11 +37,11 @@ import com.fortify.cli.aviator.grpc.CorrelationResult; import com.fortify.cli.aviator.grpc.CorrelationStreamConfig; import com.fortify.cli.aviator.grpc.CorrelationStreamProcessor; +import com.fortify.cli.aviator.ssc.helper.AviatorSSCAttributeHelper; import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelateDownloadHelper; import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelateFprParser; import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelateFprParser.ParseResult; import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelateHelper; -import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelationAttributeHelper; import com.fortify.cli.aviator.ssc.helper.CategoryBucket; import com.fortify.cli.aviator.ssc.helper.CategoryGrouper; import com.fortify.cli.aviator.ssc.helper.DastFprCorrelationEnricher; @@ -260,7 +260,7 @@ private void uploadTaggedSastFpr(Path sastPath, List confirmed, private void writeLastCorrelationTimestamp() { logger.progress("Status: Writing last_correlation timestamp to app version..."); - AviatorSSCCorrelationAttributeHelper.writeLastCorrelationTimestamp(unirest, av.getVersionId()); + AviatorSSCAttributeHelper.writeLastCorrelationTimestamp(unirest, av.getVersionId()); logger.progress("Status: last_correlation timestamp written successfully."); } diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java index 895a08d4c1..69505c07b0 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCDastAuditCommand.java @@ -29,12 +29,14 @@ import com.fortify.cli.aviator._common.session.user.helper.AviatorUserSessionDescriptor; import com.fortify.cli.aviator.audit.DastAuditFPR; import com.fortify.cli.aviator.audit.DastAuditFprResult; +import com.fortify.cli.aviator.audit.DastAuditFprStatus; import com.fortify.cli.aviator.config.AviatorLoggerImpl; import com.fortify.cli.aviator.config.IAviatorLogger; import com.fortify.cli.aviator.config.TagMappingConfig; import com.fortify.cli.aviator.grpc.AviatorGrpcClientHelper; import com.fortify.cli.aviator.grpc.DastAuditStreamConfig; import com.fortify.cli.aviator.grpc.DastAuditStreamProcessor; +import com.fortify.cli.aviator.ssc.helper.AviatorSSCAttributeHelper; import com.fortify.cli.aviator.ssc.helper.AviatorSSCAuditHelper; import com.fortify.cli.aviator.ssc.helper.AviatorSSCFprTransferHelper; import com.fortify.cli.aviator.ssc.helper.AviatorSSCTagValidator; @@ -97,6 +99,11 @@ public JsonNode getJsonNode(UnirestInstance unirest) { artifactId = AviatorSSCFprTransferHelper.uploadDastFpr( unirest, appVersion, downloadedFpr, progressWriter); } + if (result.status() == DastAuditFprStatus.AUDITED + || result.status() == DastAuditFprStatus.SKIPPED) { + AviatorSSCAttributeHelper.writeLastDastAuditTimestamp( + unirest, appVersion.getVersionId()); + } return buildOutput(appVersion, result, artifactId); } catch (RuntimeException e) { actionResult = "FAILED"; diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelationAttributeDefs.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeDefinitions.java similarity index 70% rename from fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelationAttributeDefs.java rename to fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeDefinitions.java index 7573dbace0..a72933b09f 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelationAttributeDefs.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeDefinitions.java @@ -13,27 +13,26 @@ package com.fortify.cli.aviator.ssc.helper; /** - * Attribute definitions used by the SAST-DAST correlation feature. + * SSC application-version attribute definitions used by Aviator workflows. * *

These are SSC application-version attributes (not per-issue custom tags). * The definition is created by the {@code aviator ssc prepare} command and - * the value is written by {@code aviator ssc correlate-sast-dast}. + * the values are written by {@code aviator ssc correlate-sast-dast} and + * {@code aviator ssc audit-dast}. */ -public final class AviatorSSCCorrelationAttributeDefs { +public final class AviatorSSCAttributeDefinitions { - private AviatorSSCCorrelationAttributeDefs() {} + private AviatorSSCAttributeDefinitions() {} /** * Descriptor for a custom SSC attribute definition managed by the Aviator module. * - * @param guid Fixed GUID — must never change once deployed to an SSC instance. * @param name Attribute name as it appears in SSC (used for lookup and write). * @param category SSC attribute category (e.g. {@code "TECHNICAL"}). * @param type SSC attribute type string (e.g. {@code "TEXT"}, {@code "DATE"}). * @param description Human-readable description stored in SSC. */ public record AttributeDefinition( - String guid, String name, String category, String type, @@ -53,10 +52,23 @@ public record AttributeDefinition( * comparison with artifact {@code lastScanDate} values. */ public static final AttributeDefinition LAST_CORRELATION_ATTR = new AttributeDefinition( - "B2C3D4E5-F6A7-8901-BCDE-F12345678901", "last_correlation", "TECHNICAL", "TEXT", "Timestamp of the last successful SAST-DAST correlation run (ISO-8601 UTC). Written by fcli aviator ssc correlate-sast-dast." ); + + /** + * Free-text attribute written after a DAST audit evaluation completes successfully. + * + *

The value is an ISO-8601 UTC timestamp. A successful evaluation includes a + * run that finds no eligible findings, allowing bulk DAST audit selection to avoid + * repeating a completed no-op evaluation until a newer DAST scan is available. + */ + public static final AttributeDefinition LAST_DAST_AUDIT_ATTR = new AttributeDefinition( + "last_dast_audit", + "TECHNICAL", + "TEXT", + "Timestamp of the last successful DAST audit evaluation (ISO-8601 UTC). Written by fcli aviator ssc audit-dast." + ); } diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelationAttributeHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeHelper.java similarity index 73% rename from fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelationAttributeHelper.java rename to fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeHelper.java index 1972b73b6a..1db342b23a 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCCorrelationAttributeHelper.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeHelper.java @@ -21,32 +21,33 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fortify.cli.aviator.ssc.helper.AviatorSSCCorrelationAttributeDefs.AttributeDefinition; +import com.fortify.cli.aviator.ssc.helper.AviatorSSCAttributeDefinitions.AttributeDefinition; import com.fortify.cli.common.exception.FcliSimpleException; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; import com.fortify.cli.ssc.attribute.helper.SSCAttributeUpdateBuilder; +import kong.unirest.UnirestException; import kong.unirest.UnirestInstance; import lombok.RequiredArgsConstructor; /** - * Manages the SSC attribute definitions used by the SAST-DAST correlation feature. + * Manages SSC application-version attributes used by Aviator workflows. * *

The attribute definition is created by {@code aviator ssc prepare} (admin-only). * The attribute value is written per application version by - * {@code aviator ssc correlate-sast-dast} (non-admin). + * {@code aviator ssc correlate-sast-dast} and {@code aviator ssc audit-dast} (non-admin). * *

This is distinct from the generic SSC attribute helpers in the SSC module * ({@code SSCAttributeHelper}, {@code SSCAttributeDefinitionHelper}) which * handle reading/updating existing attributes. This class also handles - * creating attribute definitions specific to correlation. + * creating attribute definitions specific to Aviator workflows. */ @RequiredArgsConstructor -public class AviatorSSCCorrelationAttributeHelper { +public class AviatorSSCAttributeHelper { - private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCCorrelationAttributeHelper.class); + private static final Logger LOG = LoggerFactory.getLogger(AviatorSSCAttributeHelper.class); private final UnirestInstance unirest; private final AttributeDefinition attrDef; @@ -63,7 +64,7 @@ public class AviatorSSCCorrelationAttributeHelper { */ public void synchronize(AviatorSSCPrepareHelper.PrepareResult result) { try { - LOG.debug("Searching for attribute definition '{}' (GUID: {})", attrDef.name(), attrDef.guid()); + LOG.debug("Searching for SSC attribute definition '{}'", attrDef.name()); if (findDefinition() != null) { LOG.info("Attribute definition '{}' is already present.", attrDef.name()); result.addEntry("Attribute Definition", "VERIFIED", @@ -82,9 +83,18 @@ public void synchronize(AviatorSSCPrepareHelper.PrepareResult result) { } } + /** Writes the current UTC timestamp to the {@code last_correlation} attribute. */ + public static void writeLastCorrelationTimestamp(UnirestInstance unirest, String versionId) { + writeTimestamp(unirest, versionId, AviatorSSCAttributeDefinitions.LAST_CORRELATION_ATTR); + } + + /** Writes the current UTC timestamp to the {@code last_dast_audit} attribute. */ + public static void writeLastDastAuditTimestamp(UnirestInstance unirest, String versionId) { + writeTimestamp(unirest, versionId, AviatorSSCAttributeDefinitions.LAST_DAST_AUDIT_ATTR); + } + /** - * Writes the current UTC timestamp to the {@code last_correlation} attribute on - * the given application version. + * Writes the current UTC timestamp to the given attribute on the application version. * *

This method assumes the attribute definition already exists — it must have * been created by a prior {@code aviator ssc prepare} run. If the definition @@ -93,19 +103,21 @@ public void synchronize(AviatorSSCPrepareHelper.PrepareResult result) { * @param unirest active SSC session * @param versionId SSC project version ID */ - public static void writeLastCorrelationTimestamp(UnirestInstance unirest, String versionId) { + private static void writeTimestamp( + UnirestInstance unirest, String versionId, AttributeDefinition attributeDefinition) { String timestamp = Instant.now().toString(); - LOG.debug("Writing last_correlation timestamp '{}' to app version {}", timestamp, versionId); + LOG.debug("Writing {} timestamp to app version {}", attributeDefinition.name(), versionId); try { new SSCAttributeUpdateBuilder(unirest) - .add(Map.of(AviatorSSCCorrelationAttributeDefs.LAST_CORRELATION_ATTR.name(), timestamp)) + .add(Map.of(attributeDefinition.category() + ":" + attributeDefinition.name(), timestamp)) .buildRequest(versionId) .asObject(JsonNode.class); - LOG.info("last_correlation timestamp '{}' written to app version {}", timestamp, versionId); - } catch (FcliSimpleException e) { - LOG.warn("WARN: Could not write last_correlation timestamp. Run 'fcli aviator ssc prepare' to create the attribute definition."); + LOG.info("{} timestamp written to app version {}", attributeDefinition.name(), versionId); + } catch (FcliSimpleException | UnirestException e) { + LOG.warn("Could not write {} timestamp; the audit result remains successful but bulk selection may retry this version. " + + "Run 'fcli aviator ssc prepare' if the attribute definition is missing.", attributeDefinition.name()); } } @@ -122,7 +134,9 @@ private JsonNode findDefinition() { JsonNode data = responseBody.get("data"); if (data == null || !data.isArray()) return null; return JsonHelper.stream((ArrayNode) data) - .filter(n -> attrDef.name().equals(n.path("name").asText())) + .filter(n -> attrDef.name().equals(n.path("name").asText()) + && attrDef.category().equals(n.path("category").asText()) + && attrDef.type().equals(n.path("type").asText())) .findFirst().orElse(null); } diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCPrepareHelper.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCPrepareHelper.java index 56f96f09e0..93073fbef5 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCPrepareHelper.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCPrepareHelper.java @@ -126,7 +126,9 @@ private void addOptionalTagWarnings(TagSynchronizationResults tagResults, Prepar /** Synchronizes Aviator custom attributes. */ private void synchronizeAttributes(PrepareResult result, IProgressWriter progress) { progress.writeProgress("Synchronizing Aviator custom attributes..."); - new AviatorSSCCorrelationAttributeHelper(unirest, AviatorSSCCorrelationAttributeDefs.LAST_CORRELATION_ATTR) + new AviatorSSCAttributeHelper(unirest, AviatorSSCAttributeDefinitions.LAST_CORRELATION_ATTR) + .synchronize(result); + new AviatorSSCAttributeHelper(unirest, AviatorSSCAttributeDefinitions.LAST_DAST_AUDIT_ATTR) .synchronize(result); } diff --git a/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeHelperTest.java b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeHelperTest.java new file mode 100644 index 0000000000..adb227c554 --- /dev/null +++ b/fcli-core/fcli-aviator/src/test/java/com/fortify/cli/aviator/ssc/helper/AviatorSSCAttributeHelperTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.aviator.ssc.helper; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.rest.unirest.UnirestHelper; +import com.fortify.cli.common.rest.unirest.config.UnirestJsonHeaderConfigurer; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +import kong.unirest.UnirestInstance; + +class AviatorSSCAttributeHelperTest { + @Test + void writesLastDastAuditTimestampAsTextAttribute() throws Exception { + try (var server = new TestSscServer(); var unirest = newUnirest(server)) { + AviatorSSCAttributeHelper.writeLastDastAuditTimestamp(unirest, "42"); + + JsonNode update = server.getLastUpdate(); + assertEquals("42", update.get(0).path("attributeDefinitionId").asText()); + assertTrue(update.get(0).path("value").asText().matches("\\d{4}-\\d{2}-\\d{2}T.*Z")); + } + } + + @Test + void exposesStableDastAuditAttributeDefinition() { + var definition = AviatorSSCAttributeDefinitions.LAST_DAST_AUDIT_ATTR; + + assertEquals("last_dast_audit", definition.name()); + assertEquals("TECHNICAL", definition.category()); + assertEquals("TEXT", definition.type()); + } + + @Test + void markerWriteFailureIsReportedWithoutThrowing() throws Exception { + try (var server = new TestSscServer().withUpdateStatus(500); var unirest = newUnirest(server)) { + assertDoesNotThrow(() -> AviatorSSCAttributeHelper.writeLastDastAuditTimestamp(unirest, "42")); + } + } + + private static UnirestInstance newUnirest(TestSscServer server) { + return UnirestHelper.createUnirestInstance(unirest -> { + UnirestJsonHeaderConfigurer.configure(unirest); + unirest.config().defaultBaseUrl(server.getBaseUrl()); + }); + } + + private static final class TestSscServer implements AutoCloseable { + private final HttpServer server; + private JsonNode lastUpdate; + private int updateStatus = 200; + + private TestSscServer() throws IOException { + server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/api/v1/attributeDefinitions", this::handleDefinitions); + server.createContext("/api/v1/projectVersions/42/attributes", this::handleAttributes); + server.start(); + } + + private String getBaseUrl() { + return "http://localhost:" + server.getAddress().getPort(); + } + + private JsonNode getLastUpdate() { + return lastUpdate; + } + + private TestSscServer withUpdateStatus(int status) { + updateStatus = status; + return this; + } + + private void handleDefinitions(HttpExchange exchange) throws IOException { + respond(exchange, 200, """ + {"data":[{"id":"42","guid":"C3D4E5F6-A7B8-9012-BCDE-F12345678902","name":"last_dast_audit","category":"TECHNICAL","type":"TEXT","required":false,"hasDefault":false,"options":[]}]} + """); + } + + private void handleAttributes(HttpExchange exchange) throws IOException { + if (!"PUT".equals(exchange.getRequestMethod())) { + respond(exchange, 405, "{}"); + return; + } + lastUpdate = JsonHelper.getObjectMapper().readTree(exchange.getRequestBody()); + respond(exchange, updateStatus, "{}"); + } + + private static void respond(HttpExchange exchange, int status, String body) throws IOException { + byte[] response = body.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(status, response.length); + try (var output = exchange.getResponseBody()) { + output.write(response); + } + } + + @Override + public void close() { + server.stop(0); + } + } +} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-dast.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-dast.yaml new file mode 100644 index 0000000000..8629743677 --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-dast.yaml @@ -0,0 +1,490 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev-2.x.json + +author: Fortify +usage: + header: (PREVIEW) Perform Fortify DAST Aviator audits of SSC application versions in bulk. + description: | + This action identifies SSC application versions with a processed DAST FPR and + submits only versions whose DAST scan is newer than the last recorded DAST audit. + Versions without a processed WebInspect result, or whose DAST scan has already + been evaluated, are skipped before any audit command is invoked. + + Before using this action, run `fcli aviator ssc prepare` with an Aviator admin + configuration so the Aviator tags and the `last_dast_audit` application-version + attribute are available in SSC. The action requires an active SSC session, an + Aviator user session for DAST auditing, and an Aviator admin configuration for + listing and creating Aviator applications. Unless nested command defaults are + overridden, these use the `default` SSC session, Aviator user session, and admin + configuration. + + For application versions that don't already exist in Fortify Aviator, the action + automatically creates them before submitting the DAST audit. By default + (--aviator-app-mapping=app), SSC project versions map to one Fortify Aviator + application per SSC project. Set --aviator-app-mapping=version to map each SSC + project version to its own Fortify Aviator application. + + If Aviator entitlement is exhausted while creating applications, further creation + attempts are suppressed and versions with existing applications continue. + +config: + output: immediate + rest.target.default: ssc + run.fcli.status.log.default: true + run.fcli.status.check.default: false + mcp: exclude # Not suitable for MCP tool invocation + +cli.options: + max-audits: + names: --max-audits, -m + description: "Maximum number of application versions to audit. Default: -1 (unlimited)" + required: false + default: -1 + type: int + aviator-app-mapping: + names: --aviator-app-mapping + description: "Controls how SSC project versions map to Fortify Aviator applications. 'app' (default) maps one application per SSC project; 'version' maps one application per project version." + required: false + default: app + filter: + names: --filter, -f + description: "Optional filter to restrict the SSC application versions considered. Example: 'Languages:java'. Default: no filtering" + required: false + default: "" + exclude-filter: + names: --exclude-filter, -e + description: "Optional filter to exclude SSC application versions. Example: 'Languages:c#'. Default: no exclusion" + required: false + default: "" + dry-run: + names: --dry-run, -n + description: "Show application versions and Aviator commands that would be processed without creating applications or running audits. Default: false" + required: false + default: false + type: boolean + tag-mapping: + names: --tag-mapping, -t + description: "Optional path to a custom DAST tag mapping YAML file. If omitted, the default DAST mapping is used" + required: false + refresh: + names: --refresh + description: "Refresh application version metrics before audit. Default: true" + required: false + type: boolean + default: true + refresh-timeout: + names: --refresh-timeout + description: "Timeout for metric refresh, for example 30s, 5m, or 1h. Default: 60s" + required: false + default: "60s" + +steps: + - var.set: + module: ssc + stats.missing_dast_skipped: 0 + stats.unchanged_skipped: 0 + stats.create_attempts: 0 + stats.create_successes: 0 + stats.create_failures: 0 + stats.audit_attempts: 0 + stats.audit_failures: 0 + stats.failures: 0 + stats.selection_failures: 0 + stats.entitlement_exhausted: false + stats.creation_blocked: false + stats.create_skipped_after_failure: 0 + stats.would_create_count: 0 + + - if: ${cli['max-audits'] < -1} + throw: "Invalid --max-audits value '${cli['max-audits']}'. Use -1 for unlimited or a non-negative number" + + - if: ${!(cli['aviator-app-mapping'] matches 'app|version')} + throw: "Invalid --aviator-app-mapping value '${cli['aviator-app-mapping']}'. Valid values are: app, version" + + - log.progress: "Using Fortify Aviator app mapping: ${cli['aviator-app-mapping']}" + + - log.progress: Retrieving existing Fortify Aviator applications... + - run.fcli: + aviator_apps: + cmd: aviator app ls -o json + status.check: true + records.collect: true + + - var.set: + aviator_app_names: null + - records.for-each: + from: ${aviator_apps.records} + record.var-name: aviator_app + do: + - var.set: + aviator_app_names..: ${aviator_app.name} + + - var.set: + last_dast_audit_guid: null + last_dast_audit_id: null + - rest.call: + dast_audit_attribute_definitions: + uri: /api/v1/attributeDefinitions + type: paged + query: + limit: -1 + records.for-each: + record.var-name: attribute_definition + breakIf: ${last_dast_audit_guid != null || last_dast_audit_id != null} + if: ${attribute_definition.name == 'last_dast_audit' && attribute_definition.category == 'TECHNICAL' && attribute_definition.type == 'TEXT'} + do: + - var.set: + last_dast_audit_guid: ${attribute_definition.guid} + last_dast_audit_id: ${attribute_definition.id} + - if: ${last_dast_audit_guid == null && last_dast_audit_id == null} + log.warn: "SSC attribute definition 'last_dast_audit' was not found. Run 'fcli aviator ssc prepare' before using bulk DAST audit" + + - log.progress: Querying SSC application versions... + - var.set: + candidate_versions: null + + - if: ${cli.filter == ''} + rest.call: + app_versions: + uri: /api/v1/projectVersions + type: paged + query: + limit: -1 + records.for-each: + record.var-name: version + do: + - var.set: + current_project_name: ${version.project.name} + current_version_name: ${version.name} + current_aviator_app_name: ${version.project.name.replaceAll('"', '')} + - if: ${'version'.equals(cli['aviator-app-mapping'])} + var.set: + current_aviator_app_name: ${(current_project_name + '__' + current_version_name).replaceAll('"', '')} + - var.set: + project_exists_in_aviator: ${aviator_app_names != null && aviator_app_names.contains(current_aviator_app_name)} + - var.set: + candidate_versions..: {fmt: candidate_version} + + - if: ${cli.filter != ''} + do: + - log.progress: Using issue aging filter scope for candidate discovery... + - rest.call: + app_versions: + uri: /api/v1/issueaging + type: paged + query: + limit: -1 + filterby: ${cli.filter} + records.for-each: + record.var-name: version + embed: + project_details: + uri: /api/v1/projectVersions/${version.id} + do: + - var.set: + current_project_name: ${version.project_details.project.name} + current_version_name: ${version.project_details.name} + current_aviator_app_name: ${version.project_details.project.name.replaceAll('"', '')} + - if: ${'version'.equals(cli['aviator-app-mapping'])} + var.set: + current_aviator_app_name: ${(current_project_name + '__' + current_version_name).replaceAll('"', '')} + - var.set: + project_exists_in_aviator: ${aviator_app_names != null && aviator_app_names.contains(current_aviator_app_name)} + - var.set: + candidate_versions..: {fmt: candidate_version} + + - if: ${candidate_versions != null} + log.progress: Found ${candidate_versions.size()} SSC application versions in initial scope + - if: ${candidate_versions == null} + log.progress: Found 0 SSC application versions in initial scope + + - var.set: + scoped_versions: ${candidate_versions} + + - if: ${candidate_versions != null && candidate_versions.size() > 0 && cli['exclude-filter'] != ''} + do: + - log.progress: Resolving exclusion filter scope... + - var.set: + excluded_version_lookup: null + - rest.call: + excluded_versions: + uri: /api/v1/issueaging + type: paged + query: + limit: -1 + filterby: ${cli['exclude-filter']} + records.for-each: + record.var-name: excluded_version + do: + - var.set: + excluded_version_lookup.${excluded_version.id}: true + + - var.set: + filtered_versions: null + excluded_count: 0 + + - records.for-each: + from: ${candidate_versions} + record.var-name: candidate + do: + - var.set: + should_exclude: false + - if: ${excluded_version_lookup != null && excluded_version_lookup[candidate.id.toString()] != null} + var.set: + should_exclude: true + - if: ${should_exclude} + var.set: + excluded_count: ${excluded_count + 1} + - if: ${!should_exclude} + var.set: + filtered_versions..: ${candidate} + + - var.set: + scoped_versions: ${filtered_versions} + - if: ${scoped_versions != null} + log.progress: Excluded ${excluded_count} application versions, ${scoped_versions.size()} remaining + - if: ${scoped_versions == null} + log.progress: Excluded ${excluded_count} application versions, 0 remaining + + - if: ${candidate_versions == null} + log.progress: 0 application versions remain after filtering + - if: ${candidate_versions != null && cli['exclude-filter'] == ''} + log.progress: ${scoped_versions.size()} application versions remain after filtering + + - if: ${scoped_versions != null && scoped_versions.size() > 0} + do: + - log.progress: Evaluating application versions for bulk DAST audit... + - var.set: + selected_versions: null + - records.for-each: + from: ${scoped_versions} + record.var-name: candidate + do: + - var.set: + latest_dast_scan_date: null + latest_dast_scan_invalid: false + latest_dast_scan_date_parsed: null + last_dast_audit_value: null + last_dast_audit_invalid: false + last_dast_audit_date_parsed: null + candidate_selection_failed: false + - rest.call: + candidate_artifacts: + uri: /api/v1/projectVersions/${candidate.id}/artifacts + type: paged + query: + limit: -1 + embed: scans + orderby: uploadDate DESC + records.for-each: + record.var-name: artifact + breakIf: ${latest_dast_scan_date != null} + do: + - var.set: + artifact_scan_date: ${#ifBlank(artifact.lastScanDate, artifact.uploadDate)} + - if: ${latest_dast_scan_date == null && artifact.status == 'PROCESS_COMPLETE' && !#isBlank(artifact_scan_date) && artifact._embed.scans?.^[type=='WEBINSPECT'] != null} + var.set: + latest_dast_scan_date: ${artifact_scan_date} + on.fail: + - var.set: + candidate_selection_failed: true + stats.selection_failures: ${stats.selection_failures + 1} + stats.failures: ${stats.failures + 1} + - log.warn: "Unable to inspect DAST artifacts for ${candidate.project_name}:${candidate.version_name}; skipping this version and continuing" + - if: ${!candidate_selection_failed && latest_dast_scan_date == null} + do: + - var.set: + stats.missing_dast_skipped: ${stats.missing_dast_skipped + 1} + - if: ${!candidate_selection_failed && latest_dast_scan_date != null} + do: + - var.set: + latest_dast_scan_date_parsed: ${#date(latest_dast_scan_date)} + on.fail: + - var.set: + latest_dast_scan_invalid: true + - log.warn: "Ignoring invalid DAST scan date for ${candidate.project_name}:${candidate.version_name}; selecting the version for audit" + - rest.call: + candidate_attributes: + uri: /api/v1/projectVersions/${candidate.id}/attributes + records.for-each: + record.var-name: attribute + breakIf: ${last_dast_audit_value != null} + if: "${last_dast_audit_guid != null && attribute.guid == last_dast_audit_guid || last_dast_audit_id != null && attribute.attributeDefinitionId != null && attribute.attributeDefinitionId.toString() == last_dast_audit_id.toString()}" + do: + - var.set: + last_dast_audit_value: ${attribute.value} + on.fail: + - var.set: + candidate_selection_failed: true + stats.selection_failures: ${stats.selection_failures + 1} + stats.failures: ${stats.failures + 1} + - log.warn: "Unable to inspect DAST audit state for ${candidate.project_name}:${candidate.version_name}; skipping this version and continuing" + - if: ${!candidate_selection_failed && !#isBlank(last_dast_audit_value)} + var.set: + last_dast_audit_date_parsed: ${#date(last_dast_audit_value)} + on.fail: + - var.set: + last_dast_audit_invalid: true + - log.warn: "Ignoring invalid last_dast_audit value for ${candidate.project_name}:${candidate.version_name}; selecting the version for audit" + - if: ${!candidate_selection_failed && !latest_dast_scan_invalid && !#isBlank(last_dast_audit_value) && !last_dast_audit_invalid && !#date(latest_dast_scan_date).isAfter(#date(last_dast_audit_value))} + var.set: + stats.unchanged_skipped: ${stats.unchanged_skipped + 1} + - if: "${!candidate_selection_failed && (latest_dast_scan_invalid || #isBlank(last_dast_audit_value) || last_dast_audit_invalid || #date(latest_dast_scan_date).isAfter(#date(last_dast_audit_value)))}" + var.set: + selected_versions..: {fmt: selected_version} + + - if: ${selected_versions != null} + log.progress: Found ${selected_versions.size()} application versions requiring DAST audit before limit + - if: ${selected_versions == null} + log.progress: Found 0 application versions requiring DAST audit before limit + + - if: ${selected_versions != null && selected_versions.size() > 0} + do: + - var.set: + audit_candidates: ${selected_versions} + - if: ${cli['max-audits'] != -1 && selected_versions.size() > cli['max-audits']} + do: + - log.progress: Limiting bulk DAST audit run to ${cli['max-audits']} application versions + - var.set: + audit_candidates: null + audit_counter: 0 + - records.for-each: + from: ${selected_versions} + record.var-name: selected_version + breakIf: ${audit_counter >= cli['max-audits']} + do: + - var.set: + audit_candidates..: ${selected_version} + audit_counter: ${audit_counter + 1} + + - if: ${audit_candidates != null} + log.progress: Processing ${audit_candidates.size()} application versions for DAST audit + - if: ${audit_candidates == null} + log.progress: Processing 0 application versions for DAST audit + + - var.set: + known_aviator_app_names: ${aviator_app_names} + + - if: ${audit_candidates != null && audit_candidates.size() > 0} + do: + - records.for-each: + from: ${audit_candidates} + record.var-name: project + do: + - var.set: + app_known_in_aviator: ${known_aviator_app_names != null && known_aviator_app_names.contains(project.aviator_app_name)} + - if: ${cli['dry-run']} + do: + - if: ${!app_known_in_aviator} + do: + - log.info: Would create app ${project.aviator_app_name} + - var.set: + stats.would_create_count: ${stats.would_create_count + 1} + known_aviator_app_names..: ${project.aviator_app_name} + - log.info: Would audit ${project.project_name}:${project.version_name} + - if: ${!cli['dry-run']} + do: + - var.set: + app_ready: ${app_known_in_aviator} + app_needs_creation: ${!app_known_in_aviator} + - if: ${!app_known_in_aviator && !stats.creation_blocked} + do: + - var.set: + stats.create_attempts: ${stats.create_attempts + 1} + create_app.stdout: "" + create_app.stderr: "" + - run.fcli: + create_app: + cmd: aviator app create "${project.aviator_app_name}" + status.check: false + stdout: collect + stderr: collect + - if: ${create_app.exitCode == 0} + var.set: + app_ready: true + stats.create_successes: ${stats.create_successes + 1} + known_aviator_app_names..: ${project.aviator_app_name} + - if: ${create_app.exitCode != 0} + do: + - var.set: + create_app_error_text: "${(create_app.stderr == null ? '' : create_app.stderr) + ' ' + (create_app.stdout == null ? '' : create_app.stdout)}" + create_app_already_exists: ${create_app_error_text.toLowerCase().contains('already exists')} + create_app_entitlement_or_quota_error: ${create_app_error_text.toLowerCase().contains('entitlement') || create_app_error_text.toLowerCase().contains('quota')} + - if: ${create_app_already_exists} + do: + - var.set: + app_ready: true + known_aviator_app_names..: ${project.aviator_app_name} + - log.warn: App ${project.aviator_app_name} already exists; continuing with audit + - if: ${!create_app_already_exists} + do: + - var.set: + stats.create_failures: ${stats.create_failures + 1} + stats.failures: ${stats.failures + 1} + stats.creation_blocked: true + - if: ${create_app_entitlement_or_quota_error && !stats.entitlement_exhausted} + do: + - log.warn: App creation failed due to entitlement or quota; suppressing further create attempts + - var.set: + stats.entitlement_exhausted: true + - if: ${!create_app_entitlement_or_quota_error} + log.warn: App creation failed for ${project.aviator_app_name}; suppressing further application creation attempts + - if: ${app_needs_creation && !app_ready && stats.creation_blocked} + do: + - var.set: + stats.create_skipped_after_failure: ${stats.create_skipped_after_failure + 1} + - if: ${stats.entitlement_exhausted} + log.warn: Skipping ${project.project_name}:${project.version_name} because Aviator entitlement is exhausted + - if: ${!stats.entitlement_exhausted} + log.warn: Skipping ${project.project_name}:${project.version_name} because application creation previously failed + - if: ${app_ready} + do: + - var.set: + stats.audit_attempts: ${stats.audit_attempts + 1} + - if: ${cli['tag-mapping'] != null && cli['tag-mapping'] != ''} + run.fcli: + run_audit: + cmd: "aviator ssc audit-dast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"" + status.check: false + records.collect: true + stdout: show + stderr: collect + - if: ${cli['tag-mapping'] == null || cli['tag-mapping'] == ''} + run.fcli: + run_audit: + cmd: "aviator ssc audit-dast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"" + status.check: false + records.collect: true + stdout: show + stderr: collect + - if: "${run_audit.exitCode != 0 || (run_audit.records != null && run_audit.records.size() > 0 && (run_audit.records[0].__action__ == 'FAILED' || run_audit.records[0].__action__ == 'PARTIALLY_AUDITED'))}" + do: + - var.set: + stats.audit_failures: ${stats.audit_failures + 1} + stats.failures: ${stats.failures + 1} + - log.warn: DAST audit failed for ${project.project_name}:${project.version_name}; continuing with remaining candidates + + - if: ${cli['dry-run']} + log.info: "Dry-run complete (mapping: ${cli['aviator-app-mapping']}) - would process ${audit_candidates == null ? 0 : audit_candidates.size()} versions, missing DAST artifact skipped ${stats.missing_dast_skipped}, up-to-date last_dast_audit skipped ${stats.unchanged_skipped}, selection failures ${stats.selection_failures}, create ${stats.would_create_count} apps" + + - if: ${!cli['dry-run']} + do: + - log.info: "Complete (mapping: ${cli['aviator-app-mapping']}) - Apps created ${stats.create_successes}/${stats.create_attempts}, Audits attempted ${stats.audit_attempts}, Failures ${stats.failures} (create ${stats.create_failures}, audit ${stats.audit_failures}, selection ${stats.selection_failures}), app creations skipped after failure ${stats.create_skipped_after_failure}, missing DAST artifact skipped ${stats.missing_dast_skipped}, up-to-date last_dast_audit skipped ${stats.unchanged_skipped}" + - if: ${stats.entitlement_exhausted} + log.info: Note - Aviator entitlement was exhausted; some application creations and audits were skipped + +formatters: + candidate_version: + id: ${version.id} + project_name: ${current_project_name} + version_name: ${current_version_name} + aviator_app_name: ${current_aviator_app_name} + exists_in_aviator: ${project_exists_in_aviator} + + selected_version: + id: ${candidate.id} + project_name: ${candidate.project_name} + version_name: ${candidate.version_name} + aviator_app_name: ${candidate.aviator_app_name} + exists_in_aviator: ${candidate.exists_in_aviator} + latest_dast_scan_date: ${latest_dast_scan_date} + last_dast_audit: ${last_dast_audit_value} \ No newline at end of file diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkcorrelate.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkcorrelate.yaml index 0d09d2185d..5be480a712 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkcorrelate.yaml +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkcorrelate.yaml @@ -107,6 +107,26 @@ steps: - var.set: aviator_app_names..: ${aviator_app.name} + - var.set: + last_correlation_guid: null + last_correlation_id: null + - rest.call: + correlation_attribute_definitions: + uri: /api/v1/attributeDefinitions + type: paged + query: + limit: -1 + records.for-each: + record.var-name: attribute_definition + breakIf: ${last_correlation_guid != null || last_correlation_id != null} + if: ${attribute_definition.name == 'last_correlation' && attribute_definition.category == 'TECHNICAL' && attribute_definition.type == 'TEXT'} + do: + - var.set: + last_correlation_guid: ${attribute_definition.guid} + last_correlation_id: ${attribute_definition.id} + - if: ${last_correlation_guid == null && last_correlation_id == null} + log.warn: "SSC attribute definition 'last_correlation' was not found. Run 'fcli aviator ssc prepare' before using bulk correlation" + - log.progress: Querying SSC application versions... - var.set: candidate_versions: null @@ -267,7 +287,7 @@ steps: records.for-each: record.var-name: attribute breakIf: ${last_correlation_value != null} - if: ${attribute.guid == 'B2C3D4E5-F6A7-8901-BCDE-F12345678901'} + if: "${last_correlation_guid != null && attribute.guid == last_correlation_guid || last_correlation_id != null && attribute.attributeDefinitionId != null && attribute.attributeDefinitionId.toString() == last_correlation_id.toString()}" do: - var.set: last_correlation_value: ${attribute.value} diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAviatorAuditValidationSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAviatorAuditValidationSpec.groovy index cb4e55bc01..a94ee6fd61 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAviatorAuditValidationSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAviatorAuditValidationSpec.groovy @@ -45,4 +45,32 @@ class SSCAviatorAuditValidationSpec extends FcliBaseSpec { } } } + + def "ssc bulkaudit-dast action rejects invalid app mapping"() { + when: + def result = Fcli.run( + "ssc action run bulkaudit-dast --progress=none --aviator-app-mapping invalid", + { it.expectSuccess(false) }) + then: + verifyAll(result) { + nonZeroExitCode + stderr.any { line -> + line.contains("Invalid --aviator-app-mapping value 'invalid'") + } + } + } + + def "ssc bulkaudit-dast action rejects invalid max audits"() { + when: + def result = Fcli.run( + "ssc action run bulkaudit-dast --progress=none --max-audits=-2", + { it.expectSuccess(false) }) + then: + verifyAll(result) { + nonZeroExitCode + stderr.any { line -> + line.contains("Invalid --max-audits value '-2'") + } + } + } } \ No newline at end of file From cb0ebba1890840fc31616f1a606f4ac47bd0724c Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 17 Sep 2026 11:14:48 +0530 Subject: [PATCH 2/3] chore: refactor bulk Aviator auditing into dedicated SAST and DAST actions with backward compatibility --- .../cli/ssc/actions/zip/bulkaudit-dast.yaml | 128 +++-- .../cli/ssc/actions/zip/bulkaudit-sast.yaml | 504 ++++++++++++++++++ .../cli/ssc/actions/zip/bulkaudit.yaml | 486 ++--------------- .../ssc/SSCAviatorAuditValidationSpec.groovy | 7 +- 4 files changed, 647 insertions(+), 478 deletions(-) create mode 100644 fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-sast.yaml diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-dast.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-dast.yaml index 8629743677..fd1410b707 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-dast.yaml +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-dast.yaml @@ -9,13 +9,13 @@ usage: Versions without a processed WebInspect result, or whose DAST scan has already been evaluated, are skipped before any audit command is invoked. - Before using this action, run `fcli aviator ssc prepare` with an Aviator admin - configuration so the Aviator tags and the `last_dast_audit` application-version - attribute are available in SSC. The action requires an active SSC session, an - Aviator user session for DAST auditing, and an Aviator admin configuration for - listing and creating Aviator applications. Unless nested command defaults are - overridden, these use the `default` SSC session, Aviator user session, and admin - configuration. + Before each DAST audit, the action runs `fcli aviator ssc prepare` for the selected + application version so the Aviator tags and the `last_dast_audit` + application-version attribute are available in SSC. The action requires an active + SSC session, an Aviator user session for DAST auditing, and an Aviator admin + configuration for listing and creating Aviator applications. Unless nested command + defaults are overridden, these use the `default` SSC session, Aviator user session, + and admin configuration. For application versions that don't already exist in Fortify Aviator, the action automatically creates them before submitting the DAST audit. By default @@ -77,6 +77,29 @@ cli.options: required: false default: "60s" +functions: + runDastAudit: + description: Run a DAST audit and return the command result + export: false + args: + projectId: { required: true } + aviatorAppName: { required: true } + tagMapping: { required: true } + refresh: { required: true, type: boolean } + refreshTimeout: { required: true } + return: ${run_audit} + steps: + - run.fcli: + run_audit: + cmd: >- + aviator ssc audit-dast --av "${args.projectId}" --app "${args.aviatorAppName}" + --log-level=INFO${#isBlank(args.tagMapping) ? '' : ' --tag-mapping="' + args.tagMapping + '"'} + --refresh=${args.refresh} --refresh-timeout="${args.refreshTimeout}" + status.check: false + records.collect: true + stdout: show + stderr: collect + steps: - var.set: module: ssc @@ -85,6 +108,7 @@ steps: stats.create_attempts: 0 stats.create_successes: 0 stats.create_failures: 0 + stats.prepare_failures: 0 stats.audit_attempts: 0 stats.audit_failures: 0 stats.failures: 0 @@ -136,7 +160,9 @@ steps: last_dast_audit_guid: ${attribute_definition.guid} last_dast_audit_id: ${attribute_definition.id} - if: ${last_dast_audit_guid == null && last_dast_audit_id == null} - log.warn: "SSC attribute definition 'last_dast_audit' was not found. Run 'fcli aviator ssc prepare' before using bulk DAST audit" + log.warn: >- + SSC attribute definition 'last_dast_audit' was not found. The action will attempt to create it + while preparing each selected application version before audit - log.progress: Querying SSC application versions... - var.set: @@ -260,6 +286,7 @@ steps: record.var-name: candidate do: - var.set: + dast_artifact_found: false latest_dast_scan_date: null latest_dast_scan_invalid: false latest_dast_scan_date_parsed: null @@ -277,31 +304,43 @@ steps: orderby: uploadDate DESC records.for-each: record.var-name: artifact - breakIf: ${latest_dast_scan_date != null} do: - var.set: artifact_scan_date: ${#ifBlank(artifact.lastScanDate, artifact.uploadDate)} - - if: ${latest_dast_scan_date == null && artifact.status == 'PROCESS_COMPLETE' && !#isBlank(artifact_scan_date) && artifact._embed.scans?.^[type=='WEBINSPECT'] != null} - var.set: - latest_dast_scan_date: ${artifact_scan_date} + artifact_scan_date_parsed: null + - if: >- + ${artifact.status == 'PROCESS_COMPLETE' && !#isBlank(artifact_scan_date) + && artifact._embed.scans?.^[type=='WEBINSPECT'] != null} + do: + - var.set: + dast_artifact_found: true + - var.set: + artifact_scan_date_parsed: ${#date(artifact_scan_date)} + on.fail: + - var.set: + latest_dast_scan_invalid: true + latest_dast_scan_date: "${latest_dast_scan_date == null ? artifact_scan_date : latest_dast_scan_date}" + - log.warn: >- + Ignoring invalid DAST scan date for + ${candidate.project_name}:${candidate.version_name}; selecting the version for audit + - if: >- + ${artifact_scan_date_parsed != null && (latest_dast_scan_date_parsed == null + || #date(artifact_scan_date).isAfter(#date(latest_dast_scan_date)))} + var.set: + latest_dast_scan_date: ${artifact_scan_date} + latest_dast_scan_date_parsed: ${artifact_scan_date_parsed} on.fail: - var.set: candidate_selection_failed: true stats.selection_failures: ${stats.selection_failures + 1} stats.failures: ${stats.failures + 1} - log.warn: "Unable to inspect DAST artifacts for ${candidate.project_name}:${candidate.version_name}; skipping this version and continuing" - - if: ${!candidate_selection_failed && latest_dast_scan_date == null} + - if: ${!candidate_selection_failed && !dast_artifact_found} do: - var.set: stats.missing_dast_skipped: ${stats.missing_dast_skipped + 1} - - if: ${!candidate_selection_failed && latest_dast_scan_date != null} + - if: ${!candidate_selection_failed && dast_artifact_found} do: - - var.set: - latest_dast_scan_date_parsed: ${#date(latest_dast_scan_date)} - on.fail: - - var.set: - latest_dast_scan_invalid: true - - log.warn: "Ignoring invalid DAST scan date for ${candidate.project_name}:${candidate.version_name}; selecting the version for audit" - rest.call: candidate_attributes: uri: /api/v1/projectVersions/${candidate.id}/attributes @@ -380,6 +419,9 @@ steps: - var.set: stats.would_create_count: ${stats.would_create_count + 1} known_aviator_app_names..: ${project.aviator_app_name} + - log.info: >- + Would prepare Fortify Aviator tags and attributes for + ${project.project_name}:${project.version_name} - log.info: Would audit ${project.project_name}:${project.version_name} - if: ${!cli['dry-run']} do: @@ -420,14 +462,14 @@ steps: - var.set: stats.create_failures: ${stats.create_failures + 1} stats.failures: ${stats.failures + 1} - stats.creation_blocked: true - if: ${create_app_entitlement_or_quota_error && !stats.entitlement_exhausted} do: - log.warn: App creation failed due to entitlement or quota; suppressing further create attempts - var.set: stats.entitlement_exhausted: true + stats.creation_blocked: true - if: ${!create_app_entitlement_or_quota_error} - log.warn: App creation failed for ${project.aviator_app_name}; suppressing further application creation attempts + log.warn: App creation failed for ${project.aviator_app_name}; continuing with remaining candidates - if: ${app_needs_creation && !app_ready && stats.creation_blocked} do: - var.set: @@ -438,24 +480,26 @@ steps: log.warn: Skipping ${project.project_name}:${project.version_name} because application creation previously failed - if: ${app_ready} do: + - log.progress: >- + Preparing Fortify Aviator tags and attributes for + ${project.project_name}:${project.version_name} + - run.fcli: + prepare_version: + cmd: aviator ssc prepare --av "${project.id}" + status.check: false + - if: ${prepare_version.exitCode != 0} + do: + - var.set: + stats.prepare_failures: ${stats.prepare_failures + 1} + - log.warn: >- + Fortify Aviator preparation failed for + ${project.project_name}:${project.version_name} - var.set: stats.audit_attempts: ${stats.audit_attempts + 1} - - if: ${cli['tag-mapping'] != null && cli['tag-mapping'] != ''} - run.fcli: - run_audit: - cmd: "aviator ssc audit-dast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"" - status.check: false - records.collect: true - stdout: show - stderr: collect - - if: ${cli['tag-mapping'] == null || cli['tag-mapping'] == ''} - run.fcli: - run_audit: - cmd: "aviator ssc audit-dast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"" - status.check: false - records.collect: true - stdout: show - stderr: collect + - var.set: + run_audit: >- + ${#fn.call('runDastAudit', project.id, project.aviator_app_name, + #ifBlank(cli['tag-mapping'], ''), cli.refresh, cli['refresh-timeout'])} - if: "${run_audit.exitCode != 0 || (run_audit.records != null && run_audit.records.size() > 0 && (run_audit.records[0].__action__ == 'FAILED' || run_audit.records[0].__action__ == 'PARTIALLY_AUDITED'))}" do: - var.set: @@ -468,7 +512,13 @@ steps: - if: ${!cli['dry-run']} do: - - log.info: "Complete (mapping: ${cli['aviator-app-mapping']}) - Apps created ${stats.create_successes}/${stats.create_attempts}, Audits attempted ${stats.audit_attempts}, Failures ${stats.failures} (create ${stats.create_failures}, audit ${stats.audit_failures}, selection ${stats.selection_failures}), app creations skipped after failure ${stats.create_skipped_after_failure}, missing DAST artifact skipped ${stats.missing_dast_skipped}, up-to-date last_dast_audit skipped ${stats.unchanged_skipped}" + - log.info: >- + Complete (mapping: ${cli['aviator-app-mapping']}) - Apps created + ${stats.create_successes}/${stats.create_attempts}, Audits attempted ${stats.audit_attempts}, + Failures ${stats.failures} (create ${stats.create_failures}, audit ${stats.audit_failures}, + selection ${stats.selection_failures}), Prepare warnings ${stats.prepare_failures}, app creations + skipped after failure ${stats.create_skipped_after_failure}, missing DAST artifact skipped + ${stats.missing_dast_skipped}, up-to-date last_dast_audit skipped ${stats.unchanged_skipped} - if: ${stats.entitlement_exhausted} log.info: Note - Aviator entitlement was exhausted; some application creations and audits were skipped diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-sast.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-sast.yaml new file mode 100644 index 0000000000..d28a21f5fd --- /dev/null +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit-sast.yaml @@ -0,0 +1,504 @@ +# yaml-language-server: $schema=https://fortify.github.io/fcli/schemas/action/fcli-action-schema-dev-2.x.json + +author: Fortify +usage: + header: (PREVIEW) Perform Fortify Remediation Aviator SAST audits of SSC application versions in bulk. + description: | + This action identifies SSC application versions with pending SAST audit issues and + automatically submits them for auditing by Fortify Remediation Aviator. The action + intelligently filters to only process versions with actual pending audit issues, + while ignoring versions for which audit information needs to be refreshed. + + For application versions that don't already exist in Fortify Aviator, the action will + automatically create them before submitting for audit. + + By default (--aviator-app-mapping=app), SSC project versions map to a single + Fortify Aviator application per SSC project. Set --aviator-app-mapping=version to map + each SSC project version to its own Fortify Aviator application (project_name__version_name). + + When quota limits are exceeded for any application, issues are prioritized by + folder using either the default order (Critical > High > Medium > Low) or a + custom priority order specified via --folder-priority-order. The same priority + order is applied to all audited versions in the bulk operation. + + Either --tag-mapping or --add-aviator-tags must be specified. The default tag + mapping leaves unsure issues unaudited, causing indefinite reselection. Use + a custom mapping file or 'fcli ssc aviator prepare' to configure Fortify Remediation Aviator-specific + issue templates. + + This action assumes active sessions with SSC, Fortify Remediation Aviator (user role for auditing), + and Fortify Aviator (admin role for application listing and creation). + +config: + output: immediate + rest.target.default: ssc + run.fcli.status.log.default: true + run.fcli.status.check.default: false + mcp: exclude # Not suitable for MCP tool invocation + +cli.options: + max-audits: + names: --max-audits, -m + description: "Maximum number of project versions to audit. Default: -1 (unlimited)" + required: false + default: -1 + type: int + aviator-app-mapping: + names: --aviator-app-mapping + description: "Controls how SSC project versions map to Fortify Aviator applications. 'app' (default) maps one Fortify Aviator app per SSC project; 'version' maps one Fortify Aviator app per SSC project version (using project_name__version_name)." + required: false + default: app + filter: + names: --filter, -f + description: "Optional filter to apply when querying SSC for projects. Example: 'Languages:java'. Default: no filtering" + required: false + default: "" + exclude-filter: + names: --exclude-filter, -e + description: "Optional inverse filter to exclude projects from audit. Projects matching this filter will be skipped. Example: 'Languages:c#' to exclude .NET projects. Can be combined with --filter. Default: no exclusion" + required: false + default: "" + dry-run: + names: --dry-run, -n + description: "Show what aviator commands would be executed without actually running them. Default: false" + required: false + default: false + type: boolean + tag-mapping: + names: --tag-mapping, -t + description: "Path to tag mapping YAML file. This custom mapping ensures that Fortify Remediation Aviator 'Unsure' audit results are properly handled. Required unless --add-aviator-tags is specified." + required: false + add-aviator-tags: + names: --add-aviator-tags + description: "If specified, runs 'fcli aviator ssc prepare' for the application before audit. Required unless --tag-mapping is specified." + required: false + type: boolean + default: false + refresh: + names: --refresh + description: "Refresh application version metrics before audit. For large applications this can lead to timeout errors. Default: true" + required: false + type: boolean + default: true + refresh-timeout: + names: --refresh-timeout + description: "Timeout period for metric refresh, e.g., 30s (30 seconds), 5m (5 minutes), 1h (1 hour). Default: 60s" + required: false + default: "60s" + skip-if-exceeding-quota: + names: --skip-if-exceeding-quota + description: "Skip audit if the number of open issues exceeds the available Fortify Remediation Aviator quota. When skipped, a summary with top unaudited categories is shown. Default: false" + required: false + default: false + type: boolean + test-exceeding-quota: + names: --test-exceeding-quota + description: "Check whether the number of open issues exceeds the available Fortify Remediation Aviator quota and report the result without performing an audit. Default: false" + required: false + default: false + type: boolean + folder-priority-order: + names: --folder-priority-order + description: "Custom priority order for folder-based filtering when quota is exceeded (comma-separated, highest priority first). Example: Critical,High,Medium,Low. Applied to all audited versions. Default: uses server default (Critical > High > Medium > Low)" + required: false + default: "" + +functions: + runSastAudit: + description: Run a SAST audit and return the command result + export: false + args: + projectId: { required: true } + aviatorAppName: { required: true } + tagMapping: { required: true } + refresh: { required: true, type: boolean } + refreshTimeout: { required: true } + quotaFlags: { required: true } + folderPriorityOrder: { required: true } + return: ${run_audit} + steps: + - run.fcli: + run_audit: + cmd: >- + aviator ssc audit-sast --av "${args.projectId}" --app "${args.aviatorAppName}" + --log-level=INFO${#isBlank(args.tagMapping) ? '' : ' --tag-mapping="' + args.tagMapping + '"'} + --refresh=${args.refresh} --refresh-timeout="${args.refreshTimeout}"${args.quotaFlags} + ${#isBlank(args.folderPriorityOrder) ? '' : ' --folder-priority-order="' + args.folderPriorityOrder + '"'} + status.check: false + records.collect: true + stdout: show + +steps: + # Configure module + - var.set: + module: ssc + + # Validate --aviator-app-mapping value + - if: ${!(cli['aviator-app-mapping'] matches 'app|version')} + throw: "Invalid --aviator-app-mapping value '${cli['aviator-app-mapping']}'. Valid values are: app, version" + + # Validate that at least one of --tag-mapping or --add-aviator-tags is specified + - if: ${(cli['tag-mapping'] == null || cli['tag-mapping'] == '') && !cli['add-aviator-tags']} + throw: "Either --tag-mapping or --add-aviator-tags must be specified." + + # Validate that --dry-run and --test-exceeding-quota are not used together + - if: ${cli['dry-run'] && cli['test-exceeding-quota']} + do: + - log.progress: "ERROR: --dry-run and --test-exceeding-quota cannot be used together. Use --test-exceeding-quota alone to check quota without auditing." + - throw: "--dry-run and --test-exceeding-quota cannot be used together. Use --test-exceeding-quota alone to check quota without auditing." + + # Validate that --dry-run and --skip-if-exceeding-quota are not used together + - if: ${cli['dry-run'] && cli['skip-if-exceeding-quota']} + do: + - log.progress: "ERROR: --dry-run and --skip-if-exceeding-quota cannot be used together. --dry-run prevents server interaction, but --skip-if-exceeding-quota requires quota retrieval." + - throw: "--dry-run and --skip-if-exceeding-quota cannot be used together. --dry-run prevents server interaction, but --skip-if-exceeding-quota requires quota retrieval." + + # Validate that --skip-if-exceeding-quota and --folder-priority-order are not used together + - if: ${cli['skip-if-exceeding-quota'] && cli['folder-priority-order'] != null && cli['folder-priority-order'] != ''} + do: + - log.progress: "ERROR: --skip-if-exceeding-quota and --folder-priority-order cannot be used together. --skip-if-exceeding-quota skips the audit when quota is insufficient, while --folder-priority-order only applies when auditing within quota constraints." + - throw: "--skip-if-exceeding-quota and --folder-priority-order cannot be used together. --skip-if-exceeding-quota skips the audit when quota is insufficient, while --folder-priority-order only applies when auditing within quota constraints." + + - log.progress: "Using Fortify Aviator app mapping: ${cli['aviator-app-mapping']}" + + # Get existing Fortify Aviator applications + - log.progress: Retrieving existing Fortify Aviator applications... + - run.fcli: + aviator_apps: + cmd: aviator app ls -o json + records.collect: true + + # Build Aviator app name set for fast membership checks + - var.set: + aviator_app_names: null + - records.for-each: + from: ${aviator_apps.records} + record.var-name: aviator_app + do: + - var.set: + aviator_app_names..: ${aviator_app.name} + + # Query SSC for projects needing audit + - log.progress: Querying SSC for projects with pending audit issues... + + - var.set: + enriched_versions: null + + - rest.call: + projects_needing_audit: + uri: /api/v1/issueaging + type: paged + query: + limit: -1 + filterby: ${cli.filter} + records.for-each: + record.var-name: version + if: ${version.issuesPendingReview > 0 && !version.snapshotOutOfDate} + embed: + project_details: + uri: /api/v1/projectVersions/${version.id} + do: + - var.set: + current_project_name: ${version.project_details.project.name} + current_version_name: ${version.project_details.name} + current_aviator_app_name: ${version.project_details.project.name.replaceAll('"', '')} + - if: ${'version'.equals(cli['aviator-app-mapping'])} + var.set: + current_aviator_app_name: ${(current_project_name + '__' + current_version_name).replaceAll('"', '')} + - var.set: + project_exists_in_aviator: ${aviator_app_names != null && aviator_app_names.contains(current_aviator_app_name)} + - var.set: + enriched_versions..: {fmt: enriched_project} + + - if: ${enriched_versions != null} + log.progress: Found ${enriched_versions.size()} application versions with pending audit issues + - if: ${enriched_versions == null} + log.progress: Found 0 application versions with pending audit issues + + # Apply exclusion filter if specified + - if: ${enriched_versions != null && enriched_versions.size() > 0 && cli['exclude-filter'] != ""} + do: + - log.progress: Applying exclusion filter... + - rest.call: + exclusion_list: + uri: /api/v1/issueaging + type: paged + query: + limit: -1 + filterby: ${cli['exclude-filter']} + records.for-each: + record.var-name: excluded_version + if: ${excluded_version.issuesPendingReview > 0 && !excluded_version.snapshotOutOfDate} + do: + - var.set: + excluded_ids..: ${excluded_version.id} + + - var.set: + filtered_versions: null + excluded_count: 0 + + - records.for-each: + from: ${enriched_versions} + record.var-name: candidate + do: + - var.set: + should_exclude: false + - if: ${excluded_ids != null && excluded_ids.contains(candidate.id)} + var.set: + should_exclude: true + - if: ${should_exclude} + var.set: + excluded_count: ${excluded_count + 1} + - if: ${!should_exclude} + var.set: + filtered_versions..: ${candidate} + + - var.set: + enriched_versions: ${filtered_versions} + - if: ${enriched_versions != null} + log.progress: Excluded ${excluded_count} application versions, ${enriched_versions.size()} remaining + - if: ${enriched_versions == null} + log.progress: Excluded ${excluded_count} application versions, 0 remaining + + # Early exit if no candidates + - if: ${enriched_versions == null || enriched_versions.size() == 0} + do: + - log.info: No application versions found that require audit + - var.set: + audit_candidates: null + + # Apply max-audits limit if we have candidates + - if: ${enriched_versions != null && enriched_versions.size() > 0} + do: + - var.set: + audit_candidates: ${enriched_versions} + - if: ${cli['max-audits'] != -1 && enriched_versions.size() > cli['max-audits']} + do: + - var.set: + audit_counter: 0 + audit_candidates: null + - records.for-each: + from: ${enriched_versions} + record.var-name: candidate + breakIf: ${audit_counter >= cli['max-audits']} + do: + - var.set: + audit_candidates..: ${candidate} + audit_counter: ${audit_counter + 1} + + - if: ${audit_candidates != null} + log.progress: Processing ${audit_candidates.size()} application versions + - if: ${audit_candidates == null} + log.progress: Processing 0 application versions + + # Process audit candidates + - if: ${audit_candidates != null && audit_candidates.size() > 0} + do: + # Initialize execution tracking + - var.set: + stats.create_attempts: 0 + stats.create_successes: 0 + stats.create_failures: 0 + stats.audit_attempts: 0 + stats.audit_failures: 0 + stats.quota_skipped: 0 + stats.entitlement_exhausted: false + stats.create_skipped_due_to_entitlement: 0 + stats.would_create_count: 0 + known_aviator_app_names: ${aviator_app_names} + + # Process each candidate + - records.for-each: + from: ${audit_candidates} + record.var-name: project + do: + - var.set: + app_known_in_aviator: ${known_aviator_app_names != null && known_aviator_app_names.contains(project.aviator_app_name)} + + - if: ${cli['dry-run']} + do: + - if: ${!app_known_in_aviator} + do: + - log.info: Would create app ${project.aviator_app_name} + - var.set: + stats.would_create_count: ${stats.would_create_count + 1} + known_aviator_app_names..: ${project.aviator_app_name} + - if: ${cli['add-aviator-tags']} + do: + - log.info: Would prepare tags for ${project.project_name}:${project.version_name} + + - if: "${cli['folder-priority-order'] != null && cli['folder-priority-order'] != ''}" + do: + - log.info: "Would audit ${project.project_name}:${project.version_name} with custom priority order: ${cli['folder-priority-order']}" + - if: "${cli['folder-priority-order'] == null || cli['folder-priority-order'] == ''}" + do: + - log.info: Would audit ${project.project_name}:${project.version_name} + - if: ${!cli['dry-run']} + do: + - var.set: + app_ready: ${app_known_in_aviator} + skip_this_version: false + + # --- TEST-EXCEEDING-QUOTA MODE --- + # In test mode we only check quota and report; no app creation, no audit. + # For non-existing apps, --default-quota-fallback tells the audit command + # to use the tenant's default quota instead of reporting "app not found". + - if: ${cli['test-exceeding-quota']} + do: + - var.set: + stats.audit_attempts: ${stats.audit_attempts + 1} + quota_flags: " --test-exceeding-quota" + - if: ${!app_known_in_aviator} + var.set: + quota_flags: " --test-exceeding-quota --default-quota-fallback" + + - var.set: + run_audit: >- + ${#fn.call('runSastAudit', project.id, project.aviator_app_name, + #ifBlank(cli['tag-mapping'], ''), cli.refresh, cli['refresh-timeout'], quota_flags, + #ifBlank(cli['folder-priority-order'], ''))} + + - if: ${run_audit.exitCode == 0 && run_audit.records != null && run_audit.records.size() > 0 && run_audit.records[0].__action__ != null && run_audit.records[0].__action__ == 'QUOTA_EXCEEDED'} + var.set: + stats.quota_skipped: ${stats.quota_skipped + 1} + + - if: ${run_audit.exitCode != 0} + do: + - var.set: + stats.audit_failures: ${stats.audit_failures + 1} + - log.warn: Quota test failed for ${project.aviator_app_name}:${project.version_name} + + - var.set: + skip_this_version: true + + # --- SKIP-IF-EXCEEDING-QUOTA + NON-EXISTING APP --- + # Pre-check default quota before creating the app. If the default quota + # would be exceeded, skip the version entirely (don't create, don't audit). + - if: ${!skip_this_version && cli['skip-if-exceeding-quota'] && !app_known_in_aviator} + do: + - log.progress: Pre-checking default quota for new app ${project.aviator_app_name}... + + - var.set: + quota_precheck: >- + ${#fn.call('runSastAudit', project.id, project.aviator_app_name, + #ifBlank(cli['tag-mapping'], ''), cli.refresh, cli['refresh-timeout'], + ' --test-exceeding-quota --default-quota-fallback', + #ifBlank(cli['folder-priority-order'], ''))} + + # If pre-check shows quota exceeded, skip this version entirely + - if: ${quota_precheck.exitCode == 0 && quota_precheck.records != null && quota_precheck.records.size() > 0 && quota_precheck.records[0].__action__ != null && quota_precheck.records[0].__action__ == 'QUOTA_EXCEEDED'} + do: + - log.progress: Default quota exceeded for ${project.aviator_app_name} - skipping app creation and audit + - var.set: + skip_this_version: true + stats.quota_skipped: ${stats.quota_skipped + 1} + + # --- NORMAL FLOW: create app, prepare tags, run audit --- + - if: ${!skip_this_version} + do: + # Create app if needed + - if: ${!app_known_in_aviator && !stats.entitlement_exhausted} + do: + - var.set: + stats.create_attempts: ${stats.create_attempts + 1} + - run.fcli: + create_app: + cmd: aviator app create "${project.aviator_app_name}" + status.check: false + - if: ${create_app.exitCode == 0} + var.set: + app_ready: true + stats.create_successes: ${stats.create_successes + 1} + known_aviator_app_names..: ${project.aviator_app_name} + - if: ${create_app.exitCode != 0} + do: + - var.set: + create_app_error_text: "${(create_app.stderr == null ? '' : create_app.stderr) + ' ' + (create_app.stdout == null ? '' : create_app.stdout)}" + create_app_already_exists: ${create_app_error_text.toLowerCase().contains('already exists')} + create_app_entitlement_or_quota_error: ${create_app_error_text.toLowerCase().contains('entitlement') || create_app_error_text.toLowerCase().contains('quota')} + - if: ${create_app_already_exists} + var.set: + app_ready: true + known_aviator_app_names..: ${project.aviator_app_name} + - if: ${!create_app_already_exists} + do: + - var.set: + stats.create_failures: ${stats.create_failures + 1} + - if: ${create_app_entitlement_or_quota_error && !stats.entitlement_exhausted} + do: + - log.warn: App creation failed due to entitlement/quota - suppressing further create attempts + - var.set: + stats.entitlement_exhausted: true + - if: ${!create_app_entitlement_or_quota_error} + log.warn: App creation failed for ${project.aviator_app_name}; continuing with remaining candidates + + - if: ${!app_known_in_aviator && stats.entitlement_exhausted} + var.set: + stats.create_skipped_due_to_entitlement: ${stats.create_skipped_due_to_entitlement + 1} + + # Prepare Fortify Remediation Aviator tags if requested + - if: ${cli['add-aviator-tags']} + do: + - log.progress: Preparing Fortify Remediation Aviator tags for ${project.project_name}:${project.version_name} + - run.fcli: + prepare_tags: + cmd: aviator ssc prepare --av "${project.id}" + status.check: false + - if: ${prepare_tags.exitCode != 0} + do: + - log.warn: Fortify Remediation Aviator tag preparation failed for ${project.project_name}:${project.version_name} + + # Run audit if app is ready + - if: ${app_ready} + do: + - var.set: + stats.audit_attempts: ${stats.audit_attempts + 1} + + # Build quota flags string (only --skip-if-exceeding-quota here; + # --test-exceeding-quota is handled in its own block above) + - var.set: + quota_flags: "" + - if: ${cli['skip-if-exceeding-quota']} + var.set: + quota_flags: " --skip-if-exceeding-quota" + + - var.set: + run_audit: >- + ${#fn.call('runSastAudit', project.id, project.aviator_app_name, + #ifBlank(cli['tag-mapping'], ''), cli.refresh, cli['refresh-timeout'], quota_flags, + #ifBlank(cli['folder-priority-order'], ''))} + + # Track quota-skipped results + - if: ${run_audit.exitCode == 0 && run_audit.records != null && run_audit.records.size() > 0 && run_audit.records[0].__action__ != null && run_audit.records[0].__action__ == 'QUOTA_EXCEEDED'} + var.set: + stats.quota_skipped: ${stats.quota_skipped + 1} + + - if: ${run_audit.exitCode != 0} + do: + - var.set: + stats.audit_failures: ${stats.audit_failures + 1} + - log.warn: Audit failed for ${project.aviator_app_name}:${project.version_name} + + # Summary + - if: ${cli['dry-run']} + do: + - log.info: "Dry-run complete (mapping: ${cli['aviator-app-mapping']}) - would process ${audit_candidates.size()} versions and create ${stats.would_create_count} apps" + + - if: ${!cli['dry-run']} + do: + - if: ${stats.quota_skipped > 0} + log.info: "Complete (mapping: ${cli['aviator-app-mapping']}) - Apps created ${stats.create_successes}/${stats.create_attempts}, Audits attempted ${stats.audit_attempts}, Quota-skipped ${stats.quota_skipped}" + - if: ${stats.quota_skipped == 0} + log.info: "Complete (mapping: ${cli['aviator-app-mapping']}) - Apps created ${stats.create_successes}/${stats.create_attempts}, Audits attempted ${stats.audit_attempts}" + - if: ${stats.entitlement_exhausted} + log.info: Note - Entitlement exhausted, some app creations were skipped + +formatters: + enriched_project: + id: ${version.id} + name: ${version.name} + issuesPendingReview: ${version.issuesPendingReview} + project_name: ${version.project_details.project.name} + version_name: ${version.project_details.name} + aviator_app_name: ${current_aviator_app_name} + exists_in_aviator: ${project_exists_in_aviator} diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml index e2b01118e1..b62cd74b3e 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml @@ -2,507 +2,119 @@ author: Fortify usage: - header: (PREVIEW) Perform Fortify Remediation Aviator audits of SSC application versions in bulk. + header: (PREVIEW, DEPRECATED) Perform SAST Aviator audits of SSC application versions in bulk. description: | - This action identifies SSC application versions with pending audit issues and - automatically submits them for auditing by Fortify Remediation Aviator. The action - intelligently filters to only process versions with actual pending audit issues, - while ignoring versions for which audit information needs to be refreshed. + This action is deprecated; use the `bulkaudit-sast` action instead. - For application versions that don't already exist in Fortify Aviator, the action will - automatically create them before submitting for audit. - - By default (--aviator-app-mapping=app), SSC project versions map to a single - Fortify Aviator application per SSC project. Set --aviator-app-mapping=version to map - each SSC project version to its own Fortify Aviator application (project_name__version_name). - - When quota limits are exceeded for any application, issues are prioritized by - folder using either the default order (Critical > High > Medium > Low) or a - custom priority order specified via --folder-priority-order. The same priority - order is applied to all audited versions in the bulk operation. - - Either --tag-mapping or --add-aviator-tags must be specified. The default tag - mapping leaves unsure issues unaudited, causing indefinite reselection. Use - a custom mapping file or 'fcli ssc aviator prepare' to configure Fortify Remediation Aviator-specific - issue templates. - - This action assumes active sessions with SSC, Fortify Remediation Aviator (user role for auditing), - and Fortify Aviator (admin role for application listing and creation). + For backward compatibility, this action accepts the original `bulkaudit` options + and delegates execution to `bulkaudit-sast`. config: output: immediate - rest.target.default: ssc run.fcli.status.log.default: true - run.fcli.status.check.default: false mcp: exclude # Not suitable for MCP tool invocation cli.options: max-audits: + group: bulkaudit-sast-options names: --max-audits, -m description: "Maximum number of project versions to audit. Default: -1 (unlimited)" required: false default: -1 type: int aviator-app-mapping: + group: bulkaudit-sast-options names: --aviator-app-mapping - description: "Controls how SSC project versions map to Fortify Aviator applications. 'app' (default) maps one Fortify Aviator app per SSC project; 'version' maps one Fortify Aviator app per SSC project version (using project_name__version_name)." + description: >- + Controls how SSC project versions map to Fortify Aviator applications. 'app' (default) maps one + Fortify Aviator app per SSC project; 'version' maps one Fortify Aviator app per SSC project version + (using project_name__version_name). required: false default: app filter: + group: bulkaudit-sast-options names: --filter, -f - description: "Optional filter to apply when querying SSC for projects. Example: 'Languages:java'. Default: no filtering" + description: >- + Optional filter to apply when querying SSC for projects. Example: 'Languages:java'. Default: no filtering required: false default: "" exclude-filter: + group: bulkaudit-sast-options names: --exclude-filter, -e - description: "Optional inverse filter to exclude projects from audit. Projects matching this filter will be skipped. Example: 'Languages:c#' to exclude .NET projects. Can be combined with --filter. Default: no exclusion" + description: >- + Optional inverse filter to exclude projects from audit. Projects matching this filter will be skipped. + Example: 'Languages:c#' to exclude .NET projects. Can be combined with --filter. Default: no exclusion required: false default: "" dry-run: + group: bulkaudit-sast-options names: --dry-run, -n description: "Show what aviator commands would be executed without actually running them. Default: false" required: false default: false type: boolean tag-mapping: + group: bulkaudit-sast-options names: --tag-mapping, -t - description: "Path to tag mapping YAML file. This custom mapping ensures that Fortify Remediation Aviator 'Unsure' audit results are properly handled. Required unless --add-aviator-tags is specified." + description: >- + Path to tag mapping YAML file. This custom mapping ensures that Fortify Remediation Aviator 'Unsure' + audit results are properly handled. Required unless --add-aviator-tags is specified. required: false add-aviator-tags: + group: bulkaudit-sast-options names: --add-aviator-tags - description: "If specified, runs 'fcli aviator ssc prepare' for the application before audit. Required unless --tag-mapping is specified." + description: >- + If specified, runs 'fcli aviator ssc prepare' for the application before audit. Required unless + --tag-mapping is specified. required: false type: boolean default: false refresh: + group: bulkaudit-sast-options names: --refresh - description: "Refresh application version metrics before audit. For large applications this can lead to timeout errors. Default: true" + description: >- + Refresh application version metrics before audit. For large applications this can lead to timeout errors. + Default: true required: false type: boolean default: true refresh-timeout: + group: bulkaudit-sast-options names: --refresh-timeout - description: "Timeout period for metric refresh, e.g., 30s (30 seconds), 5m (5 minutes), 1h (1 hour). Default: 60s" + description: >- + Timeout period for metric refresh, e.g., 30s (30 seconds), 5m (5 minutes), 1h (1 hour). Default: 60s required: false default: "60s" skip-if-exceeding-quota: + group: bulkaudit-sast-options names: --skip-if-exceeding-quota - description: "Skip audit if the number of open issues exceeds the available Fortify Remediation Aviator quota. When skipped, a summary with top unaudited categories is shown. Default: false" + description: >- + Skip audit if the number of open issues exceeds the available Fortify Remediation Aviator quota. + When skipped, a summary with top unaudited categories is shown. Default: false required: false default: false type: boolean test-exceeding-quota: + group: bulkaudit-sast-options names: --test-exceeding-quota - description: "Check whether the number of open issues exceeds the available Fortify Remediation Aviator quota and report the result without performing an audit. Default: false" + description: >- + Check whether the number of open issues exceeds the available Fortify Remediation Aviator quota and + report the result without performing an audit. Default: false required: false default: false type: boolean folder-priority-order: + group: bulkaudit-sast-options names: --folder-priority-order - description: "Custom priority order for folder-based filtering when quota is exceeded (comma-separated, highest priority first). Example: Critical,High,Medium,Low. Applied to all audited versions. Default: uses server default (Critical > High > Medium > Low)" + description: >- + Custom priority order for folder-based filtering when quota is exceeded (comma-separated, highest priority + first). Example: Critical,High,Medium,Low. Applied to all audited versions. Default: uses server default + (Critical > High > Medium > Low) required: false default: "" steps: - # Configure module - - var.set: - module: ssc - - # Validate --aviator-app-mapping value - - if: ${!(cli['aviator-app-mapping'] matches 'app|version')} - throw: "Invalid --aviator-app-mapping value '${cli['aviator-app-mapping']}'. Valid values are: app, version" - - # Validate that at least one of --tag-mapping or --add-aviator-tags is specified - - if: ${(cli['tag-mapping'] == null || cli['tag-mapping'] == '') && !cli['add-aviator-tags']} - throw: "Either --tag-mapping or --add-aviator-tags must be specified." - - # Validate that --dry-run and --test-exceeding-quota are not used together - - if: ${cli['dry-run'] && cli['test-exceeding-quota']} - do: - - log.progress: "ERROR: --dry-run and --test-exceeding-quota cannot be used together. Use --test-exceeding-quota alone to check quota without auditing." - - throw: "--dry-run and --test-exceeding-quota cannot be used together. Use --test-exceeding-quota alone to check quota without auditing." - - # Validate that --dry-run and --skip-if-exceeding-quota are not used together - - if: ${cli['dry-run'] && cli['skip-if-exceeding-quota']} - do: - - log.progress: "ERROR: --dry-run and --skip-if-exceeding-quota cannot be used together. --dry-run prevents server interaction, but --skip-if-exceeding-quota requires quota retrieval." - - throw: "--dry-run and --skip-if-exceeding-quota cannot be used together. --dry-run prevents server interaction, but --skip-if-exceeding-quota requires quota retrieval." - - # Validate that --skip-if-exceeding-quota and --folder-priority-order are not used together - - if: ${cli['skip-if-exceeding-quota'] && cli['folder-priority-order'] != null && cli['folder-priority-order'] != ''} - do: - - log.progress: "ERROR: --skip-if-exceeding-quota and --folder-priority-order cannot be used together. --skip-if-exceeding-quota skips the audit when quota is insufficient, while --folder-priority-order only applies when auditing within quota constraints." - - throw: "--skip-if-exceeding-quota and --folder-priority-order cannot be used together. --skip-if-exceeding-quota skips the audit when quota is insufficient, while --folder-priority-order only applies when auditing within quota constraints." - - - log.progress: "Using Fortify Aviator app mapping: ${cli['aviator-app-mapping']}" - - # Get existing Fortify Aviator applications - - log.progress: Retrieving existing Fortify Aviator applications... - run.fcli: - aviator_apps: - cmd: aviator app ls -o json - records.collect: true - - # Build Aviator app name set for fast membership checks - - var.set: - aviator_app_names: null - - records.for-each: - from: ${aviator_apps.records} - record.var-name: aviator_app - do: - - var.set: - aviator_app_names..: ${aviator_app.name} - - # Query SSC for projects needing audit - - log.progress: Querying SSC for projects with pending audit issues... - - - var.set: - enriched_versions: null - - - rest.call: - projects_needing_audit: - uri: /api/v1/issueaging - type: paged - query: - limit: -1 - filterby: ${cli.filter} - records.for-each: - record.var-name: version - if: ${version.issuesPendingReview > 0 && !version.snapshotOutOfDate} - embed: - project_details: - uri: /api/v1/projectVersions/${version.id} - do: - - var.set: - current_project_name: ${version.project_details.project.name} - current_version_name: ${version.project_details.name} - current_aviator_app_name: ${version.project_details.project.name.replaceAll('"', '')} - - if: ${'version'.equals(cli['aviator-app-mapping'])} - var.set: - current_aviator_app_name: ${(current_project_name + '__' + current_version_name).replaceAll('"', '')} - - var.set: - project_exists_in_aviator: ${aviator_app_names != null && aviator_app_names.contains(current_aviator_app_name)} - - var.set: - enriched_versions..: {fmt: enriched_project} - - - if: ${enriched_versions != null} - log.progress: Found ${enriched_versions.size()} application versions with pending audit issues - - if: ${enriched_versions == null} - log.progress: Found 0 application versions with pending audit issues - - # Apply exclusion filter if specified - - if: ${enriched_versions != null && enriched_versions.size() > 0 && cli['exclude-filter'] != ""} - do: - - log.progress: Applying exclusion filter... - - rest.call: - exclusion_list: - uri: /api/v1/issueaging - type: paged - query: - limit: -1 - filterby: ${cli['exclude-filter']} - records.for-each: - record.var-name: excluded_version - if: ${excluded_version.issuesPendingReview > 0 && !excluded_version.snapshotOutOfDate} - do: - - var.set: - excluded_ids..: ${excluded_version.id} - - - var.set: - filtered_versions: null - excluded_count: 0 - - - records.for-each: - from: ${enriched_versions} - record.var-name: candidate - do: - - var.set: - should_exclude: false - - if: ${excluded_ids != null && excluded_ids.contains(candidate.id)} - var.set: - should_exclude: true - - if: ${should_exclude} - var.set: - excluded_count: ${excluded_count + 1} - - if: ${!should_exclude} - var.set: - filtered_versions..: ${candidate} - - - var.set: - enriched_versions: ${filtered_versions} - - if: ${enriched_versions != null} - log.progress: Excluded ${excluded_count} application versions, ${enriched_versions.size()} remaining - - if: ${enriched_versions == null} - log.progress: Excluded ${excluded_count} application versions, 0 remaining - - # Early exit if no candidates - - if: ${enriched_versions == null || enriched_versions.size() == 0} - do: - - log.info: No application versions found that require audit - - var.set: - audit_candidates: null - - # Apply max-audits limit if we have candidates - - if: ${enriched_versions != null && enriched_versions.size() > 0} - do: - - var.set: - audit_candidates: ${enriched_versions} - - if: ${cli['max-audits'] != -1 && enriched_versions.size() > cli['max-audits']} - do: - - var.set: - audit_counter: 0 - audit_candidates: null - - records.for-each: - from: ${enriched_versions} - record.var-name: candidate - breakIf: ${audit_counter >= cli['max-audits']} - do: - - var.set: - audit_candidates..: ${candidate} - audit_counter: ${audit_counter + 1} - - - if: ${audit_candidates != null} - log.progress: Processing ${audit_candidates.size()} application versions - - if: ${audit_candidates == null} - log.progress: Processing 0 application versions - - # Process audit candidates - - if: ${audit_candidates != null && audit_candidates.size() > 0} - do: - # Initialize execution tracking - - var.set: - stats.create_attempts: 0 - stats.create_successes: 0 - stats.create_failures: 0 - stats.audit_attempts: 0 - stats.audit_failures: 0 - stats.quota_skipped: 0 - stats.entitlement_exhausted: false - stats.create_skipped_due_to_entitlement: 0 - stats.would_create_count: 0 - known_aviator_app_names: ${aviator_app_names} - - # Process each candidate - - records.for-each: - from: ${audit_candidates} - record.var-name: project - do: - - var.set: - app_known_in_aviator: ${known_aviator_app_names != null && known_aviator_app_names.contains(project.aviator_app_name)} - - - if: ${cli['dry-run']} - do: - - if: ${!app_known_in_aviator} - do: - - log.info: Would create app ${project.aviator_app_name} - - var.set: - stats.would_create_count: ${stats.would_create_count + 1} - known_aviator_app_names..: ${project.aviator_app_name} - - if: ${cli['add-aviator-tags']} - do: - - log.info: Would prepare tags for ${project.project_name}:${project.version_name} - - - if: "${cli['folder-priority-order'] != null && cli['folder-priority-order'] != ''}" - do: - - log.info: "Would audit ${project.project_name}:${project.version_name} with custom priority order: ${cli['folder-priority-order']}" - - if: "${cli['folder-priority-order'] == null || cli['folder-priority-order'] == ''}" - do: - - log.info: Would audit ${project.project_name}:${project.version_name} - - if: ${!cli['dry-run']} - do: - - var.set: - app_ready: ${app_known_in_aviator} - skip_this_version: false - - # --- TEST-EXCEEDING-QUOTA MODE --- - # In test mode we only check quota and report; no app creation, no audit. - # For non-existing apps, --default-quota-fallback tells the audit command - # to use the tenant's default quota instead of reporting "app not found". - - if: ${cli['test-exceeding-quota']} - do: - - var.set: - stats.audit_attempts: ${stats.audit_attempts + 1} - quota_flags: " --test-exceeding-quota" - - if: ${!app_known_in_aviator} - var.set: - quota_flags: " --test-exceeding-quota --default-quota-fallback" - - - if: ${cli['tag-mapping'] != null && cli['tag-mapping'] != ''} - run.fcli: - run_audit: - cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}" - status.check: false - records.collect: true - stdout: show - - - if: ${cli['tag-mapping'] == null || cli['tag-mapping'] == ''} - run.fcli: - run_audit: - cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}" - status.check: false - records.collect: true - stdout: show - - - if: ${run_audit.exitCode == 0 && run_audit.records != null && run_audit.records.size() > 0 && run_audit.records[0].__action__ != null && run_audit.records[0].__action__ == 'QUOTA_EXCEEDED'} - var.set: - stats.quota_skipped: ${stats.quota_skipped + 1} - - - if: ${run_audit.exitCode != 0} - do: - - var.set: - stats.audit_failures: ${stats.audit_failures + 1} - - log.warn: Quota test failed for ${project.aviator_app_name}:${project.version_name} - - - var.set: - skip_this_version: true - - # --- SKIP-IF-EXCEEDING-QUOTA + NON-EXISTING APP --- - # Pre-check default quota before creating the app. If the default quota - # would be exceeded, skip the version entirely (don't create, don't audit). - - if: ${!skip_this_version && cli['skip-if-exceeding-quota'] && !app_known_in_aviator} - do: - - log.progress: Pre-checking default quota for new app ${project.aviator_app_name}... - - - if: ${cli['tag-mapping'] != null && cli['tag-mapping'] != ''} - run.fcli: - quota_precheck: - cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\" --test-exceeding-quota --default-quota-fallback${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}" - status.check: false - records.collect: true - stdout: show - - - if: ${cli['tag-mapping'] == null || cli['tag-mapping'] == ''} - run.fcli: - quota_precheck: - cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\" --test-exceeding-quota --default-quota-fallback${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}" - status.check: false - records.collect: true - stdout: show - - # If pre-check shows quota exceeded, skip this version entirely - - if: ${quota_precheck.exitCode == 0 && quota_precheck.records != null && quota_precheck.records.size() > 0 && quota_precheck.records[0].__action__ != null && quota_precheck.records[0].__action__ == 'QUOTA_EXCEEDED'} - do: - - log.progress: Default quota exceeded for ${project.aviator_app_name} - skipping app creation and audit - - var.set: - skip_this_version: true - stats.quota_skipped: ${stats.quota_skipped + 1} - - # --- NORMAL FLOW: create app, prepare tags, run audit --- - - if: ${!skip_this_version} - do: - # Create app if needed - - if: ${!app_known_in_aviator && !stats.entitlement_exhausted} - do: - - var.set: - stats.create_attempts: ${stats.create_attempts + 1} - - run.fcli: - create_app: - cmd: aviator app create "${project.aviator_app_name}" - status.check: false - - if: ${create_app.exitCode == 0} - var.set: - app_ready: true - stats.create_successes: ${stats.create_successes + 1} - known_aviator_app_names..: ${project.aviator_app_name} - - if: ${create_app.exitCode != 0} - do: - - var.set: - create_app_error_text: "${(create_app.stderr == null ? '' : create_app.stderr) + ' ' + (create_app.stdout == null ? '' : create_app.stdout)}" - create_app_already_exists: ${create_app_error_text.toLowerCase().contains('already exists')} - create_app_entitlement_or_quota_error: ${create_app_error_text.toLowerCase().contains('entitlement') || create_app_error_text.toLowerCase().contains('quota')} - - if: ${create_app_already_exists} - var.set: - app_ready: true - known_aviator_app_names..: ${project.aviator_app_name} - - if: ${!create_app_already_exists} - do: - - var.set: - stats.create_failures: ${stats.create_failures + 1} - - if: ${create_app_entitlement_or_quota_error && !stats.entitlement_exhausted} - do: - - log.warn: App creation failed due to entitlement/quota - suppressing further create attempts - - var.set: - stats.entitlement_exhausted: true - - if: ${!create_app_entitlement_or_quota_error} - log.warn: App creation failed for ${project.aviator_app_name}; continuing with remaining candidates - - - if: ${!app_known_in_aviator && stats.entitlement_exhausted} - var.set: - stats.create_skipped_due_to_entitlement: ${stats.create_skipped_due_to_entitlement + 1} - - # Prepare Fortify Remediation Aviator tags if requested - - if: ${cli['add-aviator-tags']} - do: - - log.progress: Preparing Fortify Remediation Aviator tags for ${project.project_name}:${project.version_name} - - run.fcli: - prepare_tags: - cmd: aviator ssc prepare --av "${project.id}" - status.check: false - - if: ${prepare_tags.exitCode != 0} - do: - - log.warn: Fortify Remediation Aviator tag preparation failed for ${project.project_name}:${project.version_name} - - # Run audit if app is ready - - if: ${app_ready} - do: - - var.set: - stats.audit_attempts: ${stats.audit_attempts + 1} - - # Build quota flags string (only --skip-if-exceeding-quota here; - # --test-exceeding-quota is handled in its own block above) - - var.set: - quota_flags: "" - - if: ${cli['skip-if-exceeding-quota']} - var.set: - quota_flags: " --skip-if-exceeding-quota" - - - if: ${cli['tag-mapping'] != null && cli['tag-mapping'] != ''} - run.fcli: - run_audit: - cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --tag-mapping=\"${cli['tag-mapping']}\" --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}" - status.check: false - records.collect: true - stdout: show - - - if: ${cli['tag-mapping'] == null || cli['tag-mapping'] == ''} - run.fcli: - run_audit: - cmd: "aviator ssc audit-sast --av \"${project.id}\" --app \"${project.aviator_app_name}\" --log-level=INFO --refresh=${cli.refresh} --refresh-timeout=\"${cli['refresh-timeout']}\"${quota_flags}${cli['folder-priority-order'] != null && cli['folder-priority-order'] != '' ? ' --folder-priority-order=\"' + cli['folder-priority-order'] + '\"' : ''}" - status.check: false - records.collect: true - stdout: show - - # Track quota-skipped results - - if: ${run_audit.exitCode == 0 && run_audit.records != null && run_audit.records.size() > 0 && run_audit.records[0].__action__ != null && run_audit.records[0].__action__ == 'QUOTA_EXCEEDED'} - var.set: - stats.quota_skipped: ${stats.quota_skipped + 1} - - - if: ${run_audit.exitCode != 0} - do: - - var.set: - stats.audit_failures: ${stats.audit_failures + 1} - - log.warn: Audit failed for ${project.aviator_app_name}:${project.version_name} - - # Summary - - if: ${cli['dry-run']} - do: - - log.info: "Dry-run complete (mapping: ${cli['aviator-app-mapping']}) - would process ${audit_candidates.size()} versions and create ${stats.would_create_count} apps" - - - if: ${!cli['dry-run']} - do: - - if: ${stats.quota_skipped > 0} - log.info: "Complete (mapping: ${cli['aviator-app-mapping']}) - Apps created ${stats.create_successes}/${stats.create_attempts}, Audits attempted ${stats.audit_attempts}, Quota-skipped ${stats.quota_skipped}" - - if: ${stats.quota_skipped == 0} - log.info: "Complete (mapping: ${cli['aviator-app-mapping']}) - Apps created ${stats.create_successes}/${stats.create_attempts}, Audits attempted ${stats.audit_attempts}" - - if: ${stats.entitlement_exhausted} - log.info: Note - Entitlement exhausted, some app creations were skipped - -formatters: - enriched_project: - id: ${version.id} - name: ${version.name} - issuesPendingReview: ${version.issuesPendingReview} - project_name: ${version.project_details.project.name} - version_name: ${version.project_details.name} - aviator_app_name: ${current_aviator_app_name} - exists_in_aviator: ${project_exists_in_aviator} + bulkaudit_sast: + cmd: ssc action run bulkaudit-sast ${#action.copyParametersFromGroup('bulkaudit-sast-options')} + status.check: true diff --git a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAviatorAuditValidationSpec.groovy b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAviatorAuditValidationSpec.groovy index a94ee6fd61..779c7df93c 100644 --- a/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAviatorAuditValidationSpec.groovy +++ b/fcli-other/fcli-functional-test/src/ftest/groovy/com/fortify/cli/ftest/ssc/SSCAviatorAuditValidationSpec.groovy @@ -32,10 +32,11 @@ class SSCAviatorAuditValidationSpec extends FcliBaseSpec { } } - def "ssc bulkaudit action rejects skip-if-exceeding-quota with folder-priority-order"() { + def "ssc #action action rejects skip-if-exceeding-quota with folder-priority-order"() { when: def result = Fcli.run( - "ssc action run bulkaudit --progress=none --on-unsigned=ignore --on-invalid-version=ignore --add-aviator-tags --skip-if-exceeding-quota --folder-priority-order High", + "ssc action run ${action} --progress=none --on-unsigned=ignore --on-invalid-version=ignore " + + "--add-aviator-tags --skip-if-exceeding-quota --folder-priority-order High", { it.expectSuccess(false) }) then: verifyAll(result) { @@ -44,6 +45,8 @@ class SSCAviatorAuditValidationSpec extends FcliBaseSpec { line.contains("--skip-if-exceeding-quota and --folder-priority-order cannot be used together") } } + where: + action << ["bulkaudit-sast", "bulkaudit"] } def "ssc bulkaudit-dast action rejects invalid app mapping"() { From b0edfa92a281f9ad78f5d9ce7ea4b26f52618fca Mon Sep 17 00:00:00 2001 From: kireetivar Date: Thu, 17 Sep 2026 11:17:30 +0530 Subject: [PATCH 3/3] chore: update bulkaudit action to log deprecation warning for bulkaudit-sast usage --- .../resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml index b62cd74b3e..9a02a3e5b4 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/bulkaudit.yaml @@ -114,6 +114,7 @@ cli.options: default: "" steps: + - log.warn: "The 'bulkaudit' action is deprecated; use 'bulkaudit-sast' instead." - run.fcli: bulkaudit_sast: cmd: ssc action run bulkaudit-sast ${#action.copyParametersFromGroup('bulkaudit-sast-options')}