1# Copyright (C) 2017 Open Information Security Foundation
2# Copyright (c) 2013 Jason Ish
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"""Provide mappings from ID's to descriptions.
19
20Includes mapping classes for event ID messages and classification
21information.
22"""
23
24from __future__ import print_function
25
26import re
27
28class SignatureMap(object):
29    """SignatureMap maps signature IDs to a signature info dict.
30
31    The signature map can be build up from classification.config,
32    gen-msg.map, and new and old-style sid-msg.map files.
33
34    The dict's in the map will have at a minimum the following
35    fields:
36
37    * gid *(int)*
38    * sid *(int)*
39    * msg *(string)*
40    * refs *(list of strings)*
41
42    Signatures loaded from a new style sid-msg.map file will also have
43    *rev*, *classification* and *priority* fields.
44
45    Example::
46
47        >>> from idstools import maps
48        >>> sigmap = maps.SignatureMap()
49        >>> sigmap.load_generator_map(open("tests/gen-msg.map"))
50        >>> sigmap.load_signature_map(open("tests/sid-msg-v2.map"))
51        >>> print(sigmap.get(1, 2495))
52        {'classification': 'misc-attack', 'rev': 8, 'priority': 0, 'gid': 1,
53        'sid': 2495,
54        'msg': 'GPL NETBIOS SMB DCEPRC ORPCThis request flood attempt',
55        'ref': ['bugtraq,8811', 'cve,2003-0813', 'nessus,12206',
56        'url,www.microsoft.com/technet/security/bulletin/MS04-011.mspx']}
57
58    """
59
60    def __init__(self):
61        self.map = {}
62
63    def size(self):
64        return len(self.map)
65
66    def get(self, generator_id, signature_id):
67        """Get signature info by generator_id and signature_id.
68
69        :param generator_id: The generator id of the signature to lookup.
70        :param signature_id: The signature id of the signature to lookup.
71
72        For convenience, if the generator_id is 3 and the signature is
73        not found, a second lookup will be done using a generator_id
74        of 1.
75
76        """
77
78        key = (generator_id, signature_id)
79        sig = self.map.get(key)
80        if sig is None and generator_id == 3:
81            return self.get(1, signature_id)
82        return sig
83
84    def load_generator_map(self, fileobj):
85        """Load the generator message map (gen-msg.map) from a
86        file-like object.
87
88        """
89        for line in fileobj:
90            line = line.strip()
91            if not line or line.startswith("#"):
92                continue
93            gid, sid, msg = [part.strip() for part in line.split("||")]
94            entry = {
95                "gid": int(gid),
96                "sid": int(sid),
97                "msg": msg,
98                "refs": [],
99            }
100            self.map[(entry["gid"], entry["sid"])] = entry
101
102    def load_signature_map(self, fileobj, defaultgid=1):
103        """Load signature message map (sid-msg.map) from a file-like
104        object.
105
106        """
107
108        for line in fileobj:
109            line = line.strip()
110            if not line or line.startswith("#"):
111                continue
112            parts = [p.strip() for p in line.split("||")]
113
114            # If we have at least 6 parts, attempt to parse as a v2
115            # signature map file.
116            try:
117                entry = {
118                    "gid": int(parts[0]),
119                    "sid": int(parts[1]),
120                    "rev": int(parts[2]),
121                    "classification": parts[3],
122                    "priority": int(parts[4]),
123                    "msg": parts[5],
124                    "ref": parts[6:],
125                }
126            except:
127                entry = {
128                    "gid": defaultgid,
129                    "sid": int(parts[0]),
130                    "msg": parts[1],
131                    "ref": parts[2:],
132                }
133            self.map[(entry["gid"], entry["sid"])] = entry
134
135class ClassificationMap(object):
136    """ClassificationMap maps classification IDs and names to a dict
137    object describing a classification.
138
139    :param fileobj: (Optional) A file like object to load
140      classifications from on initialization.
141
142    The classification dicts stored in the map have the following
143    fields:
144
145    * name *(string)*
146    * description *(string)*
147    * priority *(int)*
148
149    Example::
150
151        >>> from idstools import maps
152        >>> classmap = maps.ClassificationMap()
153        >>> classmap.load_from_file(open("tests/classification.config"))
154
155        >>> classmap.get(3)
156        {'priority': 2, 'name': 'bad-unknown', 'description': 'Potentially Bad Traffic'}
157        >>> classmap.get_by_name("bad-unknown")
158        {'priority': 2, 'name': 'bad-unknown', 'description': 'Potentially Bad Traffic'}
159
160    """
161
162    def __init__(self, fileobj=None):
163        self.id_map = []
164        self.name_map = {}
165
166        if fileobj:
167            self.load_from_file(fileobj)
168
169    def size(self):
170        return len(self.id_map)
171
172    def add(self, classification):
173        """Add a classification to the map."""
174        self.id_map.append(classification)
175        self.name_map[classification["name"]] = classification
176
177    def get(self, class_id):
178        """Get a classification by ID.
179
180        :param class_id: The classification ID to get.
181
182        :returns: A dict describing the classification or None.
183
184        """
185        if 0 < class_id <= len(self.id_map):
186            return self.id_map[class_id - 1]
187        else:
188            return None
189
190    def get_by_name(self, name):
191        """Get a classification by name.
192
193        :param name: The name of the classification
194
195        :returns: A dict describing the classification or None.
196
197        """
198        if name in self.name_map:
199            return self.name_map[name]
200        else:
201            return None
202
203    def load_from_file(self, fileobj):
204        """Load classifications from a Snort style
205        classification.config file object.
206
207        """
208        pattern = "config classification: ([^,]+),([^,]+),([^,]+)"
209        for line in fileobj:
210            m = re.match(pattern, line.strip())
211            if m:
212                self.add({
213                    "name": m.group(1),
214                    "description": m.group(2),
215                    "priority": int(m.group(3))})
216