diff --git a/fcli-core/fcli-common-action/src/main/java/com/fortify/cli/common/action/runner/ActionSpelFunctions.java b/fcli-core/fcli-common-action/src/main/java/com/fortify/cli/common/action/runner/ActionSpelFunctions.java index 1d772109965..e1f29a3a339 100644 --- a/fcli-core/fcli-common-action/src/main/java/com/fortify/cli/common/action/runner/ActionSpelFunctions.java +++ b/fcli-core/fcli-common-action/src/main/java/com/fortify/cli/common/action/runner/ActionSpelFunctions.java @@ -15,6 +15,7 @@ import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.date; import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.fcli; import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.fortify; +import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.http; import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.internal; import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.txt; import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.util; @@ -87,7 +88,7 @@ public static final String resolveAgainstCurrentWorkDir( { return Path.of(".").resolve(path).toAbsolutePath().normalize().toString(); } - + @SpelFunction(cat=workflow, desc = "Throws an error with the given message if the first argument evaluates to true.", returns="`true` if no error is thrown") public static final boolean check( @@ -100,9 +101,9 @@ public static final boolean check( return true; } } - + @SpelFunction(cat=txt, desc = "Repeats the input text a specified number of times.", - returns= "The input text repeated the given number of times") + returns= "The input text repeated the given number of times") public static final String repeat( @SpelFunctionParam(name="input", desc="the text to repeat.") String text, @SpelFunctionParam(name="count", desc="the number of times to repeat the text; if <=0, an empty string will be returned") int count) @@ -112,7 +113,7 @@ public static final String repeat( for (int i = 0; i < count; i++) { sb.append(text);} return sb.toString(); } - + @SpelFunction(cat=txt, desc = "Converts the given HTML string into plain text.", returns="The plain text extracted from the input HTML, or `null` if the input is `null`") public static final String htmlToText( @@ -122,16 +123,16 @@ public static final String htmlToText( Document document = ActionSpelFunctionsJsoupHelper.asDocument(html); return ActionSpelFunctionsJsoupHelper.documentToPlainText(document); } - + @SpelFunction(cat=txt, desc = "Converts the given HTML string into a single-line plain text string by removing all HTML tags.", returns="The plain text representation of the given HTML input, or `null` if the input is `null`") public static final String htmlToSingleLineText( - @SpelFunctionParam(name="html", desc="the HTML string to convert to single-line plain text") String html) + @SpelFunctionParam(name="html", desc="the HTML string to convert to single-line plain text") String html) { if (html == null) { return null; } return Jsoup.clean(html, "", Safelist.none()); } - + @SpelFunction(cat=fortify, desc = "Cleans the given rule description and returns it as plain text.", returns="The cleaned rule description, or empty string if input is `null`") public static final String cleanRuleDescription( @@ -142,7 +143,7 @@ public static final String cleanRuleDescription( var paragraphs = document.select("Paragraph"); for (var p : paragraphs) { var altParagraph = p.select("AltParagraph"); - if (!altParagraph.isEmpty()) { p.replaceWith(new TextNode(String.join("\n\n", altParagraph.eachText())));} + if (!altParagraph.isEmpty()) { p.replaceWith(new TextNode(String.join("\n\n", altParagraph.eachText())));} else { p.remove(); } } document.select("IfDef").remove(); @@ -161,17 +162,17 @@ public static final String cleanIssueDescription( return ActionSpelFunctionsJsoupHelper.documentToPlainText(document); } - @SpelFunction(cat=util, desc = "Retrieves a given part of the given URI", - returns="Requested part of the given URI, or `null` if part name is not valid or not present") + @SpelFunction(cat=http, desc = "Retrieves a given part of the given URI", + returns="Requested part of the given URI, or `null` if part name is not valid or not present") public static final String uriPart( - @SpelFunctionParam(name="uri", desc="URI from which to retrieve the requested part") String uriString, + @SpelFunctionParam(name="uri", desc="URI from which to retrieve the requested part") String uriString, @SpelFunctionParam(name="part", desc="URI part to be returned; may be one of serverUrl, protocol, host, port, path, relativePath, query, fragment") String part) { if ( StringUtils.isBlank(uriString) ) {return null;} Matcher matcher = uriPartsPattern.matcher(uriString); return matcher.matches() ? matcher.group(part) : null; } - + @SpelFunction(cat=date, desc = """ Returns either current or given date/time formatted according to the given formatter pattern. See 'Patterns for Formatting and Parsing' section at @@ -189,7 +190,7 @@ public static final String formatDateTime( var dateString = dateStrings == null || dateStrings.length == 0 ? currentDateTime() : dateStrings[0]; return formatDateTimeWithZoneId(pattern, dateString, ZoneId.systemDefault()); } - + @SpelFunction(cat=date, desc = """ Returns given date/time formatted according to the given formatter pattern. See 'Patterns for Formatting and Parsing' section at @@ -206,9 +207,9 @@ public static final String formatDateTimeWithZoneId( ZonedDateTime zonedDateTime = new JSONDateTimeConverter(defaultZoneId).parseZonedDateTime(dateString); return DateTimeFormatter.ofPattern(pattern).format(zonedDateTime); } - + @SpelFunction(cat=date, desc = """ - Converts given date/time to UTC time zone and formats the result according to the given formatter pattern. + Converts given date/time to UTC time zone and formats the result according to the given formatter pattern. See 'Patterns for Formatting and Parsing' section at https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/format/DateTimeFormatter.html for details on formatter pattern syntax. If the given date/time doesn't include time zone, system @@ -217,13 +218,13 @@ public static final String formatDateTimeWithZoneId( returns="Formatted date/time") public static final String formatDateTimeAsUTC( @SpelFunctionParam(name="fmt", desc="formatter pattern used to format given date/time") String pattern, - @SpelFunctionParam(name="input", desc="date/time in JSON format to be formatted") String dateString) + @SpelFunctionParam(name="input", desc="date/time in JSON format to be formatted") String dateString) { return formatDateTimewithZoneIdAsUTC(pattern, dateString, ZoneId.systemDefault()); } - + @SpelFunction(cat=date, desc = """ - Converts given date/time to UTC time zone and formats the result according to the given formatter pattern. + Converts given date/time to UTC time zone and formats the result according to the given formatter pattern. See 'Patterns for Formatting and Parsing' section at https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/format/DateTimeFormatter.html for details on formatter pattern syntax. If the given date/time doesn't include time zone, the given @@ -239,7 +240,7 @@ public static final String formatDateTimewithZoneIdAsUTC( LocalDateTime utcDateTime = LocalDateTime.ofInstant(zonedDateTime.toInstant(), ZoneOffset.UTC); return DateTimeFormatter.ofPattern(pattern).format(utcDateTime); } - + @SpelFunction(cat=date, returns="The current date/time as `yyyy-MM-dd HH:mm:ss`") public static final String currentDateTime() { return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()); @@ -248,27 +249,27 @@ public static final String currentDateTime() { @SpelFunction(cat=workflow, desc= """ Constructs an fcli command for running an fcli action based on function arguments combined with user-supplied environment variables. Example environment variable names: - + * If `envPrefix` is `SETUP`, we look for `SETUP_ACTION` and `SETUP_EXTRA_OPTS` * If `envPrefix` is `PACKAGE_ACTION`, we look for `PACKAGE_ACTION` and `PACKAGE_ACTION_EXTRA_OPTS` - + As can be seen in the second example, if the given envPrefix already ends with `_ACTION`, \ the extra `_ACTION` suffix is skipped to avoid variable names like `PACKAGE_ACTION_ACTION`. \ Note though that we do keep `_ACTION` in `*_ACTION_EXTRA_OPTS`, to allow for having both \ PACKAGE_EXTRA_OPTS (on the `scancentral package` command), and PACKAGE_ACTION_EXTRA_OPTS \ (on the `fcli * action run package` command). - - This function returns an fcli command like `fcli action run `, + + This function returns an fcli command like `fcli action run `, where: - + * `` is taken from the corresponding function argument * `` is taken from either environment variable (if defined) or function argument, \ allowing the user to run a custom fcli action instead of built-in action * `` is taken from environment variable - """, + """, returns="`fcli action run `") public static final String actionCmd( - @SpelFunctionParam(name="envPrefix", desc="environment variable prefix") String envPrefix, + @SpelFunctionParam(name="envPrefix", desc="environment variable prefix") String envPrefix, @SpelFunctionParam(name="moduleName", desc="fcli module name") String moduleName, @SpelFunctionParam(name="actionName", desc="fcli action name") String actionName) { @@ -277,26 +278,26 @@ public static final String actionCmd( ActionSpelFunctionsHelper.envOrDefault(envPrefix.replaceAll("_ACTION$", ""), "ACTION", actionName), extraOpts(envPrefix)); } - + @SpelFunction(cat=workflow, desc = """ Returns the given fcli command, amended with extra options specified in an optional, user-supplied environment variable named `_EXTRA_OPTS`. """, - returns="` `") + returns="` `") public static final String fcliCmd( @SpelFunctionParam(name="envPrefix", desc="the environment variable prefix used to determine extra options") String envPrefix, @SpelFunctionParam(name="cmd", desc="the base command to be executed") String cmd) { return String.format("%s %s", cmd, extraOpts(envPrefix)); } - + @SpelFunction(cat=workflow, desc=""" Returns a skip reason if there's no action available to be run. If user configured a custom action \ for the given `envPrefix` (also see `#actionCmd(...)`, we assume that the action exists and thus \ return `null`. Otherwise, we check whether the given (built-in) action name is not blank and exists; - if not, we return an appropriate skip reason. - """, - returns="Skip reason or `null` if no reason to skip") + if not, we return an appropriate skip reason. + """, + returns="Skip reason or `null` if no reason to skip") public static final String actionCmdSkipNoActionReason( @SpelFunctionParam(name="envPrefix", desc="the environment variable prefix used to check the action environment variable") String envPrefix, @SpelFunctionParam(name="moduleName", desc="the name of the module to check for built-in actions") String moduleName, @@ -313,13 +314,13 @@ public static final String actionCmdSkipNoActionReason( } return null; } - + @SpelFunction(cat=workflow, desc=""" For use with `run.fcli::skip.if-reason`, returns a skip reason if either user explicitly \ set `DO_` to `false`, or if `skipByDefault` is `true` and user didn't explicitly \ set `DO_` to `true`. Note that `DO_: true` is implied if either \ `_ACTION` or `_EXTRA_OPTS` have been set. - """, + """, returns="Skip reason or `null` if no reason to skip") public static final String actionCmdSkipFromEnvReason( @SpelFunctionParam(name="envPrefix", desc="the environment variable prefix used to construct related environment variable names") String envPrefix, @@ -345,14 +346,14 @@ public static final String actionCmdSkipFromEnvReason( } return skipByDefault ? String.format("Set %s to 'true' to enable this step", doEnvName) : null; } - + @SpelFunction(cat=workflow, desc=""" For use with `run.fcli::skip.if-reason`, returns a skip reason if either user explicitly \ set `DO_` to `false`, or if `skipByDefault` is `true` and user didn't explicitly \ set `DO_` to `true`. Note that `DO_==true` is implied if \ `_EXTRA_OPTS` has been set. - """, - returns="Skip reason or `null` if no reason to skip") + """, + returns="Skip reason or `null` if no reason to skip") public static final String fcliCmdSkipFromEnvReason( @SpelFunctionParam(name="envPrefix", desc="the environment variable prefix used to construct related environment variable names") String envPrefix, @SpelFunctionParam(name="skipByDefault", desc="flag indicating whether to skip by default when no relevant environment variables are set") boolean skipByDefault) @@ -376,46 +377,46 @@ public static final String fcliCmdSkipFromEnvReason( } return skipByDefault ? String.format("Set %s to 'true' to enable this step", doEnvName) : null; } - + @SpelFunction(cat=workflow, desc=""" For use with `run.fcli::skip.if-reason`, returns the given skip reason if `skip` is \ `true`, otherwise `null` is returned. - """, - returns="Skip reason or `null` if no reason to skip") + """, + returns="Skip reason or `null` if no reason to skip") public static final String skipReasonIf( @SpelFunctionParam(name="skip", desc="the condition indicating whether to skip") boolean skip, @SpelFunctionParam(name="reason", desc="the reason to return if skipping") String reason) { return skip ? reason : null; } - + @SpelFunction(cat=workflow, desc=""" For use with `run.fcli::skip.if-reason`, returns a skip reason if the given environment \ variable hasn't been set, otherwise `null` is returned. - """, - returns="Skip reason or `null` if no reason to skip") + """, + returns="Skip reason or `null` if no reason to skip") public static final String skipBlankEnvReason( - @SpelFunctionParam(name="", desc="the name of the environment variable to check") String envName) + @SpelFunctionParam(name="", desc="the name of the environment variable to check") String envName) { return StringUtils.isNotBlank(EnvHelper.env(envName)) ? null : String.format("%s not set", envName); } @SpelFunction(cat=workflow, - returns="The given fcli action name if it exists in the given fcli module, `null` otherwise.") + returns="The given fcli action name if it exists in the given fcli module, `null` otherwise.") public static final String actionOrNull( @SpelFunctionParam(name="moduleName", desc="fcli module to check for action existence") String moduleName, @SpelFunctionParam(name="actionName", desc="fcli action to check for existence") String actionName) { return ActionSpelFunctionsHelper.hasBuiltInAction(moduleName, actionName) ? actionName : null; } - + @SpelFunction(cat=workflow, desc = """ Replaces environment variable references in the given options string with the corresponding \ environment variable values, removing any options for which the environment variable doesn't \ exist or its value is blank. For example, given `--opt1=ENV1 --opt2=ENV2`, this function will \ return `"--opt1=SomeValue"` if `ENV1` is set to `SomeValue` and `ENV2` is either blank or doesn't \ - exist. - """, returns="") + exist. + """, returns="") public static final String optsFromEnv( @SpelFunctionParam(name="input", desc="options to be resolved from environment variables") String opts) { @@ -432,7 +433,7 @@ public static final String optsFromEnv( } return String.join(" ", output); } - + @SpelFunction(cat=workflow, desc = """ Returns a formatted option string in the form `"name=value"` if the value is not blank, \ or an empty string if the value is blank. This is useful for conditionally including \ @@ -440,20 +441,20 @@ public static final String optsFromEnv( """, returns="Formatted option string `\"name=value\"` if value is not blank, empty string otherwise") public static final String opt( - @SpelFunctionParam(name="name", desc="the option name") String name, + @SpelFunctionParam(name="name", desc="the option name") String name, @SpelFunctionParam(name="value", desc="the option value; if blank, function returns empty string") String value) { if ( StringUtils.isBlank(value) ) { return ""; } return String.format("\"%s=%s\"", name, value); } @SpelFunction(cat=workflow, - returns="Value of `_EXTRA_OPTS` environment variable, or empty string if not defined") + returns="Value of `_EXTRA_OPTS` environment variable, or empty string if not defined") public static final String extraOpts( @SpelFunctionParam(name="envPrefix", desc="the environment variable prefix used to construct the full `EXTRA_OPTS` variable name") String envPrefix) { return ActionSpelFunctionsHelper.envOrDefault(envPrefix, "EXTRA_OPTS", ""); } - + @SpelFunction(cat=util, desc = """ Converts the given object into an array of key-value pairs. For example, an object `{p1: v1, p2: v2}` \ will be converted into an array `[{key: p1, value: v1}, {key: p2, value: v2}`. This can for example be @@ -481,45 +482,45 @@ public static final ArrayNode properties( Creates an issue source file resolver that maps Fortify-reported paths to workspace-relative paths. \ Fortify may add or strip leading directories during scanning; this resolver uses longest-suffix \ matching to find the correct file in the workspace. - + Configuration properties: * `workspaceDir` - Repository root directory (required for path resolution) * `sourceDir` - Directory that was scanned (optional; used to prioritize matches when multiple files share the same name) - + Example: `${#issueSourceFileResolver({workspaceDir:\"/workspace\", sourceDir:\"/workspace/src\"})}` - + For backward compatibility, if only `sourceDir` is provided, it will be used as `workspaceDir`. - + See available methods via SpEL function documentation of the returned IssueSourceFileResolver object. """, returns="Issue source file resolver with resolve() and exists() methods", - renderSubFunctions=RenderSubFunctionsMode.INLINE) + renderSubFunctions=RenderSubFunctionsMode.INLINE) public static final IssueSourceFileResolver issueSourceFileResolver( - @SpelFunctionParam(name="config", desc="configuration; may contain `workspaceDir` (repo root) and/or `sourceDir` (scan directory for prioritization)") Map config) + @SpelFunctionParam(name="config", desc="configuration; may contain `workspaceDir` (repo root) and/or `sourceDir` (scan directory for prioritization)") Map config) { var workspaceDir = config.get("workspaceDir"); var sourceDir = config.get("sourceDir"); - + // For backward compatibility: if only sourceDir provided (old usage), use it as workspaceDir if (StringUtils.isBlank(workspaceDir) && StringUtils.isNotBlank(sourceDir)) { workspaceDir = sourceDir; sourceDir = null; // Don't use as sourcePath since it's also the workspace } - + var builder = IssueSourceFileResolver.builder() .workspacePath(StringUtils.isBlank(workspaceDir) ? null : Path.of(workspaceDir)) .sourcePath(StringUtils.isBlank(sourceDir) ? null : Path.of(sourceDir)); return builder.build(); } - @SpelFunction(cat=fortify, returns="normalized array of trace nodes") + @SpelFunction(cat=fortify, returns="normalized array of trace nodes") public static final ArrayNode normalizeTraceNodes( @SpelFunctionParam(name="input", desc="the original, non-normalized array of trace nodes") ArrayNode traceNodes) { return FortifyTraceNodeHelper.normalize(traceNodes); } - @SpelFunction(cat=fortify, returns="normalized and merged array of trace nodes") + @SpelFunction(cat=fortify, returns="normalized and merged array of trace nodes") public static final ArrayNode normalizeAndMergeTraceNodes( @SpelFunctionParam(name="input", desc="the original, non-normalized array of trace nodes") ArrayNode traceNodes) { @@ -545,7 +546,7 @@ public static final JsonNode fcliBuildProperties() { public static final String copyright() { return String.format("Copyright (c) %s Open Text", Year.now().getValue()); } - + @SpelFunction(cat=internal, desc=""" Returns basic information about the local git repository for the given source directory, or null if the directory is not inside a git working tree. Only constant-time lookups are performed (HEAD commit only). @@ -571,7 +572,7 @@ public static final ObjectNode localRepo( if (builder.getGitDir()==null) { return null; } try (Repository repo = builder.build()) { var mapper = JsonHelper.getObjectMapper(); - + // Repository information var remote = ActionSpelFunctionsJGitHelper.selectRemote(repo); var remoteUrl = remote==null?null:repo.getConfig().getString("remote", remote, "url"); @@ -584,7 +585,7 @@ public static final ObjectNode localRepo( .full(names[1]) .build()) .build(); - + // Branch information CiBranch branch = null; try { @@ -596,7 +597,7 @@ public static final ObjectNode localRepo( .build(); } } catch (Exception e) { } - + // Commit information CiCommit commit = null; var headId = repo.resolve("HEAD"); @@ -610,15 +611,15 @@ public static final ObjectNode localRepo( } catch (Exception ex) { shortId = gitCommit.getId().getName().substring(0, 8); } - + var authorIdent = gitCommit.getAuthorIdent(); var committerIdent = gitCommit.getCommitterIdent(); - + var commitId = CiCommitId.builder() .full(gitCommit.getId().getName()) .short_(shortId) .build(); - + commit = CiCommit.builder() .headId(commitId) .mergeId(commitId) // Same as headId for local repos @@ -639,7 +640,7 @@ public static final ObjectNode localRepo( .build(); } catch (Exception e) { } } - + // Build root object var root = mapper.createObjectNode(); root.set("repository", mapper.valueToTree(repository)); @@ -649,11 +650,11 @@ public static final ObjectNode localRepo( if (commit != null) { root.set("commit", mapper.valueToTree(commit)); } - + return root; } catch (Exception e) { return null; } } - + private static final class ActionSpelFunctionsJsoupHelper { private static final void replaceCode(Element e) { var text = e.text(); @@ -690,7 +691,7 @@ private static String documentToPlainText(Document document) { return sb.toString(); } } - + private static final class ActionSpelFunctionsJGitHelper { private static String selectRemote(Repository repo) { try { @@ -700,7 +701,7 @@ private static String selectRemote(Repository repo) { return remotes.iterator().next(); } catch (Exception e) { return null; } } - + private static String[] deriveRepoNames(String fallbackShort, String remoteUrl) { if (StringUtils.isBlank(remoteUrl)) { return new String[]{fallbackShort, null}; } try { @@ -728,18 +729,18 @@ private static String[] deriveRepoNames(String fallbackShort, String remoteUrl) } } - - + + private static final class ActionSpelFunctionsHelper { private static final String envOrDefault(String prefix, String suffix, String defaultValue) { var envName = String.format("%s_%s", prefix, suffix).toUpperCase().replace('-', '_'); var envValue = EnvHelper.env(envName); - return StringUtils.isNotBlank(envValue) ? envValue : defaultValue; + return StringUtils.isNotBlank(envValue) ? envValue : defaultValue; } - + private static boolean hasBuiltInAction(String moduleName, String actionName) { return ActionLoaderHelper.hasBuiltInAction(moduleName, actionName); } } -} \ No newline at end of file +} diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/spel/fn/SpelFunctionsStandard.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/spel/fn/SpelFunctionsStandard.java index d6d53d5b8ca..b3bbc0ebafa 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/spel/fn/SpelFunctionsStandard.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/spel/fn/SpelFunctionsStandard.java @@ -14,14 +14,19 @@ import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.date; import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.fcli; +import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.http; import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.txt; import static com.fortify.cli.common.spel.fn.descriptor.annotation.SpelFunction.SpelFunctionCategory.util; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeParseException; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -57,11 +62,11 @@ public class SpelFunctionsStandard { @SpelFunction(cat=txt, returns="`true` if given string is null or blank, `false` otherwise") public static final boolean isBlank( - @SpelFunctionParam(name="input", desc="the string to check") String s) + @SpelFunctionParam(name="input", desc="the string to check") String s) { return StringUtils.isBlank(s); } - + @SpelFunction(cat=txt, returns="The first string if it's not blank, otherwise the second string") public static final String ifBlank( @SpelFunctionParam(name="input", desc="the string to return if not blank") String s1, @@ -69,17 +74,17 @@ public static final String ifBlank( { return StringUtils.defaultIfBlank(s1, s2); } - + @SpelFunction(cat=txt, returns="`false` if given string is null or blank, `true` otherwise") public static final boolean isNotBlank( - @SpelFunctionParam(name="input", desc="the string to check") String s) + @SpelFunctionParam(name="input", desc="the string to check") String s) { return StringUtils.isNotBlank(s); } @SpelFunction(cat=txt, returns="The substring before the first occurrence of the separator, or `null` if input string is `null`") public static final String substringBefore( - @SpelFunctionParam(name="input", desc="the string to get a substring from") String s, + @SpelFunctionParam(name="input", desc="the string to get a substring from") String s, @SpelFunctionParam(name="separator", desc="the separator to search for") String separator) { return StringUtils.substringBefore(s, separator); @@ -87,8 +92,8 @@ public static final String substringBefore( @SpelFunction(cat=txt, returns="The substring after the first occurrence of the separator, or `null` if input string is `null`") public static final String substringAfter( - @SpelFunctionParam(name="input", desc="the string to get a substring from") String s, - @SpelFunctionParam(name="separator", desc="the separator to search for") String separator) + @SpelFunctionParam(name="input", desc="the string to get a substring from") String s, + @SpelFunctionParam(name="separator", desc="the separator to search for") String separator) { return StringUtils.substringAfter(s, separator); } @@ -111,7 +116,7 @@ public static final String indent( var text = (input instanceof JsonNode j) ? j.asText() : input.toString(); return StringHelper.indent(text, prefix); } - + @SpelFunction(cat=txt, returns="String consisting of the joined elements, separated by the given delimiter") public static final String join( @SpelFunctionParam(name="delimiter", desc="the delimiter to be used between each element") String delimiter, @@ -124,21 +129,21 @@ public static final String join( } else if (source instanceof ArrayNode) { stream = JsonHelper.stream((ArrayNode) source); } - return stream == null - ? "" + return stream == null + ? "" : stream.filter(Objects::nonNull).map(SpelFunctionsStandard::toString) .collect(Collectors.joining(delimiter)); } - + @SpelFunction(cat=txt, returns="String consisting of the joined elements separated by the given delimiter _if all elements are non-null_; otherwise `null`") public static final String joinOrNull( @SpelFunctionParam(name="delimiter", desc="the delimiter to be used between each element") String delimiter, - @SpelFunctionParam(name="input", desc="the elements to join") String... parts) + @SpelFunctionParam(name="input", desc="the elements to join") String... parts) { if (parts == null || Arrays.asList(parts).stream().anyMatch(Objects::isNull)) {return null;} return String.join(delimiter, parts); } - + @SpelFunction(cat=txt, desc = "Returns a literal regex pattern string for the given input string, escaping any characters that have a special meaning in regular expressions.", returns="The regex-quoted string") public static final String regexQuote( @@ -146,7 +151,7 @@ public static final String regexQuote( { return Pattern.quote(s); } - + @SpelFunction(cat=txt, desc = """ Replaces all occurrences in the input string based on regex patterns and replacement values provided in the mapping object. @@ -170,9 +175,9 @@ public static final String replaceAllFromRegExMap( } return s; } - + @SpelFunction(cat=txt, desc = "Generates a numbered list from the given list of elements.", - returns="Numbered list of input elements, each on a new line") + returns="Numbered list of input elements, each on a new line") public static final String numberedList( @SpelFunctionParam(name="input", desc="the list of elements to be numbered and joined") List elts) { @@ -188,11 +193,81 @@ public static final boolean isDebugEnabled() { return DebugHelper.isDebugEnabled(); } - @SpelFunction(cat=util, returns="A randomly generated UUID string in standard 36-character format") + @SpelFunction(cat=util, returns="A randomly generated UUID string in standard 36-character format") public static final String uuid() { return UUID.randomUUID().toString(); } + @SpelFunction(cat=http, desc = "Builds an HTTP Basic Authorization header value from username and password.", + returns="Authorization header value in the format `Basic `") + public static final String basicAuth( + @SpelFunctionParam(name="username", desc="the username to include in the basic auth credential pair") String username, + @SpelFunctionParam(name="password", desc="the password to include in the basic auth credential pair") String password) + { + var pair = StringUtils.defaultString(username) + ":" + StringUtils.defaultString(password); + var encoded = Base64.getEncoder().encodeToString(pair.getBytes(StandardCharsets.UTF_8)); + return "Basic " + encoded; + } + + @SpelFunction(cat=http, desc = "Builds an HTTP Bearer Authorization header value from the given token.", + returns="Authorization header value in the format `Bearer `") + public static final String bearerAuth( + @SpelFunctionParam(name="token", desc="the bearer token") String token) + { + return "Bearer " + StringUtils.defaultString(token); + } + + @SpelFunction(cat=http, returns="URI-component encoded string using UTF-8, or `null` if input is `null`") + public static final String urlEncode( + @SpelFunctionParam(name="input", desc="the string to URI-component encode") String s) + { + if (s == null) { + return null; + } + return URLEncoder.encode(s, StandardCharsets.UTF_8) + .replace("+", "%20") + .replace("%7E", "~"); + } + + @SpelFunction(cat=http, returns="UTF-8 decoded string from URI-component encoded input, or `null` if input is `null`") + public static final String urlDecode( + @SpelFunctionParam(name="input", desc="the URI-component encoded string to decode") String s) + { + if (s == null) { + return null; + } + try { + return URLDecoder.decode(s.replace("+", "%2B"), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + throw new FcliSimpleException("Invalid URI-encoded input passed to #urlDecode"); + } + } + + @SpelFunction(cat=util, returns="Base64-encoded representation of the input string using UTF-8, or `null` if input is `null`") + public static final String base64Encode( + @SpelFunctionParam(name="input", desc="the string to encode as Base64") String s) + { + if (s == null) { + return null; + } + return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)); + } + + @SpelFunction(cat=util, returns="UTF-8 decoded string from the given Base64 input, or `null` if input is `null`") + public static final String base64Decode( + @SpelFunctionParam(name="input", desc="the Base64-encoded string to decode") String s) + { + if (s == null) { + return null; + } + try { + var decodedBytes = Base64.getDecoder().decode(s); + return new String(decodedBytes, StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + throw new FcliSimpleException("Invalid Base64 input passed to #base64Decode: " + s); + } + } + @SpelFunction(cat=txt, desc = """ Formats a string using the specified format string and arguments, returning the formatted string. \ See https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Formatter.html#syntax \ @@ -309,7 +384,7 @@ public static final String jsonStringify( throw new FcliTechnicalException("Error converting object to JSON string", e); } } - + private static final String toString(Object o) { if ( o==null ) { return ""; diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/spel/fn/descriptor/annotation/SpelFunction.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/spel/fn/descriptor/annotation/SpelFunction.java index 6bdb9153ce3..988036dba95 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/spel/fn/descriptor/annotation/SpelFunction.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/spel/fn/descriptor/annotation/SpelFunction.java @@ -37,8 +37,8 @@ * Example: github.repo() with SECTION creates section "repo" with uploadSarif, etc. */ RenderSubFunctionsMode renderSubFunctions() default RenderSubFunctionsMode.AUTO; - + public static enum SpelFunctionCategory { - txt, date, workflow, fortify, fcli, util, ci, internal + txt, date, workflow, fortify, fcli, util, http, ci, internal } }