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