SetWithTemplate

Set a value built by a template in context under the key you entered.

python · Common Scripts

Source

from collections.abc import Callable
from typing import Any

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401


class ContextData:
    def __init__(self, context: dict[str, Any] | None = None):
        self.__context = context or demisto.context()

    def __get_inputs_value(self, key: str) -> Any:
        inputs = demisto.callingContext.get("context.PlaybookInputs") or {}
        return demisto.dt(inputs, key)

    def __get_incident_value(self, key: str) -> Any:
        incident = demisto.incident()
        val = demisto.dt(incident, key)
        if val is None:
            val = demisto.dt(incident.get("CustomFields") or {}, key)
        return val

    def __get_lists_value(self, key: str) -> Any:
        if key.startswith("."):
            key = key[1:]

        name = key
        ok, val = execute_command("getList", {"listName": name}, fail_on_error=False)
        if not ok:
            for sep in ["=", "(", "."]:
                name = key.split(sep, maxsplit=1)[0]
                ok, val = execute_command("getList", {"listName": name}, fail_on_error=False)
                if ok:
                    break
            else:
                return None

        val = demisto.dt(val, "." + key[len(name) :])
        if isinstance(val, str):
            try:
                val = json.loads(val)
            except json.JSONDecodeError:
                pass
        return val

    def get(self, key: str | None = None) -> Any:
        """Get the context value

        :param key: The dt expressions (string within ${}).
        :return: The value.
        """
        if not key:
            return None

        for prefix, handler in {
            "inputs": ContextData.__get_inputs_value,
            "incident": ContextData.__get_incident_value,
            "lists": ContextData.__get_lists_value,
        }.items():
            if prefix == key or (key.startswith(prefix) and key[len(prefix) : len(prefix) + 1] in (".", "(", "=")):
                return handler(self, key[len(prefix) :])
        return demisto.dt(self.__context, key)


class Formatter:
    def __init__(self, start_marker: str, end_marker: str, keep_symbol_to_null: bool):
        if not start_marker:
            raise ValueError("start-marker is required.")

        self.__start_marker = start_marker
        self.__end_marker = end_marker
        self.__keep_symbol_to_null = keep_symbol_to_null

    @staticmethod
    def __is_end_mark(source: str, ci: int, end_marker: str) -> bool:
        if end_marker:
            return source[ci : ci + len(end_marker)] == end_marker
        else:
            c = source[ci]
            if c.isspace():
                return True
            elif c.isascii():
                return c != "_" and not c.isalnum()
            else:
                return False

    def __extract(
        self,
        source: str,
        extractor: Callable[[str, ContextData | None], Any] | None,
        dx: ContextData | None,
        si: int,
        markers: tuple[str, str] | None,
    ) -> tuple[Any, int | None]:
        """Extract a template text, or an enclosed value within starting and ending marks

        :param source: The template text, or the enclosed value starts with the next charactor of a start marker
        :param extractor: The function to extract an enclosed value as DT
        :param dx: The context data
        :param si: The index of `source` to start extracting
        :param markers: The start and end marker to find an end position for parsing an enclosed value.
                        It must be None when the template text is given to `source`.
        :return: The extracted value and index of `source` when parsing ended.
                 The index is the next after the end marker when extracting the enclosed value.
        """
        out = None
        ci = si
        while ci < len(source):
            if markers is not None and Formatter.__is_end_mark(source, ci, markers[1]):
                key = source[si:ci] if out is None else str(out) + source[si:ci]
                if extractor:
                    if (xval := extractor(key, dx)) is None and self.__keep_symbol_to_null:
                        xval = markers[0] + key + markers[1]
                else:
                    xval = key
                return xval, ci + len(markers[1])
            elif extractor and source[ci : ci + len(self.__start_marker)] == self.__start_marker:
                xval, ei = self.__extract(
                    source, extractor, dx, ci + len(self.__start_marker), (self.__start_marker, self.__end_marker)
                )
                if si != ci:
                    out = source[si:ci] if out is None else str(out) + source[si:ci]

                if ei is None:
                    xval = self.__start_marker
                    ei = ci + len(self.__start_marker)

                if out is None:
                    out = xval
                elif xval is not None:
                    out = str(out) + str(xval)
                si = ci = ei
            elif markers is None:
                ci += 1
            elif endc := {"(": ")", "{": "}", "[": "]", '"': '"', "'": "'"}.get(source[ci]):
                _, ei = self.__extract(source, None, dx, ci + 1, (source[ci], endc))
                ci = ci + 1 if ei is None else ei
            elif source[ci] == "\\":
                ci += 2
            else:
                ci += 1

        if markers is not None:
            # unbalanced braces, brackets, quotes, etc.
            return None, None
        elif not extractor:
            return None, ci
        elif si >= len(source):
            return out, ci
        elif out is None:
            return source[si:], ci
        else:
            return str(out) + source[si:], ci

    def build(self, template: Any, extractor: Callable[[str, ContextData | None], Any] | None, dx: ContextData | None) -> Any:
        """Format a text from a template including DT expressions

        :param template: The template.
        :param extractor: The extractor to get real value within ${dt}.
        :param dx: The context instance.
        :return: The text built from the template.
        """
        if isinstance(template, dict):
            return {self.build(k, extractor, dx): self.build(v, extractor, dx) for k, v in template.items()}
        elif isinstance(template, list):
            return [self.build(v, extractor, dx) for v in template]
        elif isinstance(template, str):
            return self.__extract(template, extractor, dx, 0, None)[0] if template else ""
        else:
            return template


def extract_dt(dtstr: str, dx: ContextData | None) -> Any:
    """Extract dt expression

    :param dtstr: The dt expressions (string within ${}).
    :param dx: The context instance.
    :return: The value extracted.
    """
    try:
        return dx.get(dtstr) if dx else dtstr
    except Exception as err:
        demisto.debug(f'failed to extract dt from "{dtstr=}". Error: {err}')
        return None


def normalize_value(value: Any, stringify: str) -> Any:
    if stringify == "noop":
        return value
    elif stringify == "all":
        if isinstance(value, dict):
            # key should be str for the context
            value = {k if isinstance(k, str) else json.dumps(k): normalize_value(v, stringify) for k, v in value.items()}
        elif isinstance(value, list):
            value = [normalize_value(v, stringify) for v in value]
        elif not isinstance(value, str):
            value = json.dumps(value)
    elif stringify == "false":
        if isinstance(value, str):
            try:
                value = json.loads(value)
            except json.JSONDecodeError:
                pass
    elif stringify == "true":
        if not isinstance(value, str):
            value = json.dumps(value)
    else:
        raise DemistoException(f"Invalid stringify value: {stringify}")

    return value


def main():
    try:
        args = assign_params(**demisto.args())
        key = args.get("key")
        template = args.get("template")
        template_type = args.get("template_type", "raw")
        append = argToBoolean(args.get("append", False))
        stringify = args.get("stringify", "noop")
        force = argToBoolean(args.get("force", False))
        keep_symbol_to_null = argToBoolean(args.get("keep_symbol_to_null", False))
        variable_markers = argToList(args.get("variable_markers", "${,}"))

        if not variable_markers or not variable_markers[0]:
            raise ValueError("variable_markers must have a start marker.")
        elif len(variable_markers) >= 3:
            raise ValueError("too many values for variable_markers.")
        elif len(variable_markers) == 1:
            variable_markers = variable_markers + [""]

        value = ""
        if template:
            if template_type == "json":
                template = json.loads(template)
            elif template_type != "raw":
                raise DemistoException(f"Invalid template type: {template_type}")

            context = args.get("context")
            if context:
                context = json.loads(context) if isinstance(context, str) else context
            else:
                context = demisto.context()

            formatter = Formatter(variable_markers[0], variable_markers[1], keep_symbol_to_null)
            value = formatter.build(template, extract_dt, ContextData(context))

        value = normalize_value(value, stringify)

        if value or force:
            readable_output = f"Key {key} set"
            outputs = {key: value}
        else:
            readable_output = "value is None"
            outputs = {}

        if not append and outputs:
            demisto.executeCommand("DeleteContext", {"key": key, "subplaybook": "auto"})

        return_results(CommandResults(readable_output=readable_output, outputs=outputs))

    except Exception as err:
        return_error(str(err))


if __name__ in ("__builtin__", "builtins", "__main__"):
    main()

README

Set a value built by a template in context under the key you entered.

Script Data


Name Description
Script Type python3
Tags Utility
Cortex XSOAR Version 6.5.0

Inputs


Argument Name Description
key The key to set. Can be a full path such as “Key.ID”. If using append=true can also use a DT selector such as “Data(val.ID == obj.ID)”.
template The template text which can include DT expressions such as ${value}.
template_type The template type.
append If false, the context key will be overwritten. If set to true, the script will be appended to the existing context key.
stringify Whether to save the argument as a string. The default value is “noop”.
force Whether to force the creation of the context. The default value is “false”.
context The context data which overwrites the Demisto context.
variable_markers The pair of start and end markers to bracket a variable name.
keep_symbol_to_null Set to true to not replace a value if the variable is null, otherwise false.

Outputs


There are no outputs for this script.

Getting Started


The script builds a text from a template text which includes variables such as:

  • This is a test message for ${user_name}.

The template will be formatted to This is a test message for John Doe. by replacing variable parameters.

By default, a variable name starts with ${ and ends with } . You can change the start marker and end marker by specifying the variable_markers parameter.

Examples


Replace variables in a text based on the context data

Command

!SetWithTemplate key=out template=${lists.Template}

Lists Library

Template:

My name is ${first_name} ${last_name}.

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

{
  "out": "My name is John Doe."
}

Change the variable start and end marker to the windows command shell style such as %name%

Command

!SetWithTemplate key=out template=${lists.Template} variable_markers=%,%

Lists Library

Template:

My name is %first_name% %last_name%.

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

{
  "out": "My name is John Doe."
}

Change the variable start and end marker to the UNIX shell style such as $name

Command

!SetWithTemplate key=out template=${lists.Template} variable_markers=$

Lists Library

Template:

My name is $first_name $last_name.

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

{
  "out": "My name is John Doe."
}

Keep variable names if they are missing in the context

Command

!SetWithTemplate key=out template=${lists.Template} keep_symbol_to_null=true

Lists Library

Template:

My name is ${first_name} ${last_name}.

Context Data

{
  "first_name": "John"
}

Output

{
  "out": "My name is John ${last_name}."
}

Use DTs to build variables

Command

!SetWithTemplate key=out template=${lists.Template}

Lists Library

Template:

My name is ${first_name=val.toUpperCase()} ${last_name=val.toUpperCase()}.

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

{
  "out": "My name is JOHN DOE."
}