Home | History | Annotate | Download | only in setools
      1 # Copyright 2014-2015, Tresys Technology, LLC
      2 #
      3 # This file is part of SETools.
      4 #
      5 # SETools is free software: you can redistribute it and/or modify
      6 # it under the terms of the GNU Lesser General Public License as
      7 # published by the Free Software Foundation, either version 2.1 of
      8 # the License, or (at your option) any later version.
      9 #
     10 # SETools is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU Lesser General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU Lesser General Public
     16 # License along with SETools.  If not, see
     17 # <http://www.gnu.org/licenses/>.
     18 #
     19 import sys
     20 import logging
     21 import copy
     22 from collections import OrderedDict
     23 from errno import ENOENT
     24 
     25 from . import exception
     26 from . import policyrep
     27 from .descriptors import PermissionMapDescriptor
     28 
     29 infoflow_directions = ["r", "w", "b", "n", "u"]
     30 min_weight = 1
     31 max_weight = 10
     32 
     33 
     34 class PermissionMap(object):
     35 
     36     """Permission Map for information flow analysis."""
     37 
     38     def __init__(self, permmapfile=None):
     39         """
     40         Parameter:
     41         permmapfile     The path to the permission map to load.
     42         """
     43         self.log = logging.getLogger(__name__)
     44         self.permmap = OrderedDict()
     45         self.permmapfile = None
     46 
     47         if permmapfile:
     48             self.load(permmapfile)
     49         else:
     50             for path in ["data/", sys.prefix + "/share/setools/"]:
     51                 try:
     52                     self.load(path + "perm_map")
     53                     break
     54                 except (IOError, OSError) as err:
     55                     if err.errno != ENOENT:
     56                         raise
     57             else:
     58                 raise RuntimeError("Unable to load default permission map.")
     59 
     60     def __str__(self):
     61         return self.permmapfile
     62 
     63     def __deepcopy__(self, memo):
     64         newobj = PermissionMap.__new__(PermissionMap)
     65         newobj.log = self.log
     66         newobj.permmap = copy.deepcopy(self.permmap)
     67         newobj.permmapfile = self.permmapfile
     68         memo[id(self)] = newobj
     69         return newobj
     70 
     71     def load(self, permmapfile):
     72         """
     73         Parameter:
     74         permmapfile     The path to the permission map to load.
     75         """
     76         self.log.info("Opening permission map \"{0}\"".format(permmapfile))
     77 
     78         # state machine
     79         # 1 = read number of classes
     80         # 2 = read class name and number of perms
     81         # 3 = read perms
     82         with open(permmapfile, "r") as mapfile:
     83             total_perms = 0
     84             class_count = 0
     85             num_classes = 0
     86             state = 1
     87 
     88             self.permmap.clear()
     89 
     90             for line_num, line in enumerate(mapfile, start=1):
     91                 entry = line.split()
     92 
     93                 if len(entry) == 0 or entry[0][0] == '#':
     94                     continue
     95 
     96                 if state == 1:
     97                     try:
     98                         num_classes = int(entry[0])
     99                     except ValueError:
    100                         raise exception.PermissionMapParseError(
    101                             "{0}:{1}:Invalid number of classes: {2}".
    102                             format(permmapfile, line_num, entry[0]))
    103 
    104                     if num_classes < 1:
    105                         raise exception.PermissionMapParseError(
    106                             "{0}:{1}:Number of classes must be positive: {2}".
    107                             format(permmapfile, line_num, entry[0]))
    108 
    109                     state = 2
    110 
    111                 elif state == 2:
    112                     if len(entry) != 3 or entry[0] != "class":
    113                         raise exception.PermissionMapParseError(
    114                             "{0}:{1}:Invalid class declaration: {2}".
    115                             format(permmapfile, line_num, entry))
    116 
    117                     class_name = str(entry[1])
    118 
    119                     try:
    120                         num_perms = int(entry[2])
    121                     except ValueError:
    122                         raise exception.PermissionMapParseError(
    123                             "{0}:{1}:Invalid number of permissions: {2}".
    124                             format(permmapfile, line_num, entry[2]))
    125 
    126                     if num_perms < 1:
    127                         raise exception.PermissionMapParseError(
    128                             "{0}:{1}:Number of permissions must be positive: {2}".
    129                             format(permmapfile, line_num, entry[2]))
    130 
    131                     class_count += 1
    132                     if class_count > num_classes:
    133                         raise exception.PermissionMapParseError(
    134                             "{0}:{1}:Extra class found: {2}".
    135                             format(permmapfile, line_num, class_name))
    136 
    137                     self.permmap[class_name] = OrderedDict()
    138                     perm_count = 0
    139                     state = 3
    140 
    141                 elif state == 3:
    142                     perm_name = str(entry[0])
    143 
    144                     flow_direction = str(entry[1])
    145                     if flow_direction not in infoflow_directions:
    146                         raise exception.PermissionMapParseError(
    147                             "{0}:{1}:Invalid information flow direction: {2}".
    148                             format(permmapfile, line_num, entry[1]))
    149 
    150                     try:
    151                         weight = int(entry[2])
    152                     except ValueError:
    153                         raise exception.PermissionMapParseError(
    154                             "{0}:{1}:Invalid permission weight: {2}".
    155                             format(permmapfile, line_num, entry[2]))
    156 
    157                     if not min_weight <= weight <= max_weight:
    158                         raise exception.PermissionMapParseError(
    159                             "{0}:{1}:Permission weight must be {3}-{4}: {2}".
    160                             format(permmapfile, line_num, entry[2],
    161                                    min_weight, max_weight))
    162 
    163                     self.log.debug("Read {0}:{1} {2} {3}".format(
    164                                    class_name, perm_name, flow_direction, weight))
    165 
    166                     if flow_direction == 'u':
    167                         self.log.info("Permission {0}:{1} is unmapped.".format(
    168                                       class_name, perm_name))
    169 
    170                     mapping = Mapping(self.permmap, class_name, perm_name, create=True)
    171                     mapping.direction = flow_direction
    172                     mapping.weight = weight
    173 
    174                     total_perms += 1
    175                     perm_count += 1
    176                     if perm_count >= num_perms:
    177                         state = 2
    178 
    179         self.permmapfile = permmapfile
    180         self.log.info("Successfully opened permission map \"{0}\"".format(permmapfile))
    181         self.log.debug("Read {0} classes and {1} total permissions.".format(
    182                        class_count, total_perms))
    183 
    184     def save(self, permmapfile):
    185         """
    186         Save the permission map to the specified path.  Existing files
    187         will be overwritten.
    188 
    189         Parameter:
    190         permmapfile         The path to write the permission map.
    191         """
    192         with open(permmapfile, "w") as mapfile:
    193             self.log.info("Writing permission map to \"{0}\"".format(permmapfile))
    194             mapfile.write("{0}\n\n".format(len(self.permmap)))
    195 
    196             for classname, perms in self.permmap.items():
    197                 mapfile.write("class {0} {1}\n".format(classname, len(perms)))
    198 
    199                 for permname, settings in perms.items():
    200                     direction = settings['direction']
    201                     weight = settings['weight']
    202 
    203                     assert min_weight <= weight <= max_weight, \
    204                         "{0}:{1} weight is out of range ({2}). This is an SETools bug.".format(
    205                             classname, permname, weight)
    206 
    207                     assert direction in infoflow_directions, \
    208                         "{0}:{1} flow direction ({2}) is invalid. This is an SETools bug.".format(
    209                             classname, permname, direction)
    210 
    211                     if direction == 'u':
    212                         self.log.warning("Warning: permission {0} in class {1} is unmapped.".format(
    213                                          permname, classname))
    214 
    215                     mapfile.write("{0:>20} {1:>9} {2:>9}\n".format(permname, direction, weight))
    216 
    217                 mapfile.write("\n")
    218 
    219             self.log.info("Successfully wrote permission map to \"{0}\"".format(permmapfile))
    220 
    221     def classes(self):
    222         """
    223         Generate class names in the permission map.
    224 
    225         Yield:
    226         class       An object class name.
    227         """
    228         for cls in self.permmap.keys():
    229             yield cls
    230 
    231     def perms(self, class_):
    232         """
    233         Generate permission mappings for the specified class.
    234 
    235         Parameter:
    236         class_      An object class name.
    237 
    238         Yield:
    239         Mapping     A permission's complete map (weight, direction, enabled)
    240         """
    241         try:
    242             for perm in self.permmap[class_].keys():
    243                 yield Mapping(self.permmap, class_, perm)
    244         except KeyError:
    245             raise exception.UnmappedClass("{0} is not mapped.".format(class_))
    246 
    247     def mapping(self, class_, perm):
    248         """Retrieve a specific permission's mapping."""
    249         return Mapping(self.permmap, class_, perm)
    250 
    251     def exclude_class(self, class_):
    252         """
    253         Exclude all permissions in an object class for calculating rule weights.
    254 
    255         Parameter:
    256         class_              The object class to exclude.
    257 
    258         Exceptions:
    259         UnmappedClass       The specified object class is not mapped.
    260         """
    261         for perm in self.perms(class_):
    262             perm.enabled = False
    263 
    264     def exclude_permission(self, class_, permission):
    265         """
    266         Exclude a permission for calculating rule weights.
    267 
    268         Parameter:
    269         class_              The object class of the permission.
    270         permission          The permission name to exclude.
    271 
    272         Exceptions:
    273         UnmappedClass       The specified object class is not mapped.
    274         UnmappedPermission  The specified permission is not mapped for the object class.
    275         """
    276         Mapping(self.permmap, class_, permission).enabled = False
    277 
    278     def include_class(self, class_):
    279         """
    280         Include all permissions in an object class for calculating rule weights.
    281 
    282         Parameter:
    283         class_              The object class to include.
    284 
    285         Exceptions:
    286         UnmappedClass       The specified object class is not mapped.
    287         """
    288 
    289         for perm in self.perms(class_):
    290             perm.enabled = True
    291 
    292     def include_permission(self, class_, permission):
    293         """
    294         Include a permission for calculating rule weights.
    295 
    296         Parameter:
    297         class_              The object class of the permission.
    298         permission          The permission name to include.
    299 
    300         Exceptions:
    301         UnmappedClass       The specified object class is not mapped.
    302         UnmappedPermission  The specified permission is not mapped for the object class.
    303         """
    304 
    305         Mapping(self.permmap, class_, permission).enabled = True
    306 
    307     def map_policy(self, policy):
    308         """Create mappings for all classes and permissions in the specified policy."""
    309         for class_ in policy.classes():
    310             class_name = str(class_)
    311 
    312             if class_name not in self.permmap:
    313                 self.log.debug("Adding unmapped class {0} from {1}".format(class_name, policy))
    314                 self.permmap[class_name] = OrderedDict()
    315 
    316             perms = class_.perms
    317 
    318             try:
    319                 perms |= class_.common.perms
    320             except policyrep.exception.NoCommon:
    321                 pass
    322 
    323             for perm_name in perms:
    324                 if perm_name not in self.permmap[class_name]:
    325                     self.log.debug("Adding unmapped permission {0} in {1} from {2}".
    326                                    format(perm_name, class_name, policy))
    327                     Mapping(self.permmap, class_name, perm_name, create=True)
    328 
    329     def rule_weight(self, rule):
    330         """
    331         Get the type enforcement rule's information flow read and write weights.
    332 
    333         Parameter:
    334         rule            A type enforcement rule.
    335 
    336         Return: Tuple(read_weight, write_weight)
    337         read_weight     The type enforcement rule's read weight.
    338         write_weight    The type enforcement rule's write weight.
    339         """
    340 
    341         write_weight = 0
    342         read_weight = 0
    343         class_name = str(rule.tclass)
    344 
    345         if rule.ruletype != 'allow':
    346             raise exception.RuleTypeError("{0} rules cannot be used for calculating a weight".
    347                                           format(rule.ruletype))
    348 
    349         # iterate over the permissions and determine the
    350         # weight of the rule in each direction. The result
    351         # is the largest-weight permission in each direction
    352         for perm_name in rule.perms:
    353             mapping = Mapping(self.permmap, class_name, perm_name)
    354 
    355             if not mapping.enabled:
    356                 continue
    357 
    358             if mapping.direction == "r":
    359                 read_weight = max(read_weight, mapping.weight)
    360             elif mapping.direction == "w":
    361                 write_weight = max(write_weight, mapping.weight)
    362             elif mapping.direction == "b":
    363                 read_weight = max(read_weight, mapping.weight)
    364                 write_weight = max(write_weight, mapping.weight)
    365 
    366         return (read_weight, write_weight)
    367 
    368     def set_direction(self, class_, permission, direction):
    369         """
    370         Set the information flow direction of a permission.
    371 
    372         Parameter:
    373         class_              The object class of the permission.
    374         permission          The permission name.
    375         direction           The information flow direction the permission (r/w/b/n).
    376 
    377         Exceptions:
    378         UnmappedClass       The specified object class is not mapped.
    379         UnmappedPermission  The specified permission is not mapped for the object class.
    380         """
    381         Mapping(self.permmap, class_, permission).direction = direction
    382 
    383     def set_weight(self, class_, permission, weight):
    384         """
    385         Set the weight of a permission.
    386 
    387         Parameter:
    388         class_              The object class of the permission.
    389         permission          The permission name.
    390         weight              The weight of the permission (1-10).
    391 
    392         Exceptions:
    393         UnmappedClass       The specified object class is not mapped.
    394         UnmappedPermission  The specified permission is not mapped for the object class.
    395         """
    396         Mapping(self.permmap, class_, permission).weight = weight
    397 
    398 
    399 #
    400 # Settings Validation Functions
    401 #
    402 def validate_weight(weight):
    403     if not min_weight <= weight <= max_weight:
    404         raise ValueError("Permission weights must be 1-10: {0}".format(weight))
    405 
    406     return weight
    407 
    408 
    409 def validate_direction(direction):
    410     if direction not in infoflow_directions:
    411         raise ValueError("Invalid information flow direction: {0}".format(direction))
    412 
    413     return direction
    414 
    415 
    416 def validate_enabled(enabled):
    417     return bool(enabled)
    418 
    419 
    420 class Mapping(object):
    421 
    422     """A mapping for a permission in the permission map."""
    423 
    424     weight = PermissionMapDescriptor("weight", validate_weight)
    425     direction = PermissionMapDescriptor("direction", validate_direction)
    426     enabled = PermissionMapDescriptor("enabled", validate_enabled)
    427 
    428     def __init__(self, perm_map, classname, permission, create=False):
    429         self.perm_map = perm_map
    430         self.class_ = classname
    431         self.perm = permission
    432 
    433         if create:
    434             if classname not in self.perm_map:
    435                 self.perm_map[classname] = OrderedDict()
    436 
    437             self.perm_map[classname][permission] = {'direction': 'u',
    438                                                     'weight': 1,
    439                                                     'enabled': True}
    440 
    441         else:
    442             if classname not in self.perm_map:
    443                 raise exception.UnmappedClass("{0} is not mapped.".format(classname))
    444 
    445             if permission not in self.perm_map[classname]:
    446                 raise exception.UnmappedPermission("{0}:{1} is not mapped.".
    447                                                    format(classname, permission))
    448 
    449     def __lt__(self, other):
    450         if self.class_ == other.class_:
    451             return self.perm < other.perm
    452         else:
    453             return self.class_ < other.class_
    454