We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add possibility to configure backend options in conversion configuration file


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `tools/sigma/backends/base.py`
Content:
```
1 # Output backends for sigmac
2 # Copyright 2016-2018 Thomas Patzke, Florian Roth
3 
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Lesser General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Lesser General Public License for more details.
13 
14 # You should have received a copy of the GNU Lesser General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 
17 import sigma
18 from .mixins import RulenameCommentMixin, QuoteCharMixin
19 
20 class BackendOptions(dict):
21     """Object contains all options that should be passed to the backend from command line (or other user interfaces)"""
22 
23     def __init__(self, options):
24         """
25         Receives the argparser result from the backend option paramater value list (nargs=*) and builds the dict from it. There are two option types:
26 
27         * key=value: self{key} = value
28         * key: self{key} = True
29         """
30         if options == None:
31             return
32         for option in options:
33             parsed = option.split("=", 1)
34             try:
35                 self[parsed[0]] = parsed[1]
36             except IndexError:
37                 self[parsed[0]] = True
38 
39 ### Generic backend base classes
40 class BaseBackend:
41     """Base class for all backends"""
42     identifier = "base"
43     active = False
44     index_field = None    # field name that is used to address indices
45     file_list = None
46     options = tuple()     # a list of tuples with following elements: option name, default value, help text, target attribute name (option name if None)
47 
48     def __init__(self, sigmaconfig, backend_options=None):
49         """
50         Initialize backend. This gets a sigmaconfig object, which is notified about the used backend class by
51         passing the object instance to it.
52         """
53         super().__init__()
54         if not isinstance(sigmaconfig, (sigma.configuration.SigmaConfiguration, None)):
55             raise TypeError("SigmaConfiguration object expected")
56         self.backend_options = backend_options
57         self.sigmaconfig = sigmaconfig
58         self.sigmaconfig.set_backend(self)
59 
60         # Parse options
61         for option, default_value, _, target in self.options:
62             if target is None:
63                 target = option
64             setattr(self, target, self.backend_options.setdefault(option, default_value))
65 
66     def generate(self, sigmaparser):
67         """Method is called for each sigma rule and receives the parsed rule (SigmaParser)"""
68         for parsed in sigmaparser.condparsed:
69             query = self.generateQuery(parsed)
70             before = self.generateBefore(parsed)
71             after = self.generateAfter(parsed)
72 
73             result = ""
74             if before is not None:
75                 result = before
76             if query is not None:
77                 result += query
78             if after is not None:
79                 result += after
80 
81             return result
82 
83     def generateQuery(self, parsed):
84         result = self.generateNode(parsed.parsedSearch)
85         if parsed.parsedAgg:
86             result += self.generateAggregation(parsed.parsedAgg)
87         return result
88 
89     def generateNode(self, node):
90         if type(node) == sigma.parser.condition.ConditionAND:
91             return self.generateANDNode(node)
92         elif type(node) == sigma.parser.condition.ConditionOR:
93             return self.generateORNode(node)
94         elif type(node) == sigma.parser.condition.ConditionNOT:
95             return self.generateNOTNode(node)
96         elif type(node) == sigma.parser.condition.ConditionNULLValue:
97             return self.generateNULLValueNode(node)
98         elif type(node) == sigma.parser.condition.ConditionNotNULLValue:
99             return self.generateNotNULLValueNode(node)
100         elif type(node) == sigma.parser.condition.NodeSubexpression:
101             return self.generateSubexpressionNode(node)
102         elif type(node) == tuple:
103             return self.generateMapItemNode(node)
104         elif type(node) in (str, int):
105             return self.generateValueNode(node)
106         elif type(node) == list:
107             return self.generateListNode(node)
108         else:
109             raise TypeError("Node type %s was not expected in Sigma parse tree" % (str(type(node))))
110 
111     def generateANDNode(self, node):
112         raise NotImplementedError("Node type not implemented for this backend")
113 
114     def generateORNode(self, node):
115         raise NotImplementedError("Node type not implemented for this backend")
116 
117     def generateNOTNode(self, node):
118         raise NotImplementedError("Node type not implemented for this backend")
119 
120     def generateSubexpressionNode(self, node):
121         raise NotImplementedError("Node type not implemented for this backend")
122 
123     def generateListNode(self, node):
124         raise NotImplementedError("Node type not implemented for this backend")
125 
126     def generateMapItemNode(self, node):
127         raise NotImplementedError("Node type not implemented for this backend")
128 
129     def generateValueNode(self, node):
130         raise NotImplementedError("Node type not implemented for this backend")
131 
132     def generateNULLValueNode(self, node):
133         raise NotImplementedError("Node type not implemented for this backend")
134 
135     def generateNotNULLValueNode(self, node):
136         raise NotImplementedError("Node type not implemented for this backend")
137 
138     def generateAggregation(self, agg):
139         raise NotImplementedError("Aggregations not implemented for this backend")
140 
141     def generateBefore(self, parsed):
142         return ""
143 
144     def generateAfter(self, parsed):
145         return ""
146 
147     def finalize(self):
148         """
149         Is called after the last file was processed with generate(). The right place if this backend is not intended to
150         look isolated at each rule, but generates an output which incorporates multiple rules, e.g. dashboards.
151         """
152         pass
153 
154 class SingleTextQueryBackend(RulenameCommentMixin, BaseBackend, QuoteCharMixin):
155     """Base class for backends that generate one text-based expression from a Sigma rule"""
156     identifier = "base-textquery"
157     active = False
158 
159     # the following class variables define the generation and behavior of queries from a parse tree some are prefilled with default values that are quite usual
160     andToken = None                     # Token used for linking expressions with logical AND
161     orToken = None                      # Same for OR
162     notToken = None                     # Same for NOT
163     subExpression = None                # Syntax for subexpressions, usually parenthesis around it. %s is inner expression
164     listExpression = None               # Syntax for lists, %s are list items separated with listSeparator
165     listSeparator = None                # Character for separation of list items
166     valueExpression = None              # Expression of values, %s represents value
167     nullExpression = None               # Expression of queries for null values or non-existing fields. %s is field name
168     notNullExpression = None            # Expression of queries for not null values. %s is field name
169     mapExpression = None                # Syntax for field/value conditions. First %s is fieldname, second is value
170     mapListsSpecialHandling = False     # Same handling for map items with list values as for normal values (strings, integers) if True, generateMapItemListNode method is called with node
171     mapListValueExpression = None       # Syntax for field/value condititons where map value is a list
172 
173     def generateANDNode(self, node):
174         generated = [ self.generateNode(val) for val in node ]
175         filtered = [ g for g in generated if g is not None ]
176         if filtered:
177             return self.andToken.join(filtered)
178         else:
179             return None
180 
181     def generateORNode(self, node):
182         generated = [ self.generateNode(val) for val in node ]
183         filtered = [ g for g in generated if g is not None ]
184         if filtered:
185             return self.orToken.join(filtered)
186         else:
187             return None
188 
189     def generateNOTNode(self, node):
190         generated = self.generateNode(node.item)
191         if generated is not None:
192             return self.notToken + generated
193         else:
194             return None
195 
196     def generateSubexpressionNode(self, node):
197         generated = self.generateNode(node.items)
198         if generated:
199             return self.subExpression % generated
200         else:
201             return None
202 
203     def generateListNode(self, node):
204         if not set([type(value) for value in node]).issubset({str, int}):
205             raise TypeError("List values must be strings or numbers")
206         return self.listExpression % (self.listSeparator.join([self.generateNode(value) for value in node]))
207 
208     def generateMapItemNode(self, node):
209         fieldname, value = node
210 
211         transformed_fieldname = self.fieldNameMapping(fieldname, value)
212         if self.mapListsSpecialHandling == False and type(value) in (str, int, list) or self.mapListsSpecialHandling == True and type(value) in (str, int):
213             return self.mapExpression % (transformed_fieldname, self.generateNode(value))
214         elif type(value) == list:
215             return self.generateMapItemListNode(transformed_fieldname, value)
216         else:
217             raise TypeError("Backend does not support map values of type " + str(type(value)))
218 
219     def generateMapItemListNode(self, fieldname, value):
220         return self.mapListValueExpression % (fieldname, self.generateNode(value))
221 
222     def generateValueNode(self, node):
223         return self.valueExpression % (self.cleanValue(str(node)))
224 
225     def generateNULLValueNode(self, node):
226         return self.nullExpression % (node.item)
227 
228     def generateNotNULLValueNode(self, node):
229         return self.notNullExpression % (node.item)
230 
231     def fieldNameMapping(self, fieldname, value):
232         """
233         Alter field names depending on the value(s). Backends may use this method to perform a final transformation of the field name
234         in addition to the field mapping defined in the conversion configuration. The field name passed to this method was already
235         transformed from the original name given in the Sigma rule.
236         """
237         return fieldname
238
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/tools/sigma/backends/base.py b/tools/sigma/backends/base.py
--- a/tools/sigma/backends/base.py
+++ b/tools/sigma/backends/base.py
@@ -14,28 +14,73 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import sys
+
 import sigma
+import yaml
+
 from .mixins import RulenameCommentMixin, QuoteCharMixin
 
+
 class BackendOptions(dict):
-    """Object contains all options that should be passed to the backend from command line (or other user interfaces)"""
+    """
+    Object containing all the options that should be passed to the backend.
+    
+    The options can come from command line and a YAML configuration file, and will be merged together.
+    Options from the command line take precedence.
+    """
+
+    def __init__(self, options, config_file):
+        """
+        :param options: unparsed options coming from the CLI
+        :param config_file: path to a YAML configuration file
+        """
+
+        self._load_config_file(config_file)
+        self._parse_options(options)
 
-    def __init__(self, options):
+    def _parse_options(self, options):
         """
-        Receives the argparser result from the backend option paramater value list (nargs=*) and builds the dict from it. There are two option types:
+        Populates options from the unparsed options of the CLI
 
-        * key=value: self{key} = value
-        * key: self{key} = True
+        :param options: list unparsed options from the CLI.
+            Each option can have one of the following formats:
+            - "key=value": the option key:value will be passed to the backend
+            - "key": the option key:True will be passed to the backend
         """
-        if options == None:
+
+        if options is None:
             return
+
         for option in options:
             parsed = option.split("=", 1)
             try:
                 self[parsed[0]] = parsed[1]
             except IndexError:
+                # If the option is present but doesn't map to a value, treat it as a boolean flag
                 self[parsed[0]] = True
 
+    def _load_config_file(self, path):
+        """
+        Populates options from a configuration file
+
+        :param path: Path to the configuration file
+        """
+        if path is None:
+            return
+
+        try:
+            with open(path, 'r') as config_file:
+                backend_config = yaml.safe_load(config_file.read())
+                for key in backend_config:
+                    self[key] = backend_config[key]
+        except (IOError, OSError) as e:
+            print("Failed to open backend configuration file '%s': %s" % (path, str(e)), file=sys.stderr)
+            exit(1)
+        except yaml.YAMLError as e:
+            print("Failed to parse backend configuration file '%s' as valid YAML: %s" % (path, str(e)), file=sys.stderr)
+            exit(1)
+
 ### Generic backend base classes
 class BaseBackend:
     """Base class for all backends"""

{"golden_diff": "diff --git a/tools/sigma/backends/base.py b/tools/sigma/backends/base.py\n--- a/tools/sigma/backends/base.py\n+++ b/tools/sigma/backends/base.py\n@@ -14,28 +14,73 @@\n # You should have received a copy of the GNU Lesser General Public License\n # along with this program.  If not, see <http://www.gnu.org/licenses/>.\n \n+import sys\n+\n import sigma\n+import yaml\n+\n from .mixins import RulenameCommentMixin, QuoteCharMixin\n \n+\n class BackendOptions(dict):\n-    \"\"\"Object contains all options that should be passed to the backend from command line (or other user interfaces)\"\"\"\n+    \"\"\"\n+    Object containing all the options that should be passed to the backend.\n+    \n+    The options can come from command line and a YAML configuration file, and will be merged together.\n+    Options from the command line take precedence.\n+    \"\"\"\n+\n+    def __init__(self, options, config_file):\n+        \"\"\"\n+        :param options: unparsed options coming from the CLI\n+        :param config_file: path to a YAML configuration file\n+        \"\"\"\n+\n+        self._load_config_file(config_file)\n+        self._parse_options(options)\n \n-    def __init__(self, options):\n+    def _parse_options(self, options):\n         \"\"\"\n-        Receives the argparser result from the backend option paramater value list (nargs=*) and builds the dict from it. There are two option types:\n+        Populates options from the unparsed options of the CLI\n \n-        * key=value: self{key} = value\n-        * key: self{key} = True\n+        :param options: list unparsed options from the CLI.\n+            Each option can have one of the following formats:\n+            - \"key=value\": the option key:value will be passed to the backend\n+            - \"key\": the option key:True will be passed to the backend\n         \"\"\"\n-        if options == None:\n+\n+        if options is None:\n             return\n+\n         for option in options:\n             parsed = option.split(\"=\", 1)\n             try:\n                 self[parsed[0]] = parsed[1]\n             except IndexError:\n+                # If the option is present but doesn't map to a value, treat it as a boolean flag\n                 self[parsed[0]] = True\n \n+    def _load_config_file(self, path):\n+        \"\"\"\n+        Populates options from a configuration file\n+\n+        :param path: Path to the configuration file\n+        \"\"\"\n+        if path is None:\n+            return\n+\n+        try:\n+            with open(path, 'r') as config_file:\n+                backend_config = yaml.safe_load(config_file.read())\n+                for key in backend_config:\n+                    self[key] = backend_config[key]\n+        except (IOError, OSError) as e:\n+            print(\"Failed to open backend configuration file '%s': %s\" % (path, str(e)), file=sys.stderr)\n+            exit(1)\n+        except yaml.YAMLError as e:\n+            print(\"Failed to parse backend configuration file '%s' as valid YAML: %s\" % (path, str(e)), file=sys.stderr)\n+            exit(1)\n+\n ### Generic backend base classes\n class BaseBackend:\n     \"\"\"Base class for all backends\"\"\"\n", "issue": "Add possibility to configure backend options in conversion configuration file\n\n", "before_files": [{"content": "# Output backends for sigmac\n# Copyright 2016-2018 Thomas Patzke, Florian Roth\n\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Lesser General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU Lesser General Public License for more details.\n\n# You should have received a copy of the GNU Lesser General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nimport sigma\nfrom .mixins import RulenameCommentMixin, QuoteCharMixin\n\nclass BackendOptions(dict):\n    \"\"\"Object contains all options that should be passed to the backend from command line (or other user interfaces)\"\"\"\n\n    def __init__(self, options):\n        \"\"\"\n        Receives the argparser result from the backend option paramater value list (nargs=*) and builds the dict from it. There are two option types:\n\n        * key=value: self{key} = value\n        * key: self{key} = True\n        \"\"\"\n        if options == None:\n            return\n        for option in options:\n            parsed = option.split(\"=\", 1)\n            try:\n                self[parsed[0]] = parsed[1]\n            except IndexError:\n                self[parsed[0]] = True\n\n### Generic backend base classes\nclass BaseBackend:\n    \"\"\"Base class for all backends\"\"\"\n    identifier = \"base\"\n    active = False\n    index_field = None    # field name that is used to address indices\n    file_list = None\n    options = tuple()     # a list of tuples with following elements: option name, default value, help text, target attribute name (option name if None)\n\n    def __init__(self, sigmaconfig, backend_options=None):\n        \"\"\"\n        Initialize backend. This gets a sigmaconfig object, which is notified about the used backend class by\n        passing the object instance to it.\n        \"\"\"\n        super().__init__()\n        if not isinstance(sigmaconfig, (sigma.configuration.SigmaConfiguration, None)):\n            raise TypeError(\"SigmaConfiguration object expected\")\n        self.backend_options = backend_options\n        self.sigmaconfig = sigmaconfig\n        self.sigmaconfig.set_backend(self)\n\n        # Parse options\n        for option, default_value, _, target in self.options:\n            if target is None:\n                target = option\n            setattr(self, target, self.backend_options.setdefault(option, default_value))\n\n    def generate(self, sigmaparser):\n        \"\"\"Method is called for each sigma rule and receives the parsed rule (SigmaParser)\"\"\"\n        for parsed in sigmaparser.condparsed:\n            query = self.generateQuery(parsed)\n            before = self.generateBefore(parsed)\n            after = self.generateAfter(parsed)\n\n            result = \"\"\n            if before is not None:\n                result = before\n            if query is not None:\n                result += query\n            if after is not None:\n                result += after\n\n            return result\n\n    def generateQuery(self, parsed):\n        result = self.generateNode(parsed.parsedSearch)\n        if parsed.parsedAgg:\n            result += self.generateAggregation(parsed.parsedAgg)\n        return result\n\n    def generateNode(self, node):\n        if type(node) == sigma.parser.condition.ConditionAND:\n            return self.generateANDNode(node)\n        elif type(node) == sigma.parser.condition.ConditionOR:\n            return self.generateORNode(node)\n        elif type(node) == sigma.parser.condition.ConditionNOT:\n            return self.generateNOTNode(node)\n        elif type(node) == sigma.parser.condition.ConditionNULLValue:\n            return self.generateNULLValueNode(node)\n        elif type(node) == sigma.parser.condition.ConditionNotNULLValue:\n            return self.generateNotNULLValueNode(node)\n        elif type(node) == sigma.parser.condition.NodeSubexpression:\n            return self.generateSubexpressionNode(node)\n        elif type(node) == tuple:\n            return self.generateMapItemNode(node)\n        elif type(node) in (str, int):\n            return self.generateValueNode(node)\n        elif type(node) == list:\n            return self.generateListNode(node)\n        else:\n            raise TypeError(\"Node type %s was not expected in Sigma parse tree\" % (str(type(node))))\n\n    def generateANDNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateORNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateNOTNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateSubexpressionNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateListNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateMapItemNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateValueNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateNULLValueNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateNotNULLValueNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateAggregation(self, agg):\n        raise NotImplementedError(\"Aggregations not implemented for this backend\")\n\n    def generateBefore(self, parsed):\n        return \"\"\n\n    def generateAfter(self, parsed):\n        return \"\"\n\n    def finalize(self):\n        \"\"\"\n        Is called after the last file was processed with generate(). The right place if this backend is not intended to\n        look isolated at each rule, but generates an output which incorporates multiple rules, e.g. dashboards.\n        \"\"\"\n        pass\n\nclass SingleTextQueryBackend(RulenameCommentMixin, BaseBackend, QuoteCharMixin):\n    \"\"\"Base class for backends that generate one text-based expression from a Sigma rule\"\"\"\n    identifier = \"base-textquery\"\n    active = False\n\n    # the following class variables define the generation and behavior of queries from a parse tree some are prefilled with default values that are quite usual\n    andToken = None                     # Token used for linking expressions with logical AND\n    orToken = None                      # Same for OR\n    notToken = None                     # Same for NOT\n    subExpression = None                # Syntax for subexpressions, usually parenthesis around it. %s is inner expression\n    listExpression = None               # Syntax for lists, %s are list items separated with listSeparator\n    listSeparator = None                # Character for separation of list items\n    valueExpression = None              # Expression of values, %s represents value\n    nullExpression = None               # Expression of queries for null values or non-existing fields. %s is field name\n    notNullExpression = None            # Expression of queries for not null values. %s is field name\n    mapExpression = None                # Syntax for field/value conditions. First %s is fieldname, second is value\n    mapListsSpecialHandling = False     # Same handling for map items with list values as for normal values (strings, integers) if True, generateMapItemListNode method is called with node\n    mapListValueExpression = None       # Syntax for field/value condititons where map value is a list\n\n    def generateANDNode(self, node):\n        generated = [ self.generateNode(val) for val in node ]\n        filtered = [ g for g in generated if g is not None ]\n        if filtered:\n            return self.andToken.join(filtered)\n        else:\n            return None\n\n    def generateORNode(self, node):\n        generated = [ self.generateNode(val) for val in node ]\n        filtered = [ g for g in generated if g is not None ]\n        if filtered:\n            return self.orToken.join(filtered)\n        else:\n            return None\n\n    def generateNOTNode(self, node):\n        generated = self.generateNode(node.item)\n        if generated is not None:\n            return self.notToken + generated\n        else:\n            return None\n\n    def generateSubexpressionNode(self, node):\n        generated = self.generateNode(node.items)\n        if generated:\n            return self.subExpression % generated\n        else:\n            return None\n\n    def generateListNode(self, node):\n        if not set([type(value) for value in node]).issubset({str, int}):\n            raise TypeError(\"List values must be strings or numbers\")\n        return self.listExpression % (self.listSeparator.join([self.generateNode(value) for value in node]))\n\n    def generateMapItemNode(self, node):\n        fieldname, value = node\n\n        transformed_fieldname = self.fieldNameMapping(fieldname, value)\n        if self.mapListsSpecialHandling == False and type(value) in (str, int, list) or self.mapListsSpecialHandling == True and type(value) in (str, int):\n            return self.mapExpression % (transformed_fieldname, self.generateNode(value))\n        elif type(value) == list:\n            return self.generateMapItemListNode(transformed_fieldname, value)\n        else:\n            raise TypeError(\"Backend does not support map values of type \" + str(type(value)))\n\n    def generateMapItemListNode(self, fieldname, value):\n        return self.mapListValueExpression % (fieldname, self.generateNode(value))\n\n    def generateValueNode(self, node):\n        return self.valueExpression % (self.cleanValue(str(node)))\n\n    def generateNULLValueNode(self, node):\n        return self.nullExpression % (node.item)\n\n    def generateNotNULLValueNode(self, node):\n        return self.notNullExpression % (node.item)\n\n    def fieldNameMapping(self, fieldname, value):\n        \"\"\"\n        Alter field names depending on the value(s). Backends may use this method to perform a final transformation of the field name\n        in addition to the field mapping defined in the conversion configuration. The field name passed to this method was already\n        transformed from the original name given in the Sigma rule.\n        \"\"\"\n        return fieldname\n", "path": "tools/sigma/backends/base.py"}], "after_files": [{"content": "# Output backends for sigmac\n# Copyright 2016-2018 Thomas Patzke, Florian Roth\n\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Lesser General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU Lesser General Public License for more details.\n\n# You should have received a copy of the GNU Lesser General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nimport sys\n\nimport sigma\nimport yaml\n\nfrom .mixins import RulenameCommentMixin, QuoteCharMixin\n\n\nclass BackendOptions(dict):\n    \"\"\"\n    Object containing all the options that should be passed to the backend.\n    \n    The options can come from command line and a YAML configuration file, and will be merged together.\n    Options from the command line take precedence.\n    \"\"\"\n\n    def __init__(self, options, config_file):\n        \"\"\"\n        :param options: unparsed options coming from the CLI\n        :param config_file: path to a YAML configuration file\n        \"\"\"\n\n        self._load_config_file(config_file)\n        self._parse_options(options)\n\n    def _parse_options(self, options):\n        \"\"\"\n        Populates options from the unparsed options of the CLI\n\n        :param options: list unparsed options from the CLI.\n            Each option can have one of the following formats:\n            - \"key=value\": the option key:value will be passed to the backend\n            - \"key\": the option key:True will be passed to the backend\n        \"\"\"\n\n        if options is None:\n            return\n\n        for option in options:\n            parsed = option.split(\"=\", 1)\n            try:\n                self[parsed[0]] = parsed[1]\n            except IndexError:\n                # If the option is present but doesn't map to a value, treat it as a boolean flag\n                self[parsed[0]] = True\n\n    def _load_config_file(self, path):\n        \"\"\"\n        Populates options from a configuration file\n\n        :param path: Path to the configuration file\n        \"\"\"\n        if path is None:\n            return\n\n        try:\n            with open(path, 'r') as config_file:\n                backend_config = yaml.safe_load(config_file.read())\n                for key in backend_config:\n                    self[key] = backend_config[key]\n        except (IOError, OSError) as e:\n            print(\"Failed to open backend configuration file '%s': %s\" % (path, str(e)), file=sys.stderr)\n            exit(1)\n        except yaml.YAMLError as e:\n            print(\"Failed to parse backend configuration file '%s' as valid YAML: %s\" % (path, str(e)), file=sys.stderr)\n            exit(1)\n\n### Generic backend base classes\nclass BaseBackend:\n    \"\"\"Base class for all backends\"\"\"\n    identifier = \"base\"\n    active = False\n    index_field = None    # field name that is used to address indices\n    file_list = None\n    options = tuple()     # a list of tuples with following elements: option name, default value, help text, target attribute name (option name if None)\n\n    def __init__(self, sigmaconfig, backend_options=None):\n        \"\"\"\n        Initialize backend. This gets a sigmaconfig object, which is notified about the used backend class by\n        passing the object instance to it.\n        \"\"\"\n        super().__init__()\n        if not isinstance(sigmaconfig, (sigma.configuration.SigmaConfiguration, None)):\n            raise TypeError(\"SigmaConfiguration object expected\")\n        self.backend_options = backend_options\n        self.sigmaconfig = sigmaconfig\n        self.sigmaconfig.set_backend(self)\n\n        # Parse options\n        for option, default_value, _, target in self.options:\n            if target is None:\n                target = option\n            setattr(self, target, self.backend_options.setdefault(option, default_value))\n\n    def generate(self, sigmaparser):\n        \"\"\"Method is called for each sigma rule and receives the parsed rule (SigmaParser)\"\"\"\n        for parsed in sigmaparser.condparsed:\n            query = self.generateQuery(parsed)\n            before = self.generateBefore(parsed)\n            after = self.generateAfter(parsed)\n\n            result = \"\"\n            if before is not None:\n                result = before\n            if query is not None:\n                result += query\n            if after is not None:\n                result += after\n\n            return result\n\n    def generateQuery(self, parsed):\n        result = self.generateNode(parsed.parsedSearch)\n        if parsed.parsedAgg:\n            result += self.generateAggregation(parsed.parsedAgg)\n        return result\n\n    def generateNode(self, node):\n        if type(node) == sigma.parser.condition.ConditionAND:\n            return self.generateANDNode(node)\n        elif type(node) == sigma.parser.condition.ConditionOR:\n            return self.generateORNode(node)\n        elif type(node) == sigma.parser.condition.ConditionNOT:\n            return self.generateNOTNode(node)\n        elif type(node) == sigma.parser.condition.ConditionNULLValue:\n            return self.generateNULLValueNode(node)\n        elif type(node) == sigma.parser.condition.ConditionNotNULLValue:\n            return self.generateNotNULLValueNode(node)\n        elif type(node) == sigma.parser.condition.NodeSubexpression:\n            return self.generateSubexpressionNode(node)\n        elif type(node) == tuple:\n            return self.generateMapItemNode(node)\n        elif type(node) in (str, int):\n            return self.generateValueNode(node)\n        elif type(node) == list:\n            return self.generateListNode(node)\n        else:\n            raise TypeError(\"Node type %s was not expected in Sigma parse tree\" % (str(type(node))))\n\n    def generateANDNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateORNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateNOTNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateSubexpressionNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateListNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateMapItemNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateValueNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateNULLValueNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateNotNULLValueNode(self, node):\n        raise NotImplementedError(\"Node type not implemented for this backend\")\n\n    def generateAggregation(self, agg):\n        raise NotImplementedError(\"Aggregations not implemented for this backend\")\n\n    def generateBefore(self, parsed):\n        return \"\"\n\n    def generateAfter(self, parsed):\n        return \"\"\n\n    def finalize(self):\n        \"\"\"\n        Is called after the last file was processed with generate(). The right place if this backend is not intended to\n        look isolated at each rule, but generates an output which incorporates multiple rules, e.g. dashboards.\n        \"\"\"\n        pass\n\nclass SingleTextQueryBackend(RulenameCommentMixin, BaseBackend, QuoteCharMixin):\n    \"\"\"Base class for backends that generate one text-based expression from a Sigma rule\"\"\"\n    identifier = \"base-textquery\"\n    active = False\n\n    # the following class variables define the generation and behavior of queries from a parse tree some are prefilled with default values that are quite usual\n    andToken = None                     # Token used for linking expressions with logical AND\n    orToken = None                      # Same for OR\n    notToken = None                     # Same for NOT\n    subExpression = None                # Syntax for subexpressions, usually parenthesis around it. %s is inner expression\n    listExpression = None               # Syntax for lists, %s are list items separated with listSeparator\n    listSeparator = None                # Character for separation of list items\n    valueExpression = None              # Expression of values, %s represents value\n    nullExpression = None               # Expression of queries for null values or non-existing fields. %s is field name\n    notNullExpression = None            # Expression of queries for not null values. %s is field name\n    mapExpression = None                # Syntax for field/value conditions. First %s is fieldname, second is value\n    mapListsSpecialHandling = False     # Same handling for map items with list values as for normal values (strings, integers) if True, generateMapItemListNode method is called with node\n    mapListValueExpression = None       # Syntax for field/value condititons where map value is a list\n\n    def generateANDNode(self, node):\n        generated = [ self.generateNode(val) for val in node ]\n        filtered = [ g for g in generated if g is not None ]\n        if filtered:\n            return self.andToken.join(filtered)\n        else:\n            return None\n\n    def generateORNode(self, node):\n        generated = [ self.generateNode(val) for val in node ]\n        filtered = [ g for g in generated if g is not None ]\n        if filtered:\n            return self.orToken.join(filtered)\n        else:\n            return None\n\n    def generateNOTNode(self, node):\n        generated = self.generateNode(node.item)\n        if generated is not None:\n            return self.notToken + generated\n        else:\n            return None\n\n    def generateSubexpressionNode(self, node):\n        generated = self.generateNode(node.items)\n        if generated:\n            return self.subExpression % generated\n        else:\n            return None\n\n    def generateListNode(self, node):\n        if not set([type(value) for value in node]).issubset({str, int}):\n            raise TypeError(\"List values must be strings or numbers\")\n        return self.listExpression % (self.listSeparator.join([self.generateNode(value) for value in node]))\n\n    def generateMapItemNode(self, node):\n        fieldname, value = node\n\n        transformed_fieldname = self.fieldNameMapping(fieldname, value)\n        if self.mapListsSpecialHandling == False and type(value) in (str, int, list) or self.mapListsSpecialHandling == True and type(value) in (str, int):\n            return self.mapExpression % (transformed_fieldname, self.generateNode(value))\n        elif type(value) == list:\n            return self.generateMapItemListNode(transformed_fieldname, value)\n        else:\n            raise TypeError(\"Backend does not support map values of type \" + str(type(value)))\n\n    def generateMapItemListNode(self, fieldname, value):\n        return self.mapListValueExpression % (fieldname, self.generateNode(value))\n\n    def generateValueNode(self, node):\n        return self.valueExpression % (self.cleanValue(str(node)))\n\n    def generateNULLValueNode(self, node):\n        return self.nullExpression % (node.item)\n\n    def generateNotNULLValueNode(self, node):\n        return self.notNullExpression % (node.item)\n\n    def fieldNameMapping(self, fieldname, value):\n        \"\"\"\n        Alter field names depending on the value(s). Backends may use this method to perform a final transformation of the field name\n        in addition to the field mapping defined in the conversion configuration. The field name passed to this method was already\n        transformed from the original name given in the Sigma rule.\n        \"\"\"\n        return fieldname\n", "path": "tools/sigma/backends/base.py"}]}