Details
| ID | If-Elif |
|---|---|
| Language | python |
| From Version | 6.9.0 |
| Docker Image | demisto/python3:3.12.13.10404775 |
| Tags | transformer 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()