If-Elif

A transformer for if-elif-else logic.

python · Filters And Transformers

Details

IDIf-Elif
Languagepython
From Version6.9.0
Docker Imagedemisto/python3:3.12.13.10404775
Tagstransformer general

README

A transformer for if-elif-else logic

The If-Elif transformer simulates a python “if elif else” tree using a JSON provided in the conditions argument.
The JSON should be a list of dictionaries where all have the keys “condition”, which holds a boolean expression, and “return”, which holds the value to return if “condition” is evaluated to be true. To return a default value if all “condition”s were false, the last dictionary should have only the key “default” holding the valid JSON value. If this is not provided an empty string will be returned as a default.
In order to prevent injections, context values should be retrieved from the value entered in the value (Get) of the transformer with the hash-curly brackets #{...} syntax. This syntax has the same behavior as the classic XSOAR ${...} syntax and uses the Cortex XSOAR Transform Language (DT). To provide the full context to the transformer, use ${.} as the value (Get) argument. Note: when used as a “return” value, this syntax should not be surrounded by quotes.

Supported operators for conditions

Comparison operators work like Python operators:

| Operator | Name | Example |
| — | — | — |
| == | Equal | x == y |
| != | Not equal | x != y |
| > | Greater than | x > y |
| < | Less than | x < y |
| >= | Greater than or equal to | x >= y |
| <= | Less than or equal to | x <= y |
| in | In | x in y|
| not in | Not in | x not in y|
Note: If a comparison is incomparable by nature (e.g., 'a' < 3), it will evaluate to false.

Logical operators also follow the Python syntax:

Operator Description Example
and Returns True if both statements are true x < 5 and x < 10
or Returns True if one of the statements is true x < 5 or x < 4
not Reverse the result, returns False if the result is true not(x < 5 and x < 10)

Regular expressions are implemented with the “regex_match” function, in the format: regex_match('pattern', 'string'). The behavior of the function is controlled with the flags argument.

Literal strings should preferably be surrounded by single quotes. Do not use #{...} in a string, instead, use the + operator. For example: 'first ' + #{second.string} + ' third' will be equal to the common "first ${second.string} third". (This method can be used for lists too.)
Note: If the + operator is used on distinct types (e.g., 'a' + None), it will evaluate to None (null).

The following flags can be used in the flags argument to control the transformer’s behavior:

Flag Effect Example
case_insensitive Comparisons between strings and regex matches are case-insensitive. 'WoRd' == 'wOrD'
list_compare Comparing an object with a list also compares the object with all values in the list and evaluates to true if any comparison is true.
Works on operators: < > <= >= in not in +
'word' in ['word1', 'word2']
regex_dot_all Make the . special character match any character at all, including a newline. Without this flag, . will match anything except a newline. regex_match('a.b', 'a\nb')
regex_multiline The patterns ^ and $ will match the beginning and end of each line respectively as opposed to the beginning and end of the string. regex_match('^\d$', '1\n2\n')
regex_full_match Regex patterns will be compared with the whole string to find a match. not regex_match('\d+', 'a12345')

Example


value (Get)
${.}
conditions
[
  {
    "condition": "'www.' + #{domain.name} + '.com' not in #{approved.sites}",
    "return": #{domain.name} + "/home"
  },
  {
    "condition": "#{number} >= 5 and #{path.to.string} == 'Yes'",
    "return": "valid"
  },
  {
    "condition": "regex_match('\d+', #{some.value})",
    "return": #{value.to.return}
  },
  {
    "default": #{default.value}
  }
]
flags
case_insensitive,regex_dot_all,regex_multiline

Script Data


Name Description
Script Type python3
Tags transformer, general
Cortex XSOAR Version 6.9.0

Inputs


Argument Name Description
value The object from which to grab values. For the full context use “${.}”.
conditions A JSON formatted list, where all but the last items are dictionaries with the keys “condition” (holding a boolean expression) and “return” (holding the value to return if “condition” is true).
The last dictionary can have the key “default” which can hold any valid JSON object to return if no “condition” was true.
flags Flags to control comparison and regular expression behavior. Possible values are: case_insensitive, list_compare, regex_dot_all, regex_multiline, regex_full_match.

Outputs


There are no outputs for this script.

import ast
from collections.abc import Callable
from functools import partial, reduce

import demistomock as demisto
from CommonServerPython import *


def return_none_on_error(func: Callable) -> Callable:
    """Makes a function return None if an error is raised."""

    def new_func(*args):
        try:
            return func(*args)
        except Exception:
            return None

    return new_func


class ConditionParser:
    known_constants: dict[str, Any] = {"true": True, "false": False, "null": None}

    comparison_operators: dict[type, Callable] = {
        ast.Eq: lambda x, y: x == y,
        ast.NotEq: lambda x, y: x != y,
        ast.Lt: return_none_on_error(lambda x, y: x < y),
        ast.LtE: return_none_on_error(lambda x, y: x <= y),
        ast.Gt: return_none_on_error(lambda x, y: x > y),
        ast.GtE: return_none_on_error(lambda x, y: x >= y),
        ast.In: return_none_on_error(lambda x, y: x in y),
        ast.NotIn: return_none_on_error(lambda x, y: x not in y),
    }

    boolean_operators: dict[type, Callable] = {
        ast.And: lambda x, y: x and y,
        ast.Or: lambda x, y: x or y,
    }

    unary_operators: dict[type, Callable] = {
        ast.Not: lambda x: not x,
        ast.USub: lambda x: -x,
    }

    binary_operators: dict[type, Callable] = {
        ast.Add: return_none_on_error(lambda x, y: x + y),
    }

    def __init__(self, context, conditions, flags=None, **_):
        self.conditions: list
        self.functions: dict[str, Callable] = {"from_context": partial(demisto.dt, context)}
        self.modify_functions_with_flags(argToList(flags))
        self.load_conditions(conditions)
        self.default = self.conditions.pop()["default"] if "default" in self.conditions[-1] else ""
        self.validate_conditions()

    def modify_functions_with_flags(self, flags: list):
        self.regex_flags = (
            re.DOTALL * ("regex_dot_all" in flags)
            | re.MULTILINE * ("regex_multiline" in flags)
            | re.IGNORECASE * ("case_insensitive" in flags)
        )
        self.functions["regex_match"] = partial(
            re.fullmatch if "regex_full_match" in flags else re.search, flags=self.regex_flags
        )
        if "case_insensitive" in flags:

            def to_case_insensitive(func):
                return lambda x, y: func(repr(x).lower(), repr(y).lower())

            self.comparison_operators |= {
                ast.Eq: to_case_insensitive(self.comparison_operators[ast.Eq]),
                ast.NotEq: to_case_insensitive(self.comparison_operators[ast.NotEq]),
            }
        if "list_compare" in flags:

            def to_deep_search(func):
                return lambda x, y: (func(x, y) or (any(func(x, i) for i in y) if isinstance(y, list) else False))

            self.comparison_operators = {k: to_deep_search(v) for k, v in self.comparison_operators.items()}

    def load_conditions(self, conditions):
        conditions = re.sub(r"#{([\s\S]+?)}", r" from_context('\1')", conditions)
        try:
            self.conditions = self.evaluate(conditions)
        except SyntaxError as e:
            raise SyntaxError(f"Cannot load JSON. Invalid syntax at line: {e.args[1][1]}; position: {e.args[1][2]}") from e

    def validate_conditions(self):
        for i, d in enumerate(self.conditions, 1):
            if "condition" not in d:
                raise ValueError(f'Condition {i} has no key "condition".')
            elif "return" not in d:
                raise ValueError(f'Condition {i} has no key "return".')
            elif not isinstance(d, dict):
                raise ValueError(f"Condition {i} is not a dictionary.")

    def get_value(self, node):
        match type(node):
            case ast.Name:
                return self.known_constants[node.id]
            case ast.Constant:
                return node.value
            case ast.List:
                return [self.get_value(item) for item in node.elts]
            case ast.Dict:
                return {self.get_value(key): self.get_value(value) for key, value in zip(node.keys, node.values)}
            case ast.Call:
                return self.functions[node.func.id](*map(self.get_value, node.args))
            case ast.Compare:
                left = self.get_value(node.left)
                return all(
                    self.comparison_operators[type(op)](
                        left,
                        left := self.get_value(right),  # noqa: F841
                    )
                    for op, right in zip(node.ops, node.comparators)
                )
            case ast.BoolOp:
                return reduce(self.boolean_operators[type(node.op)], map(self.get_value, node.values))
            case ast.BinOp:
                return self.binary_operators[type(node.op)](self.get_value(node.left), self.get_value(node.right))
            case ast.UnaryOp:
                return self.unary_operators[type(node.op)](self.get_value(node.operand))
            case _:
                raise KeyError(node.__class__.__name__)

    def evaluate(self, expression: str):
        try:
            parsed = ast.parse(expression.strip(), mode="eval")
            return self.get_value(parsed.body)
        except KeyError as e:
            raise NameError(f"Unknown variable/operator: {e.args[0]!r}") from e

    def parse_conditions(self):
        try:
            return next(
                (condition["return"] for condition in self.conditions if self.evaluate(condition["condition"])), self.default
            )
        except SyntaxError as e:
            raise SyntaxError(f"Invalid expression: {e.args[1][3]!r}") from e


def main():
    try:
        args: dict = demisto.args()
        if_elif = ConditionParser(args.pop("value"), **args)
        return_results(if_elif.parse_conditions())
    except Exception as e:
        return_error(f"Error in If-Elif Transformer: {e}")


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