Nick Kralevich | c762485 | 2014-10-01 11:23:51 -0700 | [diff] [blame] | 1 | import pdb |
| 2 | import re |
| 3 | from xml.etree.ElementTree import Element, SubElement, tostring |
| 4 | |
| 5 | #define equivalents |
| 6 | TYPE = 0 |
| 7 | ATTRIBUTE = 1 |
| 8 | TYPEATTRIBUTE = 2 |
| 9 | CLASS = 3 |
| 10 | COMMON = 4 |
| 11 | ALLOW_RULE = 5 |
| 12 | NEVERALLOW_RULE = 6 |
| 13 | OTHER = 7 |
| 14 | |
| 15 | #define helper methods |
| 16 | # advance_past_whitespace(): helper function to skip whitespace at current |
| 17 | # position in file. |
| 18 | # returns: the non-whitespace character at the file's new position |
| 19 | #TODO: should I deal with comments here as well? |
| 20 | def advance_past_whitespace(file_obj): |
| 21 | c = file_obj.read(1) |
| 22 | while c.isspace(): |
| 23 | c = file_obj.read(1) |
| 24 | file_obj.seek(-1, 1) |
| 25 | return c |
| 26 | |
| 27 | # advance_until_whitespace(): helper function to grab the string represented |
| 28 | # by the current position in file until next whitespace. |
| 29 | # returns: string until next whitespace. overlooks comments. |
| 30 | def advance_until_whitespace(file_obj): |
| 31 | ret_string = "" |
| 32 | c = file_obj.read(1) |
| 33 | #TODO: make a better way to deal with ':' and ';' |
| 34 | while not (c.isspace() or c == ':' or c == '' or c == ';'): |
| 35 | #don't count comments |
| 36 | if c == '#': |
| 37 | file_obj.readline() |
| 38 | return ret_string |
| 39 | else: |
| 40 | ret_string+=c |
| 41 | c = file_obj.read(1) |
| 42 | if not c == ':': |
| 43 | file_obj.seek(-1, 1) |
| 44 | return ret_string |
| 45 | |
| 46 | # expand_avc_rule - takes a processed avc rule and converts it into a list of |
| 47 | # 4-tuples for use in an access check of form: |
| 48 | # (source_type, target_type, class, permission) |
| 49 | def expand_avc_rule(policy, avc_rule): |
| 50 | ret_list = [ ] |
| 51 | |
| 52 | #expand source_types |
| 53 | source_types = avc_rule['source_types']['set'] |
| 54 | source_types = policy.expand_types(source_types) |
| 55 | if(avc_rule['source_types']['flags']['complement']): |
| 56 | #TODO: deal with negated 'self', not present in current policy.conf, though (I think) |
| 57 | source_types = policy.types - source_types #complement these types |
| 58 | if len(source_types) == 0: |
| 59 | print "ERROR: source_types empty after expansion" |
| 60 | print "Before: " |
| 61 | print avc_rule['source_types']['set'] |
| 62 | return |
| 63 | |
| 64 | #expand target_types |
| 65 | target_types = avc_rule['target_types']['set'] |
| 66 | target_types = policy.expand_types(target_types) |
| 67 | if(avc_rule['target_types']['flags']['complement']): |
| 68 | #TODO: deal with negated 'self', not present in current policy.conf, though (I think) |
| 69 | target_types = policy.types - target_types #complement these types |
| 70 | if len(target_types) == 0: |
| 71 | print "ERROR: target_types empty after expansion" |
| 72 | print "Before: " |
| 73 | print avc_rule['target_types']['set'] |
| 74 | return |
| 75 | |
| 76 | # get classes |
| 77 | rule_classes = avc_rule['classes']['set'] |
| 78 | if '' in rule_classes: |
| 79 | print "FOUND EMPTY STRING IN CLASSES" |
| 80 | print "Total sets:" |
| 81 | print avc_rule['source_types']['set'] |
| 82 | print avc_rule['target_types']['set'] |
| 83 | print rule_classes |
| 84 | print avc_rule['permissions']['set'] |
| 85 | |
| 86 | if len(rule_classes) == 0: |
| 87 | print "ERROR: empy set of object classes in avc rule" |
| 88 | return |
| 89 | |
| 90 | # get permissions |
| 91 | permissions = avc_rule['permissions']['set'] |
| 92 | if len(permissions) == 0: |
| 93 | print "ERROR: empy set of permissions in avc rule\n" |
| 94 | return |
| 95 | |
| 96 | #create the list with collosal nesting, n^4 baby! |
| 97 | for s in source_types: |
| 98 | for t in target_types: |
| 99 | for c in rule_classes: |
| 100 | if c == '': |
| 101 | continue |
| 102 | #expand permissions on a per-class basis |
| 103 | exp_permissions = policy.expand_permissions(c, permissions) |
| 104 | if(avc_rule['permissions']['flags']['complement']): |
| 105 | exp_permissions = policy.classes[c] - exp_permissions |
| 106 | if len(exp_permissions) == 0: |
| 107 | print "ERROR: permissions empty after expansion\n" |
| 108 | print "Before: " |
| 109 | print avc_rule['permissions']['set'] |
| 110 | return |
| 111 | for p in exp_permissions: |
| 112 | source = s |
| 113 | if t == 'self': |
| 114 | target = s |
| 115 | else: |
| 116 | target = t |
| 117 | obj_class = c |
| 118 | permission = p |
| 119 | ret_list.append((source, target, obj_class, permission)) |
| 120 | return ret_list |
| 121 | |
| 122 | # expand_avc_rule - takes a processed avc rule and converts it into an xml |
| 123 | # representation with the information needed in a checkSELinuxAccess() call. |
| 124 | # (source_type, target_type, class, permission) |
| 125 | def expand_avc_rule_to_xml(policy, avc_rule, rule_name, rule_type): |
| 126 | rule_xml = Element('avc_rule') |
| 127 | rule_xml.set('name', rule_name) |
| 128 | rule_xml.set('type', rule_type) |
| 129 | |
| 130 | #expand source_types |
| 131 | source_types = avc_rule['source_types']['set'] |
| 132 | source_types = policy.expand_types(source_types) |
| 133 | if(avc_rule['source_types']['flags']['complement']): |
| 134 | #TODO: deal with negated 'self', not present in current policy.conf, though (I think) |
| 135 | source_types = policy.types - source_types #complement these types |
| 136 | if len(source_types) == 0: |
| 137 | print "ERROR: source_types empty after expansion" |
| 138 | print "Before: " |
| 139 | print avc_rule['source_types']['set'] |
| 140 | return |
| 141 | for s in source_types: |
| 142 | elem = SubElement(rule_xml, 'type') |
| 143 | elem.set('type', 'source') |
| 144 | elem.text = s |
| 145 | |
| 146 | #expand target_types |
| 147 | target_types = avc_rule['target_types']['set'] |
| 148 | target_types = policy.expand_types(target_types) |
| 149 | if(avc_rule['target_types']['flags']['complement']): |
| 150 | #TODO: deal with negated 'self', not present in current policy.conf, though (I think) |
| 151 | target_types = policy.types - target_types #complement these types |
| 152 | if len(target_types) == 0: |
| 153 | print "ERROR: target_types empty after expansion" |
| 154 | print "Before: " |
| 155 | print avc_rule['target_types']['set'] |
| 156 | return |
| 157 | for t in target_types: |
| 158 | elem = SubElement(rule_xml, 'type') |
| 159 | elem.set('type', 'target') |
| 160 | elem.text = t |
| 161 | |
| 162 | # get classes |
| 163 | rule_classes = avc_rule['classes']['set'] |
| 164 | |
| 165 | if len(rule_classes) == 0: |
| 166 | print "ERROR: empy set of object classes in avc rule" |
| 167 | return |
| 168 | |
| 169 | # get permissions |
| 170 | permissions = avc_rule['permissions']['set'] |
| 171 | if len(permissions) == 0: |
| 172 | print "ERROR: empy set of permissions in avc rule\n" |
| 173 | return |
| 174 | |
| 175 | # permissions are class-dependent, so bundled together |
| 176 | for c in rule_classes: |
| 177 | if c == '': |
| 178 | print "AH!!! empty class found!\n" |
| 179 | continue |
| 180 | c_elem = SubElement(rule_xml, 'obj_class') |
| 181 | c_elem.set('name', c) |
| 182 | #expand permissions on a per-class basis |
| 183 | exp_permissions = policy.expand_permissions(c, permissions) |
| 184 | if(avc_rule['permissions']['flags']['complement']): |
| 185 | exp_permissions = policy.classes[c] - exp_permissions |
| 186 | if len(exp_permissions) == 0: |
| 187 | print "ERROR: permissions empty after expansion\n" |
| 188 | print "Before: " |
| 189 | print avc_rule['permissions']['set'] |
| 190 | return |
| 191 | |
| 192 | for p in exp_permissions: |
| 193 | p_elem = SubElement(c_elem, 'permission') |
| 194 | p_elem.text = p |
| 195 | |
| 196 | return rule_xml |
| 197 | |
| 198 | # expand_brackets - helper function which reads a file into a string until '{ }'s |
| 199 | # are balanced. Brackets are removed from the string. This function is based |
| 200 | # on the understanding that nested brackets in our policy.conf file occur only due |
| 201 | # to macro expansion, and we just need to know how much is included in a given |
| 202 | # policy sub-component. |
| 203 | def expand_brackets(file_obj): |
| 204 | ret_string = "" |
| 205 | c = file_obj.read(1) |
| 206 | if not c == '{': |
| 207 | print "Invalid bracket expression: " + c + "\n" |
| 208 | file_obj.seek(-1, 1) |
| 209 | return "" |
| 210 | else: |
| 211 | bracket_count = 1 |
| 212 | while bracket_count > 0: |
| 213 | c = file_obj.read(1) |
| 214 | if c == '{': |
| 215 | bracket_count+=1 |
| 216 | elif c == '}': |
| 217 | bracket_count-=1 |
| 218 | elif c == '#': |
| 219 | #get rid of comment and replace with whitespace |
| 220 | file_obj.readline() |
| 221 | ret_string+=' ' |
| 222 | else: |
| 223 | ret_string+=c |
| 224 | return ret_string |
| 225 | |
| 226 | # get_avc_rule_component - grabs the next component from an avc rule. Basically, |
| 227 | # just reads the next word or bracketed set of words. |
| 228 | # returns - a set of the word, or words with metadata |
| 229 | def get_avc_rule_component(file_obj): |
| 230 | ret_dict = { 'flags': {}, 'set': set() } |
| 231 | c = advance_past_whitespace(file_obj) |
| 232 | if c == '~': |
| 233 | ret_dict['flags']['complement'] = True |
| 234 | file_obj.read(1) #move to next char |
| 235 | c = advance_past_whitespace(file_obj) |
| 236 | else: |
| 237 | ret_dict['flags']['complement'] = False |
| 238 | if not c == '{': |
| 239 | #TODO: change operations on file to operations on string? |
| 240 | single_type = advance_until_whitespace(file_obj) |
| 241 | ret_dict['set'].add(single_type) |
| 242 | else: |
| 243 | mult_types = expand_brackets(file_obj) |
| 244 | mult_types = mult_types.split() |
| 245 | for t in mult_types: |
| 246 | ret_dict['set'].add(t) |
| 247 | return ret_dict |
| 248 | |
| 249 | def get_line_type(line): |
| 250 | if re.search(r'^type\s', line): |
| 251 | return TYPE |
| 252 | if re.search(r'^attribute\s', line): |
| 253 | return ATTRIBUTE |
| 254 | if re.search(r'^typeattribute\s', line): |
| 255 | return TYPEATTRIBUTE |
| 256 | if re.search(r'^class\s', line): |
| 257 | return CLASS |
| 258 | if re.search(r'^common\s', line): |
| 259 | return COMMON |
| 260 | if re.search(r'^allow\s', line): |
| 261 | return ALLOW_RULE |
| 262 | if re.search(r'^neverallow\s', line): |
| 263 | return NEVERALLOW_RULE |
| 264 | else: |
| 265 | return OTHER |
| 266 | |
| 267 | def is_multi_line(line_type): |
| 268 | if line_type == CLASS: |
| 269 | return True |
| 270 | elif line_type == COMMON: |
| 271 | return True |
| 272 | elif line_type == ALLOW_RULE: |
| 273 | return True |
| 274 | elif line_type == NEVERALLOW_RULE: |
| 275 | return True |
| 276 | else: |
| 277 | return False |
| 278 | |
| 279 | |
| 280 | #should only be called with file pointing to the 'i' in 'inherits' segment |
| 281 | def process_inherits_segment(file_obj): |
| 282 | inherit_keyword = file_obj.read(8) |
| 283 | if not inherit_keyword == 'inherits': |
| 284 | #TODO: handle error, invalid class statement |
| 285 | print "ERROR: invalid inherits statement" |
| 286 | return |
| 287 | else: |
| 288 | advance_past_whitespace(file_obj) |
| 289 | ret_inherited_common = advance_until_whitespace(file_obj) |
| 290 | return ret_inherited_common |
| 291 | |
| 292 | class SELinuxPolicy: |
| 293 | |
| 294 | def __init__(self): |
| 295 | self.types = set() |
| 296 | self.attributes = { } |
| 297 | self.classes = { } |
| 298 | self.common_classes = { } |
| 299 | self.allow_rules = [ ] |
| 300 | self.neverallow_rules = [ ] |
| 301 | |
| 302 | # create policy directly from policy file |
| 303 | #@classmethod |
| 304 | def from_file_name(self, policy_file_name): |
| 305 | self.types = set() |
| 306 | self.attributes = { } |
| 307 | self.classes = { } |
| 308 | self.common_classes = { } |
| 309 | self.allow_rules = [ ] |
| 310 | self.neverallow_rules = [ ] |
| 311 | with open(policy_file_name, 'r') as policy_file: |
| 312 | line = policy_file.readline() |
| 313 | while line: |
| 314 | line_type = get_line_type(line) |
| 315 | if is_multi_line(line_type): |
| 316 | self.parse_multi_line(line, line_type, policy_file) |
| 317 | else: |
| 318 | self.parse_single_line(line, line_type) |
| 319 | line = policy_file.readline() |
| 320 | |
| 321 | # expand_permissions - generates the actual permission set based on the listed |
| 322 | # permissions with wildcards and the given class on which they're based. |
| 323 | def expand_permissions(self, obj_class, permission_set): |
| 324 | ret_set = set() |
| 325 | neg_set = set() |
| 326 | for p in permission_set: |
| 327 | if p[0] == '-': |
| 328 | real_p = p[1:] |
| 329 | if real_p in self.classes[obj_class]: |
| 330 | neg_set.add(real_p) |
| 331 | else: |
| 332 | print "ERROR: invalid permission in avc rule " + real_t + "\n" |
| 333 | return |
| 334 | else: |
| 335 | if p in self.classes[obj_class]: |
| 336 | ret_set.add(p) |
| 337 | elif p == '*': #pretty sure this can't be negated? eg -* |
| 338 | ret_set |= self.classes[obj_class] #All of the permissions |
| 339 | else: |
| 340 | print "ERROR: invalid permission in avc rule " + p + "\n" |
| 341 | return |
| 342 | return ret_set - neg_set |
| 343 | |
| 344 | # expand_types - generates the actual type set based on the listed types, |
| 345 | # attributes, wildcards and negation. self is left as-is, and is processed |
| 346 | # specially when generating checkAccess() 4-tuples |
| 347 | def expand_types(self, type_set): |
| 348 | ret_set = set() |
| 349 | neg_set = set() |
| 350 | for t in type_set: |
| 351 | if t[0] == '-': |
| 352 | real_t = t[1:] |
| 353 | if real_t in self.attributes: |
| 354 | neg_set |= self.attributes[real_t] |
| 355 | elif real_t in self.types: |
| 356 | neg_set.add(real_t) |
| 357 | elif real_t == 'self': |
| 358 | ret_set |= real_t |
| 359 | else: |
| 360 | print "ERROR: invalid type in avc rule " + real_t + "\nTYPE SET:" |
| 361 | print type_set |
| 362 | return |
| 363 | else: |
| 364 | if t in self.attributes: |
| 365 | ret_set |= self.attributes[t] |
| 366 | elif t in self.types: |
| 367 | ret_set.add(t) |
| 368 | elif t == 'self': |
| 369 | ret_set.add(t) |
| 370 | elif t == '*': #pretty sure this can't be negated? |
| 371 | ret_set |= self.types #All of the types |
| 372 | else: |
| 373 | print "ERROR: invalid type in avc rule " + t + "\nTYPE SET" |
| 374 | print type_set |
| 375 | return |
| 376 | return ret_set - neg_set |
| 377 | |
| 378 | def parse_multi_line(self, line, line_type, file_obj): |
| 379 | if line_type == CLASS: |
| 380 | self.process_class_line(line, file_obj) |
| 381 | elif line_type == COMMON: |
| 382 | self.process_common_line(line, file_obj) |
| 383 | elif line_type == ALLOW_RULE: |
| 384 | self.process_avc_rule_line(line, file_obj) |
| 385 | elif line_type == NEVERALLOW_RULE: |
| 386 | self.process_avc_rule_line(line, file_obj) |
| 387 | else: |
| 388 | print "Error: This is not a multi-line input" |
| 389 | |
| 390 | def parse_single_line(self, line, line_type): |
| 391 | if line_type == TYPE: |
| 392 | self.process_type_line(line) |
| 393 | elif line_type == ATTRIBUTE: |
| 394 | self.process_attribute_line(line) |
| 395 | elif line_type == TYPEATTRIBUTE: |
| 396 | self.process_typeattribute_line(line) |
| 397 | return |
| 398 | |
| 399 | def process_attribute_line(self, line): |
| 400 | match = re.search(r'^attribute\s+(.+);', line) |
| 401 | if match: |
| 402 | declared_attribute = match.group(1) |
| 403 | self.attributes[declared_attribute] = set() |
| 404 | else: |
| 405 | #TODO: handle error? (no state changed) |
| 406 | return |
| 407 | |
| 408 | def process_class_line(self, line, file_obj): |
| 409 | match = re.search(r'^class\s([^\s]+)\s(.*$)', line) |
| 410 | if match: |
| 411 | declared_class = match.group(1) |
| 412 | #first class declaration has no perms |
| 413 | if not declared_class in self.classes: |
| 414 | self.classes[declared_class] = set() |
| 415 | return |
| 416 | else: |
| 417 | #need to parse file from after class name until end of '{ }'s |
| 418 | file_obj.seek(-(len(match.group(2)) + 1), 1) |
| 419 | c = advance_past_whitespace(file_obj) |
| 420 | if not (c == 'i' or c == '{'): |
| 421 | print "ERROR: invalid class statement" |
| 422 | return |
| 423 | elif c == 'i': |
| 424 | #add inherited permissions |
| 425 | inherited = process_inherits_segment(file_obj) |
| 426 | self.classes[declared_class] |= self.common_classes[inherited] |
| 427 | c = advance_past_whitespace(file_obj) |
| 428 | if c == '{': |
| 429 | permissions = expand_brackets(file_obj) |
| 430 | permissions = re.sub(r'#[^\n]*\n','\n' , permissions) #get rid of all comments |
| 431 | permissions = permissions.split() |
| 432 | for p in permissions: |
| 433 | self.classes[declared_class].add(p) |
| 434 | |
| 435 | def process_common_line(self, line, file_obj): |
| 436 | match = re.search(r'^common\s([^\s]+)(.*$)', line) |
| 437 | if match: |
| 438 | declared_common_class = match.group(1) |
| 439 | #TODO: common classes should only be declared once... |
| 440 | if not declared_common_class in self.common_classes: |
| 441 | self.common_classes[declared_common_class] = set() |
| 442 | #need to parse file from after common_class name until end of '{ }'s |
| 443 | file_obj.seek(-(len(match.group(2)) + 1), 1) |
| 444 | c = advance_past_whitespace(file_obj) |
| 445 | if not c == '{': |
| 446 | print "ERROR: invalid common statement" |
| 447 | return |
| 448 | permissions = expand_brackets(file_obj) |
| 449 | permissions = permissions.split() |
| 450 | for p in permissions: |
| 451 | self.common_classes[declared_common_class].add(p) |
| 452 | return |
| 453 | |
| 454 | def process_avc_rule_line(self, line, file_obj): |
| 455 | match = re.search(r'^(never)?allow\s(.*$)', line) |
| 456 | if match: |
| 457 | if(match.group(1)): |
| 458 | rule_type = 'neverallow' |
| 459 | else: |
| 460 | rule_type = 'allow' |
| 461 | #need to parse file from after class name until end of '{ }'s |
| 462 | file_obj.seek(-(len(match.group(2)) + 1), 1) |
| 463 | |
| 464 | #grab source type(s) |
| 465 | source_types = get_avc_rule_component(file_obj) |
| 466 | if len(source_types['set']) == 0: |
| 467 | print "ERROR: no source types for avc rule at line: " + line |
| 468 | return |
| 469 | |
| 470 | #grab target type(s) |
| 471 | target_types = get_avc_rule_component(file_obj) |
| 472 | if len(target_types['set']) == 0: |
| 473 | print "ERROR: no target types for avc rule at line: " + line |
| 474 | return |
| 475 | |
| 476 | #skip ':' potentially already handled by advance_until_whitespace |
| 477 | c = advance_past_whitespace(file_obj) |
| 478 | if c == ':': |
| 479 | file_obj.read(1) |
| 480 | |
| 481 | #grab class(es) |
| 482 | classes = get_avc_rule_component(file_obj) |
| 483 | if len(classes['set']) == 0: |
| 484 | print "ERROR: no classes for avc rule at line: " + line |
| 485 | return |
| 486 | |
| 487 | #grab permission(s) |
| 488 | permissions = get_avc_rule_component(file_obj) |
| 489 | if len(permissions['set']) == 0: |
| 490 | print "ERROR: no permissions for avc rule at line: " + line |
| 491 | return |
| 492 | rule_dict = { |
| 493 | 'source_types': source_types, |
| 494 | 'target_types': target_types, |
| 495 | 'classes': classes, |
| 496 | 'permissions': permissions } |
| 497 | |
| 498 | if rule_type == 'allow': |
| 499 | self.allow_rules.append(rule_dict) |
| 500 | elif rule_type == 'neverallow': |
| 501 | self.neverallow_rules.append(rule_dict) |
| 502 | |
| 503 | def process_type_line(self, line): |
| 504 | #TODO: add support for aliases (not yet in current policy.conf) |
| 505 | match = re.search(r'^type\s([^,]+),?(.*);', line) |
| 506 | if match: |
| 507 | declared_type = match.group(1) |
| 508 | self.types.add(declared_type) |
| 509 | if match.group(2): |
| 510 | declared_attributes = match.group(2) |
| 511 | declared_attributes = declared_attributes.replace(" ", "") #remove whitespace |
| 512 | declared_attributes = declared_attributes.split(',') #separate based on delimiter |
| 513 | for a in declared_attributes: |
| 514 | if not a in self.attributes: |
| 515 | #TODO: hanlde error? attribute should already exist |
| 516 | self.attributes[a] = set() |
| 517 | self.attributes[a].add(declared_type) |
| 518 | else: |
| 519 | #TODO: handle error? (no state changed) |
| 520 | return |
| 521 | |
| 522 | def process_typeattribute_line(self, line): |
| 523 | match = re.search(r'^typeattribute\s([^\s]+)\s(.*);', line) |
| 524 | if match: |
| 525 | declared_type = match.group(1) |
| 526 | if not declared_type in self.types: |
| 527 | #TODO: handle error? type should already exist |
| 528 | self.types.add(declared_type) |
| 529 | if match.group(2): |
| 530 | declared_attributes = match.group(2) |
| 531 | declared_attributes = declared_attributes.replace(" ", "") #remove whitespace |
| 532 | declared_attributes = declared_attributes.split(',') #separate based on delimiter |
| 533 | for a in declared_attributes: |
| 534 | if not a in self.attributes: |
| 535 | #TODO: hanlde error? attribute should already exist |
| 536 | self.attributes[a] = set() |
| 537 | self.attributes[a].add(declared_type) |
| 538 | else: |
| 539 | return |
| 540 | else: |
| 541 | #TODO: handle error? (no state changed) |
| 542 | return |