def parse_modifiers(matches): '''returns audit, deny, allow_keyword and comment from the matches object - audit, deny and allow_keyword are True/False - comment is the comment with a leading space''' audit = False if matches.group('audit'): audit = True deny = False allow_keyword = False allowstr = matches.group('allow') if allowstr: if allowstr.strip() == 'allow': allow_keyword = True elif allowstr.strip() == 'deny': deny = True else: raise AppArmorBug("Invalid allow/deny keyword %s" % allowstr) comment = parse_comment(matches) return (audit, deny, allow_keyword, comment)
def _is_covered_list(self, self_value, self_all, other_value, other_all, cond_name, sanity_check=True): '''check if other_* is covered by self_* - for lists''' if sanity_check and not other_value and not other_all: raise AppArmorBug( 'No %(cond_name)s specified in other %(rule_name)s rule' % { 'cond_name': cond_name, 'rule_name': self.rule_name }) if not self_all: if other_all: return False if not other_value.issubset(self_value): return False # still here? -> then it is covered return True
def __init__(self, path, ifexists, ismagic, audit=False, deny=False, allow_keyword=False, comment='', log_event=None): super(IncludeRule, self).__init__(audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment, log_event=log_event) # include doesn't support audit or deny if audit: raise AppArmorBug('Attempt to initialize %s with audit flag' % self.__class__.__name__) if deny: raise AppArmorBug('Attempt to initialize %s with deny flag' % self.__class__.__name__) if type(ifexists) is not bool: raise AppArmorBug('Passed unknown type for ifexists to %s: %s' % (self.__class__.__name__, ifexists)) if type(ismagic) is not bool: raise AppArmorBug('Passed unknown type for ismagic to %s: %s' % (self.__class__.__name__, ismagic)) if not type_is_str(path): raise AppArmorBug('Passed unknown type for path to %s: %s' % (self.__class__.__name__, path)) if not path: raise AppArmorBug('Passed empty path to %s: %s' % (self.__class__.__name__, path)) self.path = path self.ifexists = ifexists self.ismagic = ismagic
def parse_event_for_tree(self, e): aamode = e.get('aamode', 'UNKNOWN') if aamode == 'UNKNOWN': raise AppArmorBug('aamode is UNKNOWN - %s' % e['type']) # should never happen if aamode in ['AUDIT', 'STATUS', 'ERROR']: return None if 'profile_set' in e['operation']: return None # Skip if AUDIT event was issued due to a change_hat in unconfined mode if not e.get('profile', False): return None # Convert new null profiles to old single level null profile if '//null-' in e['profile']: e['profile'] = 'null-complain-profile' profile = e['profile'] hat = None if '//' in e['profile']: profile, hat = e['profile'].split('//')[:2] # Filter out change_hat events that aren't from learning if e['operation'] == 'change_hat': if aamode != 'HINT' and aamode != 'PERMITTING': return None if e['error_code'] == 1 and e[ 'info'] == 'unconfined can not change_hat': return None profile = e['name2'] #hat = None if '//' in e['name2']: profile, hat = e['name2'].split('//')[:2] if not hat: hat = profile # prog is no longer passed around consistently prog = 'HINT' if profile != 'null-complain-profile' and not self.profile_exists( profile): return None if e['operation'] == 'exec': # convert rmask and dmask to mode arrays e['denied_mask'], e['name2'] = log_str_to_mode( e['profile'], e['denied_mask'], e['name2']) e['request_mask'], e['name2'] = log_str_to_mode( e['profile'], e['request_mask'], e['name2']) if e.get('info', False) and e['info'] == 'mandatory profile missing': return (e['pid'], e['parent'], 'exec', [ profile, hat, aamode, 'PERMITTING', e['denied_mask'], e['name'], e['name2'] ]) elif (e.get('name2', False) and '//null-' in e['name2']) or e.get( 'name', False): return (e['pid'], e['parent'], 'exec', [ profile, hat, prog, aamode, e['denied_mask'], e['name'], '' ]) else: self.debug_logger.debug( 'parse_event_for_tree: dropped exec event in %s' % e['profile']) elif self.op_type(e) == 'file': # Map c (create) and d (delete) to w (logging is more detailed than the profile language) rmask = e['request_mask'] rmask = rmask.replace('c', 'w') rmask = rmask.replace('d', 'w') if not validate_log_mode(hide_log_mode(rmask)): raise AppArmorException( _('Log contains unknown mode %s') % rmask) dmask = e['denied_mask'] dmask = dmask.replace('c', 'w') dmask = dmask.replace('d', 'w') if not validate_log_mode(hide_log_mode(dmask)): raise AppArmorException( _('Log contains unknown mode %s') % dmask) if e.get('ouid') is not None and e['fsuid'] == e['ouid']: # mark as "owner" event if '::' not in rmask: rmask = '%s::' % rmask if '::' not in dmask: dmask = '%s::' % dmask # convert rmask and dmask to mode arrays e['denied_mask'], e['name2'] = log_str_to_mode( e['profile'], dmask, e['name2']) e['request_mask'], e['name2'] = log_str_to_mode( e['profile'], rmask, e['name2']) # check if this is an exec event is_domain_change = False if e['operation'] == 'inode_permission' and ( e['denied_mask'] & AA_MAY_EXEC) and aamode == 'PERMITTING': following = self.peek_at_next_log_entry() if following: entry = self.parse_log_record(following) if entry and entry.get('info', False) == 'set profile': is_domain_change = True self.throw_away_next_log_entry() if is_domain_change: return (e['pid'], e['parent'], 'exec', [ profile, hat, prog, aamode, e['denied_mask'], e['name'], e['name2'] ]) else: return (e['pid'], e['parent'], 'path', [ profile, hat, prog, aamode, e['denied_mask'], e['name'], '' ]) elif e['operation'] == 'capable': return (e['pid'], e['parent'], 'capability', [profile, hat, prog, aamode, e['name'], '']) elif e['operation'] == 'clone': parent, child = e['pid'], e['task'] if not parent: parent = 'null-complain-profile' if not hat: hat = 'null-complain-profile' arrayref = [] if self.pid.get(parent, False): self.pid[parent].append(arrayref) else: self.log.append(arrayref) self.pid[child].append(arrayref) for ia in ['fork', child, profile, hat]: arrayref.append(ia) # if self.pid.get(parent, False): # self.pid[parent] += [arrayref] # else: # self.log += [arrayref] # self.pid[child] = arrayref elif self.op_type(e) == 'net': return (e['pid'], e['parent'], 'netdomain', [ profile, hat, prog, aamode, e['family'], e['sock_type'], e['protocol'] ]) elif e['operation'] == 'change_hat': return (e['pid'], e['parent'], 'unknown_hat', [profile, hat, aamode, hat]) elif e['operation'] == 'ptrace': if not e['peer']: self.debug_logger.debug( 'ignored garbage ptrace event with empty peer') return None if not e['denied_mask']: self.debug_logger.debug( 'ignored garbage ptrace event with empty denied_mask') return None return (e['pid'], e['parent'], 'ptrace', [profile, hat, prog, aamode, e['denied_mask'], e['peer']]) elif e['operation'] == 'signal': return (e['pid'], e['parent'], 'signal', [ profile, hat, prog, aamode, e['denied_mask'], e['signal'], e['peer'] ]) elif e['operation'].startswith('dbus_'): return (e['pid'], e['parent'], 'dbus', [ profile, hat, prog, aamode, e['denied_mask'], e['bus'], e['path'], e['name'], e['interface'], e['member'], e['peer_profile'] ]) else: self.debug_logger.debug('UNHANDLED: %s' % e)
def get(self, key, fallback=None): if key in self.data: return self.data.get(key, fallback) else: raise AppArmorBug('attempt to read unknown key %s' % key)
def __getitem__(self, key): if key in self.data: return self.data[key] else: raise AppArmorBug('attempt to read unknown key %s' % key)
def parse_event_for_tree(self, e): aamode = e.get('aamode', 'UNKNOWN') if aamode == 'UNKNOWN': raise AppArmorBug('aamode is UNKNOWN - %s' % e['type']) # should never happen if aamode in ['AUDIT', 'STATUS', 'ERROR']: return None # Skip if AUDIT event was issued due to a change_hat in unconfined mode if not e.get('profile', False): return None full_profile = e['profile'] # full, nested profile name self.init_hashlog(aamode, full_profile) # Convert new null profiles to old single level null profile if '//null-' in e['profile']: e['profile'] = 'null-complain-profile' profile, hat = split_name(e['profile']) if profile != 'null-complain-profile' and not self.profile_exists( profile): return None if e['operation'] == 'exec': if not e['name']: raise AppArmorException('exec without executed binary') if not e['name2']: e['name2'] = '' # exec events in enforce mode don't have target=... self.hashlog[aamode][full_profile]['exec'][e['name']][ e['name2']] = True return None elif self.op_type(e) == 'file': # Map c (create) and d (delete) to w (logging is more detailed than the profile language) dmask = e['denied_mask'] dmask = dmask.replace('c', 'w') dmask = dmask.replace('d', 'w') owner = False if '::' in dmask: # old log styles used :: to indicate if permissions are meant for owner or other (owner_d, other_d) = dmask.split('::') if owner_d and other_d: raise AppArmorException( 'Found log event with both owner and other permissions. Please open a bugreport!' ) if owner_d: dmask = owner_d owner = True else: dmask = other_d if e.get('ouid') is not None and e['fsuid'] == e['ouid']: # in current log style, owner permissions are indicated by a match of fsuid and ouid owner = True for perm in dmask: if perm in 'mrwalk': # intentionally not allowing 'x' here self.hashlog[aamode][full_profile]['path'][ e['name']][owner][perm] = True else: raise AppArmorException( _('Log contains unknown mode %s') % dmask) return None elif e['operation'] == 'capable': self.hashlog[aamode][full_profile]['capability'][e['name']] = True return None elif self.op_type(e) == 'net': self.hashlog[aamode][full_profile]['network'][e['family']][ e['sock_type']][e['protocol']] = True return None elif e['operation'] == 'change_hat': if e['error_code'] == 1 and e[ 'info'] == 'unconfined can not change_hat': return None self.hashlog[aamode][full_profile]['change_hat'][e['name2']] = True return None elif e['operation'] == 'change_profile': self.hashlog[aamode][full_profile]['change_profile'][ e['name2']] = True return None elif e['operation'] == 'ptrace': if not e['peer']: self.debug_logger.debug( 'ignored garbage ptrace event with empty peer') return None if not e['denied_mask']: self.debug_logger.debug( 'ignored garbage ptrace event with empty denied_mask') return None self.hashlog[aamode][full_profile]['ptrace'][e['peer']][ e['denied_mask']] = True return None elif e['operation'] == 'signal': self.hashlog[aamode][full_profile]['signal'][e['peer']][ e['denied_mask']][e['signal']] = True return None elif e['operation'].startswith('dbus_'): self.hashlog[aamode][full_profile]['dbus'][e['denied_mask']][ e['bus']][e['path']][e['name']][e['interface']][e['member']][ e['peer_profile']] = True return None else: self.debug_logger.debug('UNHANDLED: %s' % e)
def __init__(self, path, perms, exec_perms, target, owner, file_keyword=False, leading_perms=False, audit=False, deny=False, allow_keyword=False, comment='', log_event=None): '''Initialize FileRule Parameters: - path: string, AARE or FileRule.ALL - perms: string, set of chars or FileRule.ALL (must not contain exec mode) - exec_perms: None or string - target: string, AARE or FileRule.ALL - owner: bool - file_keyword: bool - leading_perms: bool ''' super(FileRule, self).__init__(audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment, log_event=log_event) # rulepart partperms is_path log_event self.path, self.all_paths = self._aare_or_all(path, 'path', True, log_event) self.target, self.all_targets, = self._aare_or_all( target, 'target', False, log_event) self.can_glob = not self.all_paths self.can_glob_ext = not self.all_paths self.can_edit = not self.all_paths if type_is_str(perms): perms, tmp_exec_perms = split_perms(perms, deny) if tmp_exec_perms: raise AppArmorBug('perms must not contain exec perms') elif perms == None: perms = set() if perms == {'subset'}: raise AppArmorBug('subset without link permissions given') elif perms in [{'link'}, {'link', 'subset'}]: self.perms = perms self.all_perms = False else: self.perms, self.all_perms, unknown_items = check_and_split_list( perms, file_permissions, FileRule.ALL, 'FileRule', 'permissions', allow_empty_list=True) if unknown_items: raise AppArmorBug('Passed unknown perms to FileRule: %s' % str(unknown_items)) if self.perms and 'a' in self.perms and 'w' in self.perms: raise AppArmorException( "Conflicting permissions found: 'a' and 'w'") self.original_perms = None # might be set by aa-logprof / aa.py propose_file_rules() if exec_perms is None: self.exec_perms = None elif 'link' in self.perms: raise AppArmorBug("link rules can't have execute permissions") elif exec_perms == self.ANY_EXEC: self.exec_perms = exec_perms elif type_is_str(exec_perms): if deny: if exec_perms != 'x': raise AppArmorException( _("file deny rules only allow to use 'x' as execute mode, but not %s" % exec_perms)) else: if exec_perms == 'x': raise AppArmorException( _("Execute flag ('x') in file rule must specify the exec mode (ix, Px, Cx etc.)" )) elif exec_perms not in allow_exec_transitions and exec_perms not in allow_exec_fallback_transitions: raise AppArmorBug( 'Unknown execute mode specified in file rule: %s' % exec_perms) self.exec_perms = exec_perms else: raise AppArmorBug('Passed unknown perms object to FileRule: %s' % str(perms)) if type(owner) is not bool: raise AppArmorBug('non-boolean value passed to owner flag') self.owner = owner self.can_owner = owner # offer '(O)wner permissions on/off' buttons only if the rule has the owner flag if type(file_keyword) is not bool: raise AppArmorBug('non-boolean value passed to file keyword flag') self.file_keyword = file_keyword if type(leading_perms) is not bool: raise AppArmorBug( 'non-boolean value passed to leading permissions flag') self.leading_perms = leading_perms # XXX subset # check for invalid combinations (bare 'file,' vs. path rule) # if (self.all_paths and not self.all_perms) or (not self.all_paths and self.all_perms): # raise AppArmorBug('all_paths and all_perms must be equal') # elif if self.all_paths and (self.exec_perms or self.target): raise AppArmorBug( 'exec perms or target specified for bare file rule')
def edit_header(self): if self.all_paths: raise AppArmorBug('Attemp to edit bare file rule') return (_('Enter new path: '), self.path.regex)
def is_covered_localvars(self, other_rule): '''check if other_rule is covered by this rule object''' if not self._is_covered_aare(self.path, self.all_paths, other_rule.path, other_rule.all_paths, 'path'): return False if self.perms and 'subset' in self.perms and other_rule.perms and 'subset' not in other_rule.perms: return False # subset is a restriction (also, if subset is included, this means this instance is a link rule, so other file permissions can't be covered) elif self.perms and 'link' in self.perms and other_rule.perms and 'link' in other_rule.perms: pass # skip _is_covered_list() because it would interpret 'subset' as additional permissions, not as restriction elif not self._is_covered_list(perms_with_a(self.perms), self.all_perms, perms_with_a(other_rule.perms), other_rule.all_perms, 'perms', sanity_check=False): # perms can be empty if only exec_perms are specified, therefore disable the sanity check in _is_covered_list()... # 'w' covers 'a', therefore use perms_with_a() to temporarily add 'a' if 'w' is present return False # TODO: check link / link subset vs. 'l'? # ... and do our own sanity check if not other_rule.perms and not other_rule.all_perms and not other_rule.exec_perms: raise AppArmorBug( 'No permission or exec permission specified in other file rule' ) if not self.exec_perms and other_rule.exec_perms: return False # TODO: handle fallback modes? if other_rule.exec_perms == self.ANY_EXEC and self.exec_perms: pass # other_rule has ANY_EXEC and self has an exec rule set -> covered, so avoid hitting the 'elif' branch elif other_rule.exec_perms and self.exec_perms != other_rule.exec_perms: return False # check exec_mode and target only if other_rule contains exec_perms (except ANY_EXEC) or link permissions # (for mrwk permissions, the target is ignored anyway) if (other_rule.exec_perms and other_rule.exec_perms != self.ANY_EXEC) or \ (other_rule.perms and 'l' in other_rule.perms) or \ (other_rule.perms and 'link' in other_rule.perms): if not self._is_covered_aare(self.target, self.all_targets, other_rule.target, other_rule.all_targets, 'target'): return False # a different target means running with a different profile, therefore we have to be more strict than _is_covered_aare() # XXX should we enforce an exact match for a) exec and/or b) link target? if self.all_targets != other_rule.all_targets: return False if self.owner and not other_rule.owner: return False # no check for file_keyword and leading_perms - they are not relevant for is_covered() # still here? -> then it is covered return True
def store_edit(self, newpath): if self.all_paths: raise AppArmorBug('Attemp to edit bare file rule') self.path = AARE(newpath, True) # might raise AppArmorException if the new path doesn't start with / or a variable self.raw_rule = None
def validate_edit(self, newpath): if self.all_paths: raise AppArmorBug('Attemp to edit bare file rule') newpath = AARE(newpath, True) # might raise AppArmorException if the new path doesn't start with / or a variable return newpath.match(self.path)
def __init__(self, execmode, execcond, targetprofile, audit=False, deny=False, allow_keyword=False, comment='', log_event=None): ''' CHANGE_PROFILE RULE = 'change_profile' [ [ EXEC MODE ] EXEC COND ] [ -> PROGRAMCHILD ] ''' super(ChangeProfileRule, self).__init__(audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment, log_event=log_event) if execmode: if execmode != 'safe' and execmode != 'unsafe': raise AppArmorBug( 'Unknown exec mode (%s) in change_profile rule' % execmode) elif not execcond or execcond == ChangeProfileRule.ALL: raise AppArmorException( 'Exec condition is required when unsafe or safe keywords are present' ) self.execmode = execmode self.execcond = None self.all_execconds = False if execcond == ChangeProfileRule.ALL: self.all_execconds = True elif type_is_str(execcond): if not execcond.strip(): raise AppArmorBug( 'Empty exec condition in change_profile rule') elif execcond.startswith('/') or execcond.startswith('@'): self.execcond = execcond else: raise AppArmorException( 'Exec condition in change_profile rule does not start with /: %s' % str(execcond)) else: raise AppArmorBug( 'Passed unknown object to ChangeProfileRule: %s' % str(execcond)) self.targetprofile = None self.all_targetprofiles = False if targetprofile == ChangeProfileRule.ALL: self.all_targetprofiles = True elif type_is_str(targetprofile): if targetprofile.strip(): self.targetprofile = targetprofile else: raise AppArmorBug( 'Empty target profile in change_profile rule') else: raise AppArmorBug( 'Passed unknown object to ChangeProfileRule: %s' % str(targetprofile))
def re_match_include_parse(line, rule_name): '''Matches the path for include, include if exists and abi rules rule_name can be 'include' or 'abi' Returns a tuple with - if the "if exists" condition is given - the include/abi path - if the path is a magic path (enclosed in <...>) ''' if rule_name == 'include': matches = RE_INCLUDE.search(line) elif rule_name == 'abi': matches = RE_ABI.search(line) else: raise AppArmorBug( 're_match_include_parse() called with invalid rule name %s' % rule_name) if not matches: return None, None, None path = None ismagic = False if matches.group('magicpath'): path = matches.group('magicpath').strip() ismagic = True elif matches.group('unquotedpath'): path = matches.group('unquotedpath').strip() if re.search('\s', path): raise AppArmorException( _('Syntax error: %s must use quoted path or <...>') % rule_name) # LP: #1738879 - parser doesn't handle unquoted paths everywhere if rule_name == 'include': raise AppArmorException( _('Syntax error: %s must use quoted path or <...>') % rule_name) elif matches.group('quotedpath'): path = matches.group('quotedpath') # LP: 1738880 - parser doesn't handle relative paths everywhere, and # neither do we (see aa.py) if rule_name == 'include' and len(path) > 0 and path[0] != '/': raise AppArmorException( _('Syntax error: %s must use quoted path or <...>') % rule_name) # if path is empty or the empty string if path is None or path == "": raise AppArmorException( _('Syntax error: %s rule with empty filename') % rule_name) # LP: #1738877 - parser doesn't handle files with spaces in the name if rule_name == 'include' and re.search('\s', path): raise AppArmorException( _('Syntax error: %s rule filename cannot contain spaces') % rule_name) ifexists = False if rule_name == 'include' and matches.group('ifexists'): ifexists = True return path, ifexists, ismagic
def get_glob(self, path_or_rule): '''Return the next possible glob. For rlimit rules, that can mean changing the value to 'infinity' ''' # XXX implement all options mentioned above ;-) raise AppArmorBug('get_glob() is not (yet) available for this rule type')