summaryrefslogtreecommitdiff
path: root/lib/python2.7/site-packages/setools/permmap.py
blob: 55da2fa53fb0a3d5132bba33f8d744750bd79e7b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# Copyright 2014-2015, Tresys Technology, LLC
#
# This file is part of SETools.
#
# SETools is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 2.1 of
# the License, or (at your option) any later version.
#
# SETools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with SETools.  If not, see
# <http://www.gnu.org/licenses/>.
#
import sys
import logging
import copy
from collections import OrderedDict
from errno import ENOENT

from . import exception
from . import policyrep
from .descriptors import PermissionMapDescriptor

infoflow_directions = ["r", "w", "b", "n", "u"]
min_weight = 1
max_weight = 10


class PermissionMap(object):

    """Permission Map for information flow analysis."""

    def __init__(self, permmapfile=None):
        """
        Parameter:
        permmapfile     The path to the permission map to load.
        """
        self.log = logging.getLogger(__name__)
        self.permmap = OrderedDict()
        self.permmapfile = None

        if permmapfile:
            self.load(permmapfile)
        else:
            for path in ["data/", sys.prefix + "/share/setools/"]:
                try:
                    self.load(path + "perm_map")
                    break
                except (IOError, OSError) as err:
                    if err.errno != ENOENT:
                        raise
            else:
                raise RuntimeError("Unable to load default permission map.")

    def __str__(self):
        return self.permmapfile

    def __deepcopy__(self, memo):
        newobj = PermissionMap.__new__(PermissionMap)
        newobj.log = self.log
        newobj.permmap = copy.deepcopy(self.permmap)
        newobj.permmapfile = self.permmapfile
        memo[id(self)] = newobj
        return newobj

    def load(self, permmapfile):
        """
        Parameter:
        permmapfile     The path to the permission map to load.
        """
        self.log.info("Opening permission map \"{0}\"".format(permmapfile))

        # state machine
        # 1 = read number of classes
        # 2 = read class name and number of perms
        # 3 = read perms
        with open(permmapfile, "r") as mapfile:
            total_perms = 0
            class_count = 0
            num_classes = 0
            state = 1

            self.permmap.clear()

            for line_num, line in enumerate(mapfile, start=1):
                entry = line.split()

                if len(entry) == 0 or entry[0][0] == '#':
                    continue

                if state == 1:
                    try:
                        num_classes = int(entry[0])
                    except ValueError:
                        raise exception.PermissionMapParseError(
                            "{0}:{1}:Invalid number of classes: {2}".
                            format(permmapfile, line_num, entry[0]))

                    if num_classes < 1:
                        raise exception.PermissionMapParseError(
                            "{0}:{1}:Number of classes must be positive: {2}".
                            format(permmapfile, line_num, entry[0]))

                    state = 2

                elif state == 2:
                    if len(entry) != 3 or entry[0] != "class":
                        raise exception.PermissionMapParseError(
                            "{0}:{1}:Invalid class declaration: {2}".
                            format(permmapfile, line_num, entry))

                    class_name = str(entry[1])

                    try:
                        num_perms = int(entry[2])
                    except ValueError:
                        raise exception.PermissionMapParseError(
                            "{0}:{1}:Invalid number of permissions: {2}".
                            format(permmapfile, line_num, entry[2]))

                    if num_perms < 1:
                        raise exception.PermissionMapParseError(
                            "{0}:{1}:Number of permissions must be positive: {2}".
                            format(permmapfile, line_num, entry[2]))

                    class_count += 1
                    if class_count > num_classes:
                        raise exception.PermissionMapParseError(
                            "{0}:{1}:Extra class found: {2}".
                            format(permmapfile, line_num, class_name))

                    self.permmap[class_name] = OrderedDict()
                    perm_count = 0
                    state = 3

                elif state == 3:
                    perm_name = str(entry[0])

                    flow_direction = str(entry[1])
                    if flow_direction not in infoflow_directions:
                        raise exception.PermissionMapParseError(
                            "{0}:{1}:Invalid information flow direction: {2}".
                            format(permmapfile, line_num, entry[1]))

                    try:
                        weight = int(entry[2])
                    except ValueError:
                        raise exception.PermissionMapParseError(
                            "{0}:{1}:Invalid permission weight: {2}".
                            format(permmapfile, line_num, entry[2]))

                    if not min_weight <= weight <= max_weight:
                        raise exception.PermissionMapParseError(
                            "{0}:{1}:Permission weight must be {3}-{4}: {2}".
                            format(permmapfile, line_num, entry[2],
                                   min_weight, max_weight))

                    self.log.debug("Read {0}:{1} {2} {3}".format(
                                   class_name, perm_name, flow_direction, weight))

                    if flow_direction == 'u':
                        self.log.info("Permission {0}:{1} is unmapped.".format(
                                      class_name, perm_name))

                    mapping = Mapping(self.permmap, class_name, perm_name, create=True)
                    mapping.direction = flow_direction
                    mapping.weight = weight

                    total_perms += 1
                    perm_count += 1
                    if perm_count >= num_perms:
                        state = 2

        self.permmapfile = permmapfile
        self.log.info("Successfully opened permission map \"{0}\"".format(permmapfile))
        self.log.debug("Read {0} classes and {1} total permissions.".format(
                       class_count, total_perms))

    def save(self, permmapfile):
        """
        Save the permission map to the specified path.  Existing files
        will be overwritten.

        Parameter:
        permmapfile         The path to write the permission map.
        """
        with open(permmapfile, "w") as mapfile:
            self.log.info("Writing permission map to \"{0}\"".format(permmapfile))
            mapfile.write("{0}\n\n".format(len(self.permmap)))

            for classname, perms in self.permmap.items():
                mapfile.write("class {0} {1}\n".format(classname, len(perms)))

                for permname, settings in perms.items():
                    direction = settings['direction']
                    weight = settings['weight']

                    assert min_weight <= weight <= max_weight, \
                        "{0}:{1} weight is out of range ({2}). This is an SETools bug.".format(
                            classname, permname, weight)

                    assert direction in infoflow_directions, \
                        "{0}:{1} flow direction ({2}) is invalid. This is an SETools bug.".format(
                            classname, permname, direction)

                    if direction == 'u':
                        self.log.warning("Warning: permission {0} in class {1} is unmapped.".format(
                                         permname, classname))

                    mapfile.write("{0:>20} {1:>9} {2:>9}\n".format(permname, direction, weight))

                mapfile.write("\n")

            self.log.info("Successfully wrote permission map to \"{0}\"".format(permmapfile))

    def classes(self):
        """
        Generate class names in the permission map.

        Yield:
        class       An object class name.
        """
        for cls in self.permmap.keys():
            yield cls

    def perms(self, class_):
        """
        Generate permission mappings for the specified class.

        Parameter:
        class_      An object class name.

        Yield:
        Mapping     A permission's complete map (weight, direction, enabled)
        """
        try:
            for perm in self.permmap[class_].keys():
                yield Mapping(self.permmap, class_, perm)
        except KeyError:
            raise exception.UnmappedClass("{0} is not mapped.".format(class_))

    def mapping(self, class_, perm):
        """Retrieve a specific permission's mapping."""
        return Mapping(self.permmap, class_, perm)

    def exclude_class(self, class_):
        """
        Exclude all permissions in an object class for calculating rule weights.

        Parameter:
        class_              The object class to exclude.

        Exceptions:
        UnmappedClass       The specified object class is not mapped.
        """
        for perm in self.perms(class_):
            perm.enabled = False

    def exclude_permission(self, class_, permission):
        """
        Exclude a permission for calculating rule weights.

        Parameter:
        class_              The object class of the permission.
        permission          The permission name to exclude.

        Exceptions:
        UnmappedClass       The specified object class is not mapped.
        UnmappedPermission  The specified permission is not mapped for the object class.
        """
        Mapping(self.permmap, class_, permission).enabled = False

    def include_class(self, class_):
        """
        Include all permissions in an object class for calculating rule weights.

        Parameter:
        class_              The object class to include.

        Exceptions:
        UnmappedClass       The specified object class is not mapped.
        """

        for perm in self.perms(class_):
            perm.enabled = True

    def include_permission(self, class_, permission):
        """
        Include a permission for calculating rule weights.

        Parameter:
        class_              The object class of the permission.
        permission          The permission name to include.

        Exceptions:
        UnmappedClass       The specified object class is not mapped.
        UnmappedPermission  The specified permission is not mapped for the object class.
        """

        Mapping(self.permmap, class_, permission).enabled = True

    def map_policy(self, policy):
        """Create mappings for all classes and permissions in the specified policy."""
        for class_ in policy.classes():
            class_name = str(class_)

            if class_name not in self.permmap:
                self.log.debug("Adding unmapped class {0} from {1}".format(class_name, policy))
                self.permmap[class_name] = OrderedDict()

            perms = class_.perms

            try:
                perms |= class_.common.perms
            except policyrep.exception.NoCommon:
                pass

            for perm_name in perms:
                if perm_name not in self.permmap[class_name]:
                    self.log.debug("Adding unmapped permission {0} in {1} from {2}".
                                   format(perm_name, class_name, policy))
                    Mapping(self.permmap, class_name, perm_name, create=True)

    def rule_weight(self, rule):
        """
        Get the type enforcement rule's information flow read and write weights.

        Parameter:
        rule            A type enforcement rule.

        Return: Tuple(read_weight, write_weight)
        read_weight     The type enforcement rule's read weight.
        write_weight    The type enforcement rule's write weight.
        """

        write_weight = 0
        read_weight = 0
        class_name = str(rule.tclass)

        if rule.ruletype != 'allow':
            raise exception.RuleTypeError("{0} rules cannot be used for calculating a weight".
                                          format(rule.ruletype))

        # iterate over the permissions and determine the
        # weight of the rule in each direction. The result
        # is the largest-weight permission in each direction
        for perm_name in rule.perms:
            mapping = Mapping(self.permmap, class_name, perm_name)

            if not mapping.enabled:
                continue

            if mapping.direction == "r":
                read_weight = max(read_weight, mapping.weight)
            elif mapping.direction == "w":
                write_weight = max(write_weight, mapping.weight)
            elif mapping.direction == "b":
                read_weight = max(read_weight, mapping.weight)
                write_weight = max(write_weight, mapping.weight)

        return (read_weight, write_weight)

    def set_direction(self, class_, permission, direction):
        """
        Set the information flow direction of a permission.

        Parameter:
        class_              The object class of the permission.
        permission          The permission name.
        direction           The information flow direction the permission (r/w/b/n).

        Exceptions:
        UnmappedClass       The specified object class is not mapped.
        UnmappedPermission  The specified permission is not mapped for the object class.
        """
        Mapping(self.permmap, class_, permission).direction = direction

    def set_weight(self, class_, permission, weight):
        """
        Set the weight of a permission.

        Parameter:
        class_              The object class of the permission.
        permission          The permission name.
        weight              The weight of the permission (1-10).

        Exceptions:
        UnmappedClass       The specified object class is not mapped.
        UnmappedPermission  The specified permission is not mapped for the object class.
        """
        Mapping(self.permmap, class_, permission).weight = weight


#
# Settings Validation Functions
#
def validate_weight(weight):
    if not min_weight <= weight <= max_weight:
        raise ValueError("Permission weights must be 1-10: {0}".format(weight))

    return weight


def validate_direction(direction):
    if direction not in infoflow_directions:
        raise ValueError("Invalid information flow direction: {0}".format(direction))

    return direction


def validate_enabled(enabled):
    return bool(enabled)


class Mapping(object):

    """A mapping for a permission in the permission map."""

    weight = PermissionMapDescriptor("weight", validate_weight)
    direction = PermissionMapDescriptor("direction", validate_direction)
    enabled = PermissionMapDescriptor("enabled", validate_enabled)

    def __init__(self, perm_map, classname, permission, create=False):
        self.perm_map = perm_map
        self.class_ = classname
        self.perm = permission

        if create:
            if classname not in self.perm_map:
                self.perm_map[classname] = OrderedDict()

            self.perm_map[classname][permission] = {'direction': 'u',
                                                    'weight': 1,
                                                    'enabled': True}

        else:
            if classname not in self.perm_map:
                raise exception.UnmappedClass("{0} is not mapped.".format(classname))

            if permission not in self.perm_map[classname]:
                raise exception.UnmappedPermission("{0}:{1} is not mapped.".
                                                   format(classname, permission))

    def __lt__(self, other):
        if self.class_ == other.class_:
            return self.perm < other.perm
        else:
            return self.class_ < other.class_