|
| 1 | +# Copyright (C) 2017-2019 Open Information Security Foundation |
| 2 | +# Copyright (c) 2020 Michael Schem |
| 3 | +# |
| 4 | +# You can copy, redistribute or modify this Program under the terms of |
| 5 | +# the GNU General Public License version 2 as published by the Free |
| 6 | +# Software Foundation. |
| 7 | +# |
| 8 | +# This program is distributed in the hope that it will be useful, |
| 9 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 | +# GNU General Public License for more details. |
| 12 | +# |
| 13 | +# You should have received a copy of the GNU General Public License |
| 14 | +# version 2 along with this program; if not, write to the Free Software |
| 15 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 16 | +# 02110-1301, USA. |
| 17 | + |
| 18 | +""" Module for parsing categories.txt files. |
| 19 | +
|
| 20 | +Parsing is done using regular expressions and the job of the module is |
| 21 | +to do its best at parsing out fields of interest from the categories file |
| 22 | +rather than perform a sanity check. |
| 23 | +
|
| 24 | +""" |
| 25 | + |
| 26 | +from __future__ import print_function |
| 27 | + |
| 28 | +from suricata.update import fileparser |
| 29 | + |
| 30 | +import io |
| 31 | +import re |
| 32 | +import logging |
| 33 | + |
| 34 | +logger = logging.getLogger(__name__) |
| 35 | + |
| 36 | +# Compile a re pattern for basic iprep directive matching |
| 37 | +category_pattern = re.compile(r"^(?P<enabled>#)*[\s#]*" |
| 38 | + r"(?P<id>\d+)," |
| 39 | + r"(?P<short_name>\w+)," |
| 40 | + r"(?P<description>.*$)") |
| 41 | + |
| 42 | +class Category(dict): |
| 43 | + """ Class representing an iprep category |
| 44 | +
|
| 45 | + The category class also acts like a dictionary. |
| 46 | +
|
| 47 | + Dictionary fields: |
| 48 | +
|
| 49 | + - **enabled**: True if the category is enabled (uncommented), false is |
| 50 | + disabled (commented) |
| 51 | + - **id**: The maximum value for the category id is hard coded at 60 |
| 52 | + currently (Suricata 5.0.3). |
| 53 | + - **short_name**: The shortname that refers to the category. |
| 54 | + - **description**: A description of the category. |
| 55 | +
|
| 56 | + :param enabled: Optional parameter to set the enabled state of the category |
| 57 | +
|
| 58 | + """ |
| 59 | + |
| 60 | + def __init__(self, enabled=None): |
| 61 | + dict.__init__(self) |
| 62 | + self["enabled"] = enabled |
| 63 | + self["id"] = None |
| 64 | + self["short_name"] = None |
| 65 | + self["description"] = None |
| 66 | + |
| 67 | + def __getattr__(self, name): |
| 68 | + return self[name] |
| 69 | + |
| 70 | + @property |
| 71 | + def id(self): |
| 72 | + """ The ID of the category. |
| 73 | +
|
| 74 | + :returns: An int ID of the category |
| 75 | + :rtype: int |
| 76 | + """ |
| 77 | + return int(self["id"]) |
| 78 | + |
| 79 | + @property |
| 80 | + def idstr(self): |
| 81 | + """Return the gid and sid of the rule as a string formatted like: |
| 82 | + '[id]'""" |
| 83 | + return "[%s]" % str(self.id) |
| 84 | + |
| 85 | + def __str__(self): |
| 86 | + """ The string representation of the category. |
| 87 | +
|
| 88 | + If the category is disabled it will be returned as commented out. |
| 89 | + """ |
| 90 | + return self.format() |
| 91 | + |
| 92 | + def format(self): |
| 93 | + return "{0}{1},{2},{3}".format(u"" if self["enabled"] else u"# ", |
| 94 | + self['id'], |
| 95 | + self['short_name'], |
| 96 | + self['description']) |
| 97 | + |
| 98 | + |
| 99 | +def parse(buf, group=None): |
| 100 | + """ Parse a single Iprep category from a string buffer. |
| 101 | +
|
| 102 | + :param buf: A string buffer containing a single Iprep category. |
| 103 | +
|
| 104 | + :returns: An instance of a :py:class:`.Category` representing the parsed Iprep category |
| 105 | + """ |
| 106 | + |
| 107 | + if type(buf) == type(b""): |
| 108 | + buf = buf.decode("utf-8") |
| 109 | + buf = buf.strip() |
| 110 | + |
| 111 | + m = category_pattern.match(buf) |
| 112 | + if not m: |
| 113 | + return None |
| 114 | + |
| 115 | + if m.group("enabled") == "#": |
| 116 | + enabled = False |
| 117 | + else: |
| 118 | + enabled = True |
| 119 | + |
| 120 | + # header = m.group("header").strip() |
| 121 | + |
| 122 | + category = Category(enabled=enabled) |
| 123 | + |
| 124 | + category["id"] = int(m.group("id").strip()) |
| 125 | + |
| 126 | + if not 0 < category["id"] < 60: |
| 127 | + logging.error("Category id of {0}, not valid. Id is required to be between 0 and 60.".format(category["id"])) |
| 128 | + return None |
| 129 | + |
| 130 | + category["short_name"] = m.group("short_name").strip() |
| 131 | + |
| 132 | + category["description"] = m.group("description").strip() |
| 133 | + |
| 134 | + return category |
| 135 | + |
| 136 | + |
| 137 | +def parse_fileobj(fileobj, group=None): |
| 138 | + """ Parse multiple ipreps from a file like object. |
| 139 | +
|
| 140 | + Note: At this time ipreps must exist on one line. |
| 141 | +
|
| 142 | + :param fileobj: A file like object to parse rules from. |
| 143 | +
|
| 144 | + :returns: A list of :py:class:`.Iprep` instances, one for each rule parsed |
| 145 | + """ |
| 146 | + return fileparser.parse_fileobj(fileobj, parse, group) |
| 147 | + |
| 148 | +def parse_file(filename, group=None): |
| 149 | + """ Parse multiple ipreps from the provided filename. |
| 150 | +
|
| 151 | + :param filename: Name of file to parse ipreps from |
| 152 | +
|
| 153 | + :returns: A list of :py:class:`.Iprep` instances, one for each iprep parsed |
| 154 | + """ |
| 155 | + with io.open(filename, encoding="utf-8") as fileobj: |
| 156 | + return parse_fileobj(fileobj, group) |
| 157 | + |
0 commit comments