blob: ec12be0ea96ca229a31cd4040f12a450aeefdd0a [file] [log] [blame]
Nick Kralevichc7624852014-10-01 11:23:51 -07001import pdb
2import re
3from xml.etree.ElementTree import Element, SubElement, tostring
4
5#define equivalents
6TYPE = 0
7ATTRIBUTE = 1
8TYPEATTRIBUTE = 2
9CLASS = 3
10COMMON = 4
11ALLOW_RULE = 5
12NEVERALLOW_RULE = 6
13OTHER = 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?
20def 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.
30def 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)
49def 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)
125def 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.
203def 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
229def 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
249def 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
267def 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
281def 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
292class 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