From 576b6200f28ff96f159e9f154562895b7c245259 Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:52:51 +0200 Subject: [PATCH 1/6] chore: `fcli fod release create` enhancements feat: `fcli fod release create`: Add support for creating parent application & microservice if not existing feat: `fcli fod release create`: Throw user-friendly error when trying to copy release from different application feat: `fcli fod release create`: Ignore `--copy-from` if equal to release being created feat: `fcli fod release create`: Ignore `--copy-from` if first release on new application --- .../fod/app/cli/cmd/FoDAppCreateCommand.java | 32 +--- .../fod/app/helper/FoDAppCreateRequest.java | 112 ++++++++++- .../cli/fod/app/helper/FoDAppHelper.java | 11 +- .../helper/FoDMicroserviceHelper.java | 5 + .../cli/cmd/FoDReleaseCreateCommand.java | 180 ++++++++++++++---- ...leaseByQualifiedNameOrIdResolverMixin.java | 8 + .../cli/fod/i18n/FoDMessages.properties | 10 +- 7 files changed, 290 insertions(+), 68 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppCreateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppCreateCommand.java index 0a3247cc86..031ba23882 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppCreateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/cli/cmd/FoDAppCreateCommand.java @@ -15,9 +15,6 @@ import static com.fortify.cli.common.util.DisableTest.TestType.MULTI_OPT_PLURAL_NAME; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import com.fasterxml.jackson.databind.JsonNode; import com.fortify.cli.common.cli.util.EnvSuffix; @@ -27,11 +24,7 @@ import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; -import com.fortify.cli.fod._common.util.FoDEnums; -import com.fortify.cli.fod.access_control.helper.FoDUserGroupHelper; -import com.fortify.cli.fod.access_control.helper.FoDUserHelper; import com.fortify.cli.fod.app.attr.cli.mixin.FoDAttributeUpdateOptions; -import com.fortify.cli.fod.app.attr.helper.FoDAttributeHelper; import com.fortify.cli.fod.app.cli.mixin.FoDAppTypeOptions; import com.fortify.cli.fod.app.cli.mixin.FoDCriticalityTypeOptions; import com.fortify.cli.fod.app.cli.mixin.FoDMicroserviceAndReleaseNameResolverMixin; @@ -85,26 +78,19 @@ public JsonNode getJsonNode(UnirestInstance unirest) { var releaseNameDescriptor = releaseNameResolverMixin.getMicroserviceAndReleaseNameDescriptor(); var microserviceName = releaseNameDescriptor.getMicroserviceName(); validateMicroserviceName(microserviceName); - - var ownerId = FoDUserHelper.getUserDescriptor(unirest, owner, true).getUserId(); - List microservices = StringUtils.isBlank(microserviceName) - ? Collections.emptyList() : new ArrayList<>(Arrays.asList(microserviceName)); FoDAppCreateRequest appCreateRequest = FoDAppCreateRequest.builder() .applicationName(applicationName) .applicationDescription(description) - .businessCriticalityType(criticalityType.getCriticalityType().name()) - .emailList(FoDAppHelper.getEmailList(notifications)) - .releaseName(releaseNameDescriptor.getReleaseName()) + .businessCriticality(criticalityType.getCriticalityType()) + .notify(notifications) + .microserviceAndReleaseNameDescriptor(releaseNameDescriptor) .releaseDescription(releaseDescription) - .sdlcStatusType(String.valueOf(sdlcStatus.getSdlcStatusType())) - .ownerId(ownerId) - .applicationType(appType.getAppType().getFoDValue()) - .hasMicroservices(appType.getAppType().isMicroservice()) - .microservices(FoDAppHelper.getMicroservicesNode(microservices)) - .attributes(FoDAttributeHelper.getAttributesNode(unirest, FoDEnums.AttributeTypes.All, - appAttrs.getAttributes(), autoRequiredAttrs)) - .userGroupIds(FoDUserGroupHelper.getUserGroupsNode(unirest, userGroups)).build(); - + .sdlcStatus(sdlcStatus.getSdlcStatusType()) + .owner(unirest, owner) + .appType(appType.getAppType()) + .autoAttributes(unirest, appAttrs.getAttributes(), autoRequiredAttrs) + .userGroups(unirest, userGroups) + .build().validate(); return FoDAppHelper.createApp(unirest, appCreateRequest).asJsonNode(); } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/helper/FoDAppCreateRequest.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/helper/FoDAppCreateRequest.java index 936093f091..82cfe332d2 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/helper/FoDAppCreateRequest.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/helper/FoDAppCreateRequest.java @@ -12,9 +12,29 @@ *******************************************************************************/ package com.fortify.cli.fod.app.helper; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.formkiq.graalvm.annotations.Reflectable; +import com.fortify.cli.common.json.JsonHelper; +import com.fortify.cli.common.util.StringUtils; +import com.fortify.cli.fod._common.util.FoDEnums; +import com.fortify.cli.fod.access_control.helper.FoDUserGroupHelper; +import com.fortify.cli.fod.access_control.helper.FoDUserHelper; +import com.fortify.cli.fod.app.attr.helper.FoDAttributeHelper; +import com.fortify.cli.fod.app.cli.mixin.FoDAppTypeOptions.FoDAppType; +import com.fortify.cli.fod.app.cli.mixin.FoDCriticalityTypeOptions.FoDCriticalityType; +import com.fortify.cli.fod.app.cli.mixin.FoDSdlcStatusTypeOptions.FoDSdlcStatusType; +import com.fortify.cli.fod.release.helper.FoDQualifiedReleaseNameDescriptor; +import kong.unirest.UnirestInstance; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -35,10 +55,98 @@ public class FoDAppCreateRequest { private String sdlcStatusType; private Integer ownerId; private String applicationType; - private Boolean hasMicroservices; + @Builder.Default private boolean hasMicroservices = false; private JsonNode microservices; private String releaseMicroserviceName; private JsonNode attributes; private JsonNode userGroupIds; - private Boolean autoRequiredAttrs; + + @JsonIgnore + public final ObjectNode asObjectNode() { + ObjectNode body = JsonHelper.getObjectMapper().valueToTree(this); + // if microservice, remove applicationType field + if (isHasMicroservices()) { + body.remove("applicationType"); + } + return body; + } + + @JsonIgnore + public final FoDAppCreateRequest validate(Consumer> validationMessageConsumer) { + var messages = new ArrayList(); + validateRequired(messages, applicationName, "Required application name not specified"); + validateRequired(messages, businessCriticalityType, "Required application business criticality not specified"); + validateRequired(messages, releaseName, "Required release name not specified"); + validateRequired(messages, sdlcStatusType, "Required release SDLC status not specified"); + validateRequired(messages, ownerId, "Required application owner not specified"); + validateRequired(messages, applicationType, "Required application type not specified"); + if ( hasMicroservices ) { + validateRequired(messages, releaseMicroserviceName, "Required release microservice name not specified"); + } + if ( !messages.isEmpty() ) { + validationMessageConsumer.accept(messages); + } + return this; + } + + @JsonIgnore + public final FoDAppCreateRequest validate() { + return validate(messages->{throw new IllegalArgumentException("Unable to create application:\n\t"+String.join("\n\t", messages)); }); + } + + @JsonIgnore + private final void validateRequired(List messages, Object obj, String message) { + if ( obj==null || (obj instanceof String && StringUtils.isBlank((String)obj)) ) { + messages.add(message); + } + } + + public static class FoDAppCreateRequestBuilder { + public FoDAppCreateRequestBuilder appType(FoDAppType appType) { + if ( appType==null ) { return hasMicroservices(false).applicationType(null); } + else { return hasMicroservices(appType.isMicroservice()).applicationType(appType.getFoDValue()); } + } + + public FoDAppCreateRequestBuilder autoAttributes(UnirestInstance unirest, Map attributes, boolean autoRequiredAttrs) { + return attributes(FoDAttributeHelper.getAttributesNode(unirest, FoDEnums.AttributeTypes.All, attributes, autoRequiredAttrs)); + } + + public FoDAppCreateRequestBuilder businessCriticality(FoDCriticalityType businessCriticalityType) { + return businessCriticalityType(businessCriticalityType==null ? null : businessCriticalityType.name()); + } + + public FoDAppCreateRequestBuilder notify(ArrayList notifications) { + return emailList(FoDAppHelper.getEmailList(notifications)); + } + + public FoDAppCreateRequestBuilder microserviceName(String microserviceName) { + List microservices = StringUtils.isBlank(microserviceName) + ? Collections.emptyList() : new ArrayList<>(Arrays.asList(microserviceName)); + microservices(FoDAppHelper.getMicroservicesNode(microservices)); + return releaseMicroserviceName(microserviceName); + } + + public FoDAppCreateRequestBuilder microserviceAndReleaseNameDescriptor(FoDMicroserviceAndReleaseNameDescriptor microserviceAndReleaseNameDescriptor) { + microserviceName(microserviceAndReleaseNameDescriptor==null ? null : microserviceAndReleaseNameDescriptor.getMicroserviceName()); + return releaseName(microserviceAndReleaseNameDescriptor==null ? null : microserviceAndReleaseNameDescriptor.getReleaseName()); + } + + public FoDAppCreateRequestBuilder releaseNameDescriptor(FoDQualifiedReleaseNameDescriptor releaseNameDescriptor) { + applicationName(releaseNameDescriptor==null ? null : releaseNameDescriptor.getAppName()); + microserviceName(releaseNameDescriptor==null ? null : releaseNameDescriptor.getMicroserviceName()); + return releaseName(releaseNameDescriptor==null ? null : releaseNameDescriptor.getReleaseName()); + } + + public FoDAppCreateRequestBuilder owner(UnirestInstance unirest, String owner) { + return ownerId(owner==null ? null : FoDUserHelper.getUserDescriptor(unirest, owner, true).getUserId()); + } + + public FoDAppCreateRequestBuilder sdlcStatus(FoDSdlcStatusType sdlcStatusType) { + return sdlcStatusType(sdlcStatusType==null ? null : sdlcStatusType.name()); + } + + public FoDAppCreateRequestBuilder userGroups(UnirestInstance unirest, ArrayList userGroups) { + return userGroupIds(FoDUserGroupHelper.getUserGroupsNode(unirest, userGroups)); + } + } } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/helper/FoDAppHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/helper/FoDAppHelper.java index 25fdd001b8..58eebd9773 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/helper/FoDAppHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/helper/FoDAppHelper.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.json.JsonHelper; -import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.fod._common.rest.FoDUrls; import com.fortify.cli.fod._common.rest.helper.FoDDataHelper; import com.fortify.cli.fod.app.cli.mixin.FoDAppTypeOptions.FoDAppType; @@ -62,16 +61,8 @@ public static final FoDAppDescriptor getAppDescriptor(UnirestInstance unirest, S } public static final FoDAppDescriptor createApp(UnirestInstance unirest, FoDAppCreateRequest appCreateRequest) { - ObjectNode body = objectMapper.valueToTree(appCreateRequest); - // if microservice, remove applicationType field and set releaseMicroserviceName if not already set - if (appCreateRequest.getHasMicroservices()) { - body.remove("applicationType"); - if (StringUtils.isBlank(appCreateRequest.getReleaseMicroserviceName())) { - body.replace("releaseMicroserviceName", appCreateRequest.getMicroservices().get(0)); - } - } var appId = unirest.post(FoDUrls.APPLICATIONS) - .body(body).asObject(JsonNode.class).getBody().get("applicationId").asText(); + .body(appCreateRequest.asObjectNode()).asObject(JsonNode.class).getBody().get("applicationId").asText(); return getAppDescriptor(unirest, appId, true); } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/helper/FoDMicroserviceHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/helper/FoDMicroserviceHelper.java index 832af07475..b5a685cc11 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/helper/FoDMicroserviceHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/microservice/helper/FoDMicroserviceHelper.java @@ -90,6 +90,11 @@ public static final FoDMicroserviceDescriptor createMicroservice(UnirestInstance .body(body).asObject(JsonNode.class).getBody(); return getDescriptor(appDescriptor, response, msRequest.getMicroserviceName()); } + + public static final FoDMicroserviceDescriptor createMicroservice(UnirestInstance unirest, FoDAppDescriptor appDescriptor, String microserviceName) { + var request = FoDMicroserviceUpdateRequest.builder().microserviceName(microserviceName).build(); + return createMicroservice(unirest, appDescriptor, request); + } public static final FoDMicroserviceDescriptor updateMicroservice(UnirestInstance unirest, FoDMicroserviceDescriptor currentMs, FoDMicroserviceUpdateRequest msRequest) { FoDAppDescriptor appDescriptor = FoDAppHelper.getAppDescriptor(unirest, currentMs.getApplicationId(), true); diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseCreateCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseCreateCommand.java index 74c1c6bc98..9767169f5c 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseCreateCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/cmd/FoDReleaseCreateCommand.java @@ -13,30 +13,52 @@ package com.fortify.cli.fod.release.cli.cmd; +import static com.fortify.cli.common.util.DisableTest.TestType.MULTI_OPT_PLURAL_NAME; + +import java.util.ArrayList; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.common.output.transform.IRecordTransformer; +import com.fortify.cli.common.util.DisableTest; import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; import com.fortify.cli.fod._common.output.cli.cmd.AbstractFoDJsonNodeOutputCommand; import com.fortify.cli.fod._common.util.FoDEnums; import com.fortify.cli.fod.app.attr.cli.mixin.FoDAttributeUpdateOptions; import com.fortify.cli.fod.app.attr.helper.FoDAttributeHelper; +import com.fortify.cli.fod.app.cli.mixin.FoDAppTypeOptions.FoDAppType; +import com.fortify.cli.fod.app.cli.mixin.FoDAppTypeOptions.FoDAppTypeIterable; +import com.fortify.cli.fod.app.cli.mixin.FoDCriticalityTypeOptions.FoDCriticalityType; +import com.fortify.cli.fod.app.cli.mixin.FoDCriticalityTypeOptions.FoDCriticalityTypeIterable; import com.fortify.cli.fod.app.cli.mixin.FoDSdlcStatusTypeOptions; +import com.fortify.cli.fod.app.helper.FoDAppCreateRequest; +import com.fortify.cli.fod.app.helper.FoDAppDescriptor; +import com.fortify.cli.fod.app.helper.FoDAppHelper; +import com.fortify.cli.fod.microservice.helper.FoDMicroserviceDescriptor; +import com.fortify.cli.fod.microservice.helper.FoDMicroserviceHelper; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin; import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameResolverMixin; import com.fortify.cli.fod.release.helper.FoDReleaseCreateRequest; +import com.fortify.cli.fod.release.helper.FoDReleaseCreateRequest.FoDReleaseCreateRequestBuilder; +import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; import com.fortify.cli.fod.release.helper.FoDReleaseHelper; import kong.unirest.UnirestInstance; import lombok.Getter; +import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; @Command(name = OutputHelperMixins.Create.CMD_NAME) public class FoDReleaseCreateCommand extends AbstractFoDJsonNodeOutputCommand implements IRecordTransformer, IActionCommandResultSupplier { + private static final Log LOG = LogFactory.getLog(FoDReleaseCreateCommand.class); @Getter @Mixin private OutputHelperMixins.Create outputHelper; @Mixin private FoDDelimiterMixin delimiterMixin; // Is automatically injected in resolver mixins @Mixin private FoDReleaseByQualifiedNameResolverMixin.PositionalParameter releaseNameResolver; @@ -47,33 +69,82 @@ public class FoDReleaseCreateCommand extends AbstractFoDJsonNodeOutputCommand im @Option(names={"--skip-if-exists"}) private boolean skipIfExists = false; @Option(names={"--auto-required-attrs"}, required = false) - protected boolean autoRequiredAttrs = false; + private boolean autoRequiredAttrs = false; - @Mixin - private FoDSdlcStatusTypeOptions.RequiredOption sdlcStatus; - @Mixin - protected FoDAttributeUpdateOptions.OptionalAttrOption relAttrs; + @Mixin private FoDSdlcStatusTypeOptions.RequiredOption sdlcStatus; + @Mixin private FoDAttributeUpdateOptions.OptionalAttrOption relAttrs; + + @ArgGroup(exclusive = false, headingKey = "fcli.fod.release.create.app-options") + private FoDReleaseAppCreateOptionsArgGroup appCreateOptions = new FoDReleaseAppCreateOptionsArgGroup(); - // TODO Consider splitting method @Override public JsonNode getJsonNode(UnirestInstance unirest) { if (skipIfExists) { var descriptor = releaseNameResolver.getReleaseDescriptor(unirest, false); if (descriptor != null) { - return descriptor.asObjectNode().put(IActionCommandResultSupplier.actionFieldName, "SKIPPED_EXISTING"); + return addActionCommandResult(descriptor.asObjectNode(), false, false, false); } } - // Ensure app exists - var appDescriptor = releaseNameResolver.getAppDescriptor(unirest, true); - // Ensure microservice exists (if specified) - var microserviceDescriptor = releaseNameResolver.getMicroserviceDescriptor(unirest, true); - // Ensure microservice is specified if application has microservices - if ( appDescriptor.isHasMicroservices() && microserviceDescriptor==null ) { - throw new IllegalArgumentException("Microservice name must be specified for microservices application"); + var appDescriptor = releaseNameResolver.getAppDescriptor(unirest, false); + if ( appDescriptor==null ) { // If app doesn't exist yet, create app, microservice & release + return createAppWithRelease(unirest); + } else { // If app exists, create microservice if necessary, then create release + return createReleaseWithOptionalMicroservice(unirest, appDescriptor); + } + } + + public JsonNode transformRecord(JsonNode record) { + return FoDReleaseHelper.renameFields(record); + } + + @Override + public String getActionCommandResult() { + return "CREATED"; + } + + @Override + public boolean isSingular() { + return true; + } + + private final JsonNode createAppWithRelease(UnirestInstance unirest) { + if ( StringUtils.isNotBlank(copyFromReleaseResolver.getQualifiedReleaseNameOrId()) ) { + LOG.warn("WARN: Ignoring --copy-from option as this is the first release on a new application"); + } + var releaseNameDescriptor = releaseNameResolver.getQualifiedReleaseNameDescriptor(); + FoDAppCreateRequest appCreateRequest = FoDAppCreateRequest.builder() + .applicationDescription(appCreateOptions.getAppDescription()) + .businessCriticality(appCreateOptions.getCriticalityType()) + .notify(appCreateOptions.getAppNotifications()) + .releaseNameDescriptor(releaseNameDescriptor) + .releaseDescription(description) + .sdlcStatus(sdlcStatus.getSdlcStatusType()) + .owner(unirest, appCreateOptions.getAppOwner()) + .appType(appCreateOptions.getAppType()) + .autoAttributes(unirest, relAttrs.getAttributes(), autoRequiredAttrs) + .userGroups(unirest, appCreateOptions.getAppUserGroups()) + .build().validate(); + FoDAppHelper.createApp(unirest, appCreateRequest).asJsonNode(); + var rel = FoDReleaseHelper.getReleaseDescriptor(unirest, releaseNameDescriptor.getQualifiedName(), delimiterMixin.getDelimiter(), true); + return addActionCommandResult(rel.asObjectNode(), true, appCreateOptions.getAppType().equals(FoDAppType.Microservice), true); + } + + private final ObjectNode createReleaseWithOptionalMicroservice(UnirestInstance unirest, FoDAppDescriptor appDescriptor) { + boolean msCreated = false; + var microserviceDescriptor = releaseNameResolver.getMicroserviceDescriptor(unirest, false); + if ( microserviceDescriptor==null && appDescriptor.isHasMicroservices() ) { + String microserviceName = releaseNameResolver.getQualifiedReleaseNameDescriptor().getMicroserviceName(); + if ( StringUtils.isBlank(microserviceName) ) { + throw new IllegalArgumentException("Microservice name must be specified for microservices application"); + } + microserviceDescriptor = FoDMicroserviceHelper.createMicroservice(unirest, appDescriptor, releaseNameResolver.getQualifiedReleaseNameDescriptor().getMicroserviceName()); + msCreated = true; } + return createRelease(unirest, appDescriptor, microserviceDescriptor, msCreated); + } + private final ObjectNode createRelease(UnirestInstance unirest, FoDAppDescriptor appDescriptor, FoDMicroserviceDescriptor microserviceDescriptor, boolean msCreated) { String simpleReleaseName = releaseNameResolver.getSimpleReleaseName(); - String copyReleaseId = copyFromReleaseResolver.getReleaseId(unirest); var requestBuilder = FoDReleaseCreateRequest.builder() .applicationId(Integer.valueOf(appDescriptor.getApplicationId())) @@ -82,29 +153,74 @@ public JsonNode getJsonNode(UnirestInstance unirest) { .sdlcStatusType(sdlcStatus.getSdlcStatusType().name()) .attributes(FoDAttributeHelper.getAttributesNode(unirest, FoDEnums.AttributeTypes.Release, relAttrs.getAttributes(), autoRequiredAttrs)); - if ( microserviceDescriptor!=null ) { - requestBuilder = requestBuilder.microserviceId(Integer.valueOf(microserviceDescriptor.getMicroserviceId())); - } - if ( StringUtils.isNotBlank(copyReleaseId) ) { + requestBuilder = addMicroservice(microserviceDescriptor, requestBuilder); + requestBuilder = addCopyFrom(unirest, appDescriptor, requestBuilder); + + var rel = FoDReleaseHelper.createRelease(unirest, requestBuilder.build()).asObjectNode(); + return addActionCommandResult(rel, false, msCreated, true); + } + + private FoDReleaseCreateRequestBuilder addCopyFrom(UnirestInstance unirest, FoDAppDescriptor appDescriptor, FoDReleaseCreateRequestBuilder requestBuilder) { + var copyFromReleaseDescriptor = getCopyFromReleaseDescriptor(unirest, appDescriptor); + if ( copyFromReleaseDescriptor!=null ) { requestBuilder = requestBuilder .copyState(true) - .copyStateReleaseId(Integer.parseInt(copyReleaseId)); + .copyStateReleaseId(Integer.parseInt(copyFromReleaseDescriptor.getReleaseId())); } - - return FoDReleaseHelper.createRelease(unirest, requestBuilder.build()).asJsonNode(); + return requestBuilder; } - - public JsonNode transformRecord(JsonNode record) { - return FoDReleaseHelper.renameFields(record); + + private final FoDReleaseDescriptor getCopyFromReleaseDescriptor(UnirestInstance unirest, FoDAppDescriptor appDescriptor) { + var copyFromReleaseNameDescriptor = copyFromReleaseResolver.getQualifiedReleaseNameDescriptor(); + if ( copyFromReleaseNameDescriptor!=null ) { + if ( copyFromReleaseNameDescriptor.getQualifiedName().equals(releaseNameResolver.getQualifiedReleaseName()) ) { + // Ignore --copy-from if pointing to same release as the release to be created + LOG.warn("WARN: Ignoring --copy-from option as it's the same as the release being created"); + return null; + } + } + var copyFromReleaseDescriptor = copyFromReleaseResolver.getReleaseDescriptor(unirest); + if ( copyFromReleaseDescriptor!=null && !copyFromReleaseDescriptor.getApplicationId().equals(appDescriptor.getApplicationId()) ) { + throw new IllegalArgumentException("Copy release from different application is not allowed"); + } + return copyFromReleaseDescriptor; } - @Override - public String getActionCommandResult() { - return "CREATED"; + private FoDReleaseCreateRequestBuilder addMicroservice(FoDMicroserviceDescriptor microserviceDescriptor, + FoDReleaseCreateRequestBuilder requestBuilder) { + if ( microserviceDescriptor!=null ) { + requestBuilder = requestBuilder.microserviceId(Integer.valueOf(microserviceDescriptor.getMicroserviceId())); + } + return requestBuilder; } - - @Override - public boolean isSingular() { - return true; + + private final ObjectNode addActionCommandResult(ObjectNode rel, boolean appCreated, boolean msCreated, boolean relCreated) { + var result = new ArrayList(); + addActionCommandResult(result, appCreated, "APP_CREATED"); + addActionCommandResult(result, msCreated, "MICROSERVICE_CREATED"); + addActionCommandResult(result, relCreated, "RELEASE_CREATED"); + if ( result.isEmpty() ) { result.add("SKIPPED_EXISTING"); } + return rel.put(IActionCommandResultSupplier.actionFieldName, String.join("\n", result)); + } + + private final void addActionCommandResult(ArrayList result, boolean add, String value) { + if ( add ) { result.add(value); } + } + + @Getter + public static final class FoDReleaseAppCreateOptionsArgGroup { + @Option(names = {"--app-description"}, required = false) + protected String appDescription; + @DisableTest(MULTI_OPT_PLURAL_NAME) + @Option(names = {"--app-notify"}, required = false, split=",") + protected ArrayList appNotifications; + @Option(names = {"--app-owner"}, required = false) + protected String appOwner; + @Option(names = {"--app-groups"}, required = false, split=",") + protected ArrayList appUserGroups; + @Option(names = {"--app-type"}, required = false, completionCandidates = FoDAppTypeIterable.class) + private FoDAppType appType; + @Option(names = {"--app-criticality"}, required = false, completionCandidates = FoDCriticalityTypeIterable.class) + private FoDCriticalityType criticalityType; } } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/mixin/FoDReleaseByQualifiedNameOrIdResolverMixin.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/mixin/FoDReleaseByQualifiedNameOrIdResolverMixin.java index b3305d06a8..6f7ea3de5b 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/mixin/FoDReleaseByQualifiedNameOrIdResolverMixin.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/release/cli/mixin/FoDReleaseByQualifiedNameOrIdResolverMixin.java @@ -22,6 +22,7 @@ import com.fortify.cli.common.util.StringUtils; import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin; import com.fortify.cli.fod._common.cli.mixin.IFoDDelimiterMixinAware; +import com.fortify.cli.fod.release.helper.FoDQualifiedReleaseNameDescriptor; import com.fortify.cli.fod.release.helper.FoDReleaseDescriptor; import com.fortify.cli.fod.release.helper.FoDReleaseHelper; @@ -35,6 +36,13 @@ public class FoDReleaseByQualifiedNameOrIdResolverMixin { public static abstract class AbstractFoDQualifiedReleaseNameOrIdResolverMixin implements IFoDDelimiterMixinAware { @Setter private FoDDelimiterMixin delimiterMixin; public abstract String getQualifiedReleaseNameOrId(); + + public final FoDQualifiedReleaseNameDescriptor getQualifiedReleaseNameDescriptor() { + var qualifiedReleaseNameOrId = getQualifiedReleaseNameOrId(); + var delimiter = delimiterMixin.getDelimiter(); + if (qualifiedReleaseNameOrId == null || !qualifiedReleaseNameOrId.contains(delimiter)) { return null; } + return FoDQualifiedReleaseNameDescriptor.fromQualifiedReleaseName(qualifiedReleaseNameOrId, delimiter); + } public FoDReleaseDescriptor getReleaseDescriptor(UnirestInstance unirest, String... fields) { var qualifiedReleaseNameOrId = getQualifiedReleaseNameOrId(); diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index 12a81a0332..902a632944 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -368,13 +368,21 @@ fcli.fod.release.create.copy-from = The id or name of a release to copy existing FoD will put the release in 'suspended' state until copying is complete, during which time scan \ requests and other operations may be rejected. If you want to run any other operations on the release, \ it is recommended to first invoke the `fcli fod release wait-for` command to wait until the release \ - leaves suspended state. + leaves suspended state. This option is ignored if the release name to be copied is equal to the release \ + name to be created, or when creating the first release on a (new) application. fcli.fod.release.create.microservice = The id or name of the microservice to create the release on. fcli.fod.release.create.status = SDLC lifecycle status of the release. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.release.create.attr = Attribute id or name and its value to set on the release. \ Please note for user attributes only the userId is currently supported. fcli.fod.release.create.skip-if-exists = Check to see if release already exists before creating. fcli.fod.release.create.auto-required-attrs = Automatically set a default value for required attributes. +fcli.fod.release.create.app-options = Application creation options (ignored if application exists)%n +fcli.fod.release.create.app-owner = Required application owner when creating a new application. +fcli.fod.release.create.app-criticality = Required application business criticality when creating a new application. Valid values: ${COMPLETION-CANDIDATES}. +fcli.fod.release.create.app-type = Required application type when creating a new application. Valid values: ${COMPLETION-CANDIDATES}. +fcli.fod.release.create.app-description = Optional application description when creating a new application. +fcli.fod.release.create.app-notify = Optional email address(es) when creating a new application to specify application user(s) to send notifications to. +fcli.fod.release.create.app-groups = Optional user group ids or names to give access to the application when creating a new application. fcli.fod.release.delete.usage.header = Delete an application release. fcli.fod.release.list.usage.header = List application releases. fcli.fod.release.get.usage.header = Get application release details. From 9e3a8fd4d1e49195eee9468ab01d52c93c0688f0 Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:02:43 +0100 Subject: [PATCH 2/6] feat: `fcli fod action run setup-release`: Add support for creating parent application & microservice if not existing --- .../cli/fod/actions/zip/setup-release.yaml | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml index af52e9c260..4d434e8538 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml @@ -52,8 +52,34 @@ parameters: required: true name: sdlc-status cliAliases: status - description: "See `fcli fod release create`" + description: "See `fcli fod release create`. Default value: Development" defaultValue: Development + - group: rel_create_opts + required: false + name: app-description + description: "See `fcli fod release create`" + - group: rel_create_opts + required: false + name: app-notify + description: "See `fcli fod release create`" + - group: rel_create_opts + required: false + name: app-owner + description: "See `fcli fod release create`" + - group: rel_create_opts + required: false + name: app-groups + description: "See `fcli fod release create`" + - group: rel_create_opts + required: false + name: app-type + description: "See `fcli fod release create`. Default value: Web" + defaultValue: Web + - group: rel_create_opts + required: false + name: app-criticality + description: "See `fcli fod release create`. Default value: Medium" + defaultValue: Medium - group: sast_setup_opts name: assessment-type required: false @@ -95,7 +121,7 @@ steps: - write: - to: stdout value: | - Create application release ${parameters.release} (id ${createRelease[0].releaseId}): ${createRelease[0].__action__} + Create application release ${parameters.release} (id ${createRelease[0].releaseId}): ${createRelease[0].__action__.replaceAll('\n', ', ')} - progress: "Waiting for release to leave suspended state" - fcli: - args: fod release wait-for "${parameters.release}" --progress=none From 293744329020b4f9828f6d695e95ba739a9ac98e Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:12:39 +0100 Subject: [PATCH 3/6] chore: Update usage help --- .../fortify/cli/fod/i18n/FoDMessages.properties | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index 902a632944..e2dc712093 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -358,9 +358,7 @@ fcli.fod.release.output.header.applicationName = Application fcli.fod.release.output.header.releaseDescription = Description fcli.fod.release.output.header.sdlcStatusType = SDLC Status -fcli.fod.release.create.usage.header = Create a new application release. \ - Please note some attributes might be mandatory depending on the configuration of your tenant. Please check the \ - Fortify on Demand web portal first. +fcli.fod.release.create.usage.header = Create a new application release. fcli.fod.release.create.application-name = The id or name of the application to create the release on. fcli.fod.release.create.release-name = The name of the release to create for the application. fcli.fod.release.create.description = Description of the release. @@ -372,10 +370,15 @@ fcli.fod.release.create.copy-from = The id or name of a release to copy existing name to be created, or when creating the first release on a (new) application. fcli.fod.release.create.microservice = The id or name of the microservice to create the release on. fcli.fod.release.create.status = SDLC lifecycle status of the release. Valid values: ${COMPLETION-CANDIDATES}. -fcli.fod.release.create.attr = Attribute id or name and its value to set on the release. \ - Please note for user attributes only the userId is currently supported. +fcli.fod.release.create.attr = Set of application & release attribute id's or names and corresponding values. \ + Please note for user attributes only the userId is currently supported. Depending on FoD configuration, \ + some attributes may be required and either need to be set explicitly or auto-filled through the \ + --auto-required-attrs option. Application attributes will be applied when creating a new application and \ + ignored if the application already exists. Similarly, release attributes will be ignored if the release \ + already exists and --skip-if-exists is enabled. fcli.fod.release.create.skip-if-exists = Check to see if release already exists before creating. -fcli.fod.release.create.auto-required-attrs = Automatically set a default value for required attributes. +fcli.fod.release.create.auto-required-attrs = Automatically set a default value for required application and \ + release attributes. fcli.fod.release.create.app-options = Application creation options (ignored if application exists)%n fcli.fod.release.create.app-owner = Required application owner when creating a new application. fcli.fod.release.create.app-criticality = Required application business criticality when creating a new application. Valid values: ${COMPLETION-CANDIDATES}. From d3b6f4c260580c6db7f0edcc175dc354db72eae1 Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:10:41 +0100 Subject: [PATCH 4/6] fix: Improve parsing of boolean action parameters --- .../cli/common/action/runner/ActionParameterHelper.java | 1 + .../fortify/cli/common/cli/util/SimpleOptionsParser.java | 9 +++++++-- .../com/fortify/cli/fod/actions/zip/setup-release.yaml | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java index e9ea65a9d6..818e468193 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/action/runner/ActionParameterHelper.java @@ -39,6 +39,7 @@ private static final void addOptionDescriptor(List result, Ac .name(getOptionName(parameter.getName())) .aliases(getOptionAliases(parameter.getCliAliasesArray())) .description(parameter.getDescription()) + .bool(parameter.getType()!=null && parameter.getType().equals("boolean")) .build()); } diff --git a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/SimpleOptionsParser.java b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/SimpleOptionsParser.java index c0ddbaab2d..67b34d8480 100644 --- a/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/SimpleOptionsParser.java +++ b/fcli-core/fcli-common/src/main/java/com/fortify/cli/common/cli/util/SimpleOptionsParser.java @@ -45,6 +45,7 @@ public static interface IOptionDescriptor { String getName(); List getAliases(); String getDescription(); + boolean isBool(); default String getOptionNamesAndAliasesString(String delimiter) { var name = getName(); @@ -59,6 +60,7 @@ public static class OptionDescriptor implements IOptionDescriptor { private final String name; private final List aliases; private final String description; + private final boolean bool; } @Data @@ -107,13 +109,16 @@ private Map parseArgs(List validationErrors, String[] ar var argWithPossibleValue = argsDeque.pop(); var argElts = argWithPossibleValue.split("=", 2); var arg = argElts[0]; - if ( !optionsByNameAndAliases.containsKey(arg) ) { + var optionDescriptor = optionsByNameAndAliases.get(arg); + if ( optionDescriptor==null ) { validationErrors.add("Unknown command line option: "+arg); } else if ( argElts.length==2 ) { result.put(arg, argElts[1]); } else { var nextArg = argsDeque.peek(); - var value = optionsByNameAndAliases.containsKey(nextArg) ? null : argsDeque.pop(); + var value = nextArg==null || optionsByNameAndAliases.containsKey(nextArg) + ? (optionDescriptor.isBool() ? "true" : null) + : argsDeque.pop(); result.put(arg, value); } } diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml index af52e9c260..c39342f76d 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml @@ -62,11 +62,13 @@ parameters: - group: sast_setup_opts required: false name: use-aviator - description: "See `fcli fod sast-scan setup`" + description: "See `fcli fod sast-scan setup`" + type: boolean - group: sast_setup_opts required: false name: oss description: "See `fcli fod sast-scan setup`" + type: boolean - name: attributes required: false cliAliases: attrs From cb5c9f2fe6f4715abdf0ae3872274e7e4227d0c2 Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:18:03 +0100 Subject: [PATCH 5/6] chore: Improve & simplify setup actions --- .../cli/fod/actions/zip/setup-release.yaml | 87 +++++++------------ .../cli/ssc/actions/zip/setup-appversion.yaml | 53 +++++------ 2 files changed, 53 insertions(+), 87 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml index 61dbfb81df..7e7e5393f3 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/actions/zip/setup-release.yaml @@ -2,24 +2,30 @@ author: Fortify usage: - header: (PREVIEW) Set up application release. + header: Set up application release. description: | - his action is primarily meant for use in CI/CD integrations, allowing users to - rovide a custom action with a customized application release setup process if - ecessary. - - For example, such a custom action could define standard profiles (based on team, - business unit, application type/risk, ...) with predefined users, attributes or - issue template to be set on newly created application releases. Of course, instead - of having a single custom action that defines profiles, you could also provide - multiple custom actions that users can select from, or you can use a combination; - each business unit or team providing their own custom action, with each of these - custom actions defining profiles for different application types/risk. - - This built-in action only provides a 'default' - profile that simply invokes the `fcli fod release create` and optionally - `fcli fod sast-scan setup` commands with some default options, amended with - options passed to this action. + This action allows for preparing an application release for running an application + security scan. It will create the application and/or release if they do not exist + yet, and optionally configure scan settings. For now, only static scan setup is + supported, including optional software composition analysis. Support for other + scan types like Dynamic or Mobile may be added in the future, or you may consider + implementing a custom setup action to set up other scan types. + + Although the same functionality can be achieved by manually running the various + fcli commands used by this action, like `fcli fod release create` and + `fcli fod sast-scan setup`, this action provides a convenient and standardized + approach for running those commands, providing default values for many of the + required options. + + To provide even more consistency across CI/CD pipelines in your organization, it + is recommended to implement one or more custom setup actions that provide suitable + default values or even hard-coded, non-overridable values for the various options, + for example based on business unit, team, and/or application type. Such custom + actions could for example set standard application or release attributes for a + particular type of application to be scanned. Alternative to implementing multiple + custom actions, you may also consider implementing a single custom action that takes + for example a --profile option to select between different profiles that each define + appropriate option values and setup commands to run. defaults: requestTarget: fod @@ -29,16 +35,16 @@ parameters: cliAliases: rel required: true description: "Required release name as [:]:" - - name: profile - cliAliases: p - required: true - defaultValue: default - description: "This built-in action only supports the 'default' profile, which is selected by default" - name: scan-types cliAliases: t required: false type: array description: "Optional comma-separated list of scan type(s) to set up; for now, only 'sast' is supported" + - group: rel_create_opts + name: attributes + required: false + cliAliases: attrs + description: "Optional comma-separated list of attributes to set on the application and/or release" - group: rel_create_opts required: false name: copy-from @@ -94,32 +100,13 @@ parameters: required: false name: oss description: "See `fcli fod sast-scan setup`" - type: boolean - - name: attributes - required: false - cliAliases: attrs - description: "Optional comma-separated list of attributes to set on the application and/or release" + type: boolean steps: - - progress: "Creating FoD application release if non-existing (profile: ${parameters.profile})" - - if: ${parameters['attributes']!=null} - set: - - name: customAttrArgs - value: --attrs "${parameters['attributes']}" - - if: ${parameters['attributes']==null} - set: - - name: customAttrArgs - value: --auto-required-attrs - - if: ${parameters.profile=="default"} - set: - - name: relCreateArgs - value: --skip-if-exists ${#action.copyParametersFromGroup("rel_create_opts")} ${customAttrArgs} - # Custom actions can replace/repeat the above to define custom profiles. - - if: ${relCreateArgs==null} - throw: "Invalid profile: ${parameters.profile}" + - progress: "Creating FoD application & release if non-existing" - fcli: - name: createRelease - args: fod release create "${parameters.release}" ${relCreateArgs} + args: fod release create "${parameters.release}" --skip-if-exists --auto-required-attrs ${#action.copyParametersFromGroup("rel_create_opts")} - write: - to: stdout value: | @@ -128,21 +115,13 @@ steps: - fcli: - args: fod release wait-for "${parameters.release}" --progress=none - if: ${parameters["scan-types"].contains("sast")} - steps: - - if: ${parameters.profile=="default"} - set: - - name: sastSetupArgs - value: --skip-if-exists --frequency "Subscription" --audit-preference Automated ${#action.copyParametersFromGroup("sast_setup_opts")} + steps: - progress: "Configuring FoD application release ${parameters.release} for SAST scanning" - fcli: - name: setupSast - args: fod sast-scan setup --rel "${parameters.release}" ${sastSetupArgs} + args: fod sast-scan setup --rel "${parameters.release}" --skip-if-exists --frequency "Subscription" --audit-preference Automated ${#action.copyParametersFromGroup("sast_setup_opts")} - write: - to: stdout value: | SAST scan setup status: ${setupSast[0].__action__} - - if: ${parameters["scan-types"].contains("dast")} - steps: - - if: ${parameters.profile=="default"} - # Custom actions can replace the above to define DAST setup. diff --git a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/setup-appversion.yaml b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/setup-appversion.yaml index 37cd3493a6..5214e38740 100644 --- a/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/setup-appversion.yaml +++ b/fcli-core/fcli-ssc/src/main/resources/com/fortify/cli/ssc/actions/zip/setup-appversion.yaml @@ -2,26 +2,25 @@ author: Fortify usage: - header: (PREVIEW) Set up application version. + header: Set up application version. description: | - This action is primarily meant for use in CI/CD integrations, allowing users to - provide a custom action with a customized application version setup process if - necessary. - - For example, such a custom action could define standard profiles (based on team, - business unit, application type/risk, ...) with predefined users, attributes or - issue template to be set on newly created application versions. Of course, instead - of having a single custom action that defines profiles, you could also provide - multiple custom actions that users can select from, or you can use a combination; - each business unit or team providing their own custom action, with each of these - custom actions defining profiles for different application types/risk. - - This built-in action only provides a 'default' profile that simply invokes the - `fcli ssc appversion create` command, passing the following options by default: - `--skip-if-exists`, `--auto-required-attrs`, `--refresh`, `--refresh-timeout 300s`. - Additional creation options can be passed through the various action options, which - includes the ability to override the default refresh timeout (only applicable when - copying an existing application version). + This action allows for preparing an application version for running an application + security scan, creating the application and/or release if they do not exist yet. + + Although the same functionality can be achieved by manually running the + `fcli ssc appversion create` command, this action provides a convenient and + standardized approach for running this command with some default options like + `--skip-if-exists` and `--auto-required-attrs`. + + To provide even more consistency across CI/CD pipelines in your organization, it + is recommended to implement one or more custom setup actions that provide suitable + default values or even hard-coded, non-overridable values for the various options, + for example based on business unit, team, and/or application type. Such custom + actions could for example set standard application version attributes for a + particular type of application to be scanned. Alternative to implementing multiple + custom actions, you may also consider implementing a single custom action that takes + for example a --profile option to select between different profiles that each define + appropriate option values and setup commands to run. defaults: requestTarget: ssc @@ -31,11 +30,6 @@ parameters: cliAliases: av required: true description: "Required application version name as :" - - name: profile - cliAliases: p - required: true - defaultValue: default - description: "This built-in action only supports the 'default' profile, which is selected by default" - group: av_create_opts required: false name: add-users @@ -70,17 +64,10 @@ parameters: description: "See 'fcli ssc av create'. Default value: 300s" steps: - - progress: "Creating SSC application version if non-existing (profile: ${parameters.profile})" - - if: ${parameters.profile=="default"} - set: - - name: avCreateArgs - value: --skip-if-exists --auto-required-attrs --refresh ${#action.copyParametersFromGroup("av_create_opts")} - # Custom actions can replace/repeat the above to define custom profiles. - - if: ${avCreateArgs==null} - throw: "Invalid profile: ${parameters.profile}" + - progress: "Creating SSC application version if non-existing" - fcli: - name: createAppVersion - args: ssc av create ${parameters.appversion} ${avCreateArgs} + args: ssc av create ${parameters.appversion} --skip-if-exists --auto-required-attrs --refresh ${#action.copyParametersFromGroup("av_create_opts")} - write: - to: stdout value: | From d81d923ac9f43e12954aa92a8cdf7cb09f36ee1e Mon Sep 17 00:00:00 2001 From: Ruud Senden <8635138+rsenden@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:10:25 +0100 Subject: [PATCH 6/6] chore: Usage help updates --- .../attr/cli/mixin/FoDAttributeUpdateOptions.java | 6 +++--- .../fortify/cli/fod/i18n/FoDMessages.properties | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/attr/cli/mixin/FoDAttributeUpdateOptions.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/attr/cli/mixin/FoDAttributeUpdateOptions.java index 77f8b60e20..4a712b5c96 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/attr/cli/mixin/FoDAttributeUpdateOptions.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/app/attr/cli/mixin/FoDAttributeUpdateOptions.java @@ -28,17 +28,17 @@ public static abstract class AbstractFoDAppAttributeUpdateMixin { } public static class OptionalAttrCreateOption extends AbstractFoDAppAttributeUpdateMixin { - @Option(names = {"--attrs", "--attributes"}, required = false, split=",", paramLabel = PARAM_LABEL, descriptionKey = "fcli.fod.app.create.attr") + @Option(names = {"--attrs", "--attributes"}, required = false, split=",", paramLabel = PARAM_LABEL) @Getter private Map attributes; } public static class OptionalAttrOption extends AbstractFoDAppAttributeUpdateMixin { - @Option(names = {"--attrs", "--attributes"}, required = false, split=",", paramLabel = PARAM_LABEL, descriptionKey = "fcli.fod.app.update.attr") + @Option(names = {"--attrs", "--attributes"}, required = false, split=",", paramLabel = PARAM_LABEL) @Getter private Map attributes; } public static class RequiredPositionalParameter extends AbstractFoDAppAttributeUpdateMixin { - @EnvSuffix("ATTRS") @Parameters(index = "0..*", arity = "1..*", paramLabel = PARAM_LABEL, descriptionKey = "fcli.fod.app.update.attr") + @EnvSuffix("ATTRS") @Parameters(index = "0..*", arity = "1..*", paramLabel = PARAM_LABEL) @Getter private Map attributes; } diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index e2dc712093..1a4a0078de 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -300,7 +300,7 @@ fcli.fod.app.create.owner = The owner of the application/release who will receiv fcli.fod.app.create.groups = User group ids or names to give access of the application to. fcli.fod.app.create.criticality = The business criticality of the application. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.app.create.status = The SDLC lifecycle status of the release. Valid values: ${COMPLETION-CANDIDATES} -fcli.fod.app.create.attr = Attribute id or name and its value to set. You can specify application and release attributes. \ +fcli.fod.app.create.attrs = Set of application and release attribute id's or names and their values to set. \ Please note for user attributes only the userId is currently supported. fcli.fod.app.create.skip-if-exists = Check to see if application already exists before creating. fcli.fod.app.create.auto-required-attrs = Automatically set a default value for required attributes. \ @@ -324,8 +324,9 @@ fcli.fod.app.update.name = The updated name for the application. fcli.fod.app.update.description = The updated description for the application. fcli.fod.app.update.notify = Email address of user(s) to send notifications to (Please note, any existing entries will be replaced). fcli.fod.app.update.criticality = The business criticality of the application. -fcli.fod.app.update.attr = Attribute id or name and its value to set. \ - Please note for user attributes only the userId is currently supported. +fcli.fod.app.update.attrs = Set of application attribute id's or names and their values to set. \ + Please note for user attributes only the userId is currently supported. Release attributes \ + may be provided but will be ignored. fcli.fod.app.list-scans.usage.header = List scans for a given application. # fcli fod microservice @@ -370,7 +371,7 @@ fcli.fod.release.create.copy-from = The id or name of a release to copy existing name to be created, or when creating the first release on a (new) application. fcli.fod.release.create.microservice = The id or name of the microservice to create the release on. fcli.fod.release.create.status = SDLC lifecycle status of the release. Valid values: ${COMPLETION-CANDIDATES}. -fcli.fod.release.create.attr = Set of application & release attribute id's or names and corresponding values. \ +fcli.fod.release.create.attrs = Set of application & release attribute id's or names and corresponding values. \ Please note for user attributes only the userId is currently supported. Depending on FoD configuration, \ some attributes may be required and either need to be set explicitly or auto-filled through the \ --auto-required-attrs option. Application attributes will be applied when creating a new application and \ @@ -398,8 +399,9 @@ fcli.fod.release.update.description = Updated description for the release. fcli.fod.release.update.owner = Updated id or username for the owner of the release. fcli.fod.release.update.microservice = The updated microservice id or name to create the release on. fcli.fod.release.update.status = SDLC lifecycle status of the release. Valid values: ${COMPLETION-CANDIDATES}. -fcli.fod.release.update.attr = Attribute id or name and its value to set on the release. \ - Please note for user attributes only the userId is currently supported. +fcli.fod.release.update.attrs = Set of release attribute id's or names and their values to set on the release. \ + Please note for user attributes only the userId is currently supported. Application attributes may be provided \ + but will be ignored. fcli.fod.release.list-assessment-types.usage.header = List assessment types for a given release. fcli.fod.release.list-assessment-types.scan-types = Comma-separated list of scan types for which to list assessment types. Default value: ${DEFAULT-VALUE}. Valid values: ${COMPLETION-CANDIDATES}. fcli.fod.release.list-scans.usage.header = List scans for a given release.