1import 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
543