def load_variables(self, prof_path): """Loads the variables for the given profile""" if os.path.isfile(prof_path): with open_file_read(prof_path) as f_in: for line in f_in: line = line.strip() # If any includes, load variables from them first match = re_match_include(line) if match: new_path = self.PROF_DIR + '/' + match self.load_variables(new_path) else: # Remove any comments if '#' in line: line = line.split('#')[0].rstrip() # Expected format is @{Variable} = value1 value2 .. if line.startswith('@') and '=' in line: if '+=' in line: line = line.split('+=') try: self.severity['VARIABLES'][line[0]] += [i.strip('"') for i in line[1].split()] except KeyError: raise AppArmorException("Variable %s was not previously declared, but is being assigned additional value in file: %s" % (line[0], prof_path)) else: line = line.split('=') if line[0] in self.severity['VARIABLES'].keys(): raise AppArmorException("Variable %s was previously declared in file: %s" % (line[0], prof_path)) self.severity['VARIABLES'][line[0]] = [i.strip('"') for i in line[1].split()]
def _run_test(self, params, expected): with open_file_read(params['file']) as f_in: data = f_in.readlines() if params['disabled']: # skip disabled testcases return if params['tools_wrong']: # if the tools are marked as being wrong about a profile, expect the opposite result # this makes sure we notice any behaviour change, especially not being wrong anymore expected = not expected # make sure the profile is known in active_profiles.files apparmor.active_profiles.init_file(params['file']) if expected: apparmor.parse_profile_data(data, params['file'], 0) apparmor.active_profiles.get_all_merged_variables( params['file'], apparmor.include_list_recursive( apparmor.active_profiles.files[params['file']])) else: with self.assertRaises(AppArmorException): apparmor.parse_profile_data(data, params['file'], 0) apparmor.active_profiles.get_all_merged_variables( params['file'], apparmor.include_list_recursive( apparmor.active_profiles.files[params['file']]))
def _raw_transform(fin, fout): '''Copy input file to output file''' if fin.endswith('.profile'): name = os.path.basename(os.path.splitext(fin)[0]) else: name = os.path.basename(fin) orig = open_file_read(fin).read() out = re.sub(r'###PROFILEATTACH###', 'profile "%s"' % name, orig) tmp = name.split("_") if len(tmp) != 3: raise AppArmorException("unable to parse profile name %s" % (name)) (package, appname, version) = tuple(tmp) profile_vars = '''@{CLICK_DIR}="%s" @{APP_PKGNAME}="%s" @{APP_APPNAME}="%s" @{APP_VERSION}="%s"''' % (_get_click_dir_variable(), package, appname, version) out = re.sub(r'###VAR###', profile_vars, out) tmp, tmp_fn = tempfile.mkstemp(prefix='aa-profile-hook') if not isinstance(out, bytes): out = out.encode('utf-8') os.write(tmp, out) os.close(tmp) shutil.move(tmp_fn, fout) os.chmod(fout, 0o644)
def read_log(self, logmark): self.logmark = logmark seenmark = True if self.logmark: seenmark = False #last = None #event_type = None try: #print(self.filename) self.LOG = open_file_read(self.filename) except IOError: raise AppArmorException('Can not read AppArmor logfile: ' + self.filename) #LOG = open_file_read(log_open) line = True while line: line = self.get_next_log_entry() if not line: break line = line.strip() self.debug_logger.debug('read_log: %s' % line) if self.logmark in line: seenmark = True self.debug_logger.debug('read_log: seenmark = %s' % seenmark) if not seenmark: continue event = self.parse_log_record(line) #print(event) if event: self.add_event_to_tree(event) self.LOG.close() self.logmark = '' return self.log
def __init__(self, dbname=None, default_rank=10): """Initialises the class object""" self.PROF_DIR = '/etc/apparmor.d' # The profile directory self.NOT_IMPLEMENTED = '_-*not*implemented*-_' # used for rule types that don't have severity ratings self.severity = dict() self.severity['DATABASENAME'] = dbname self.severity['CAPABILITIES'] = {} self.severity['FILES'] = {} self.severity['REGEXPS'] = {} self.severity['DEFAULT_RANK'] = default_rank # For variable expansions for the profile self.severity['VARIABLES'] = dict() if not dbname: raise AppArmorException("No severity db file given") with open_file_read(dbname) as database: # open(dbname, 'r') for lineno, line in enumerate(database, start=1): line = line.strip() # or only rstrip and lstrip? if line == '' or line.startswith('#'): continue if line.startswith('/'): try: path, read, write, execute = line.split() read, write, execute = int(read), int(write), int(execute) except ValueError: raise AppArmorException("Insufficient values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) else: if read not in range(0, 11) or write not in range(0, 11) or execute not in range(0, 11): raise AppArmorException("Inappropriate values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) path = path.lstrip('/') if '*' not in path: self.severity['FILES'][path] = {'r': read, 'w': write, 'x': execute} else: ptr = self.severity['REGEXPS'] pieces = path.split('/') for index, piece in enumerate(pieces): if '*' in piece: path = '/'.join(pieces[index:]) regexp = convert_regexp(path) ptr[regexp] = {'AA_RANK': {'r': read, 'w': write, 'x': execute}} break else: ptr[piece] = ptr.get(piece, {}) ptr = ptr[piece] elif line.startswith('CAP_'): try: resource, severity = line.split() severity = int(severity) except ValueError: error_message = 'No severity value present in file: %s\n\t[Line %s]: %s' % (dbname, lineno, line) #error(error_message) raise AppArmorException(error_message) # from None else: if severity not in range(0, 11): raise AppArmorException("Inappropriate severity value present in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) self.severity['CAPABILITIES'][resource] = severity else: raise AppArmorException("Unexpected line in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
def py2_parser(filename): """Returns the de-dented ini file from the new format ini""" tmp = tempfile.NamedTemporaryFile('rw') f_out = open(tmp.name, 'w') if os.path.exists(filename): with open_file_read(filename) as f_in: for line in f_in: # The ini format allows for multi-line entries, with the subsequent # entries being indented deeper hence simple lstrip() is not appropriate if line[:2] == ' ': line = line[2:] elif line[0] == '\t': line = line[1:] f_out.write(line) f_out.flush() return tmp
def read_log(self, logmark): self.logmark = logmark seenmark = True if self.logmark: seenmark = False #last = None #event_type = None try: #print(self.filename) self.LOG = open_file_read(self.filename) except IOError: raise AppArmorException('Can not read AppArmor logfile: ' + self.filename) #LOG = open_file_read(log_open) line = True while line: line = self.get_next_log_entry() if not line: break line = line.strip() self.debug_logger.debug('read_log: %s' % line) if self.logmark in line: seenmark = True self.debug_logger.debug('read_log: seenmark = %s' % seenmark) if not seenmark: continue event = self.parse_log_record(line) #print(event) if event: try: self.add_event_to_tree(event) except AppArmorException as e: ex_msg = ( '%(msg)s\n\nThis error was caused by the log line:\n%(logline)s' % { 'msg': e.value, 'logline': line }) # when py3 only: Drop the original AppArmorException by passing None as the parent exception raise AppArmorBug(ex_msg) # py3-only: from None self.LOG.close() self.logmark = '' return self.log
def read_shell(self, filepath): """Reads the shell type conf files and returns config[''][option]=value""" config = {'': dict()} with open_file_read(filepath) as conf_file: for line in conf_file: result = shlex.split(line, True) # If not a comment of empty line if result: # option="value" or option=value type if '=' in result[0]: option, value = result[0].split('=') # option type else: option = result[0] value = None config[''][option] = value return config
def read_shell(self, filepath): """Reads the shell type conf files and returns config[''][option]=value""" # @TODO: Use standard ConfigParser when https://bugs.python.org/issue22253 is fixed config = {'': dict()} with open_file_read(filepath) as conf_file: for line in conf_file: result = shlex.split(line, True) # If not a comment of empty line if result: # option="value" or option=value type if '=' in result[0]: option, value = result[0].split('=') # option type else: option = result[0] value = None config[''][option] = value return config
def _run_test(self, params, expected): with open_file_read(params['file']) as f_in: data = f_in.readlines() if params['disabled']: # skip disabled testcases return if params['tools_wrong']: # if the tools are marked as being wrong about a profile, expect the opposite result # this makes sure we notice any behaviour change, especially not being wrong anymore expected = not expected if expected: apparmor.parse_profile_data(data, params['file'], 0) else: with self.assertRaises(AppArmorException): apparmor.parse_profile_data(data, params['file'], 0)
def _parse_libapparmor_test_multi(self, file_with_path): '''parse the libapparmor test_multi *.in tests and their expected result in *.out''' with open_file_read('%s.out' % file_with_path) as f_in: expected = f_in.readlines() if expected[0].rstrip('\n') != 'START': raise Exception( "%s.out doesn't have 'START' in its first line! (%s)" % (file_with_path, expected[0])) expected.pop(0) exresult = dict() for line in expected: label, value = line.split(':', 1) # test_multi doesn't always use the original labels :-/ if label in self.label_map.keys(): label = self.label_map[label] label = label.replace(' ', '_').lower() exresult[label] = value.strip() if not exresult['event_type'].startswith('AA_RECORD_'): raise Exception( "event_type doesn't start with AA_RECORD_: %s in file %s" % (exresult['event_type'], file_with_path)) exresult['aamode'] = exresult['event_type'].replace('AA_RECORD_', '') if exresult['aamode'] == 'ALLOWED': exresult['aamode'] = 'PERMITTING' if exresult['aamode'] == 'DENIED': exresult['aamode'] = 'REJECTING' if exresult[ 'event_type'] == 'AA_RECORD_INVALID': # or exresult.get('error_code', 0) != 0: # XXX should events with errors be ignored? exresult = None return exresult
def load_variables(self, prof_path): """Loads the variables for the given profile""" if os.path.isfile(prof_path): with open_file_read(prof_path) as f_in: for line in f_in: line = line.strip() # If any includes, load variables from them first match = re_match_include(line) if match: new_path = match if not new_path.startswith('/'): new_path = self.PROF_DIR + '/' + match self.load_variables(new_path) else: # Remove any comments if '#' in line: line = line.split('#')[0].rstrip() # Expected format is @{Variable} = value1 value2 .. if line.startswith('@') and '=' in line: if '+=' in line: line = line.split('+=') try: self.severity['VARIABLES'][line[0]] += [ i.strip('"') for i in line[1].split() ] except KeyError: raise AppArmorException( "Variable %s was not previously declared, but is being assigned additional value in file: %s" % (line[0], prof_path)) else: line = line.split('=') if line[0] in self.severity['VARIABLES'].keys( ): raise AppArmorException( "Variable %s was previously declared in file: %s" % (line[0], prof_path)) self.severity['VARIABLES'][line[0]] = [ i.strip('"') for i in line[1].split() ]
def _run_test(self, params, expected): # tests[][expected] is a dummy, replace it with the real values if params.split('/')[-1] in log_to_skip: return expected = self._parse_libapparmor_test_multi(params) with open_file_read('%s.in' % params) as f_in: loglines = f_in.readlines() loglines2 = [] for line in loglines: if line.strip(): loglines2 += [line] self.assertEqual(len(loglines2), 1, '%s.in should only contain one line!' % params) parser = ReadLog('', '', '') parsed_event = parser.parse_event(loglines2[0]) if parsed_event and expected: parsed_items = dict(parsed_event.items()) # check if the line passes the regex in logparser.py if not parser.RE_LOG_ALL.search(loglines2[0]): raise Exception("Log event doesn't match RE_LOG_ALL") for label in expected: if label in [ 'file', # filename of the *.in file 'event_type', # mapped to aamode 'audit_id', 'audit_sub_id', # not set nor relevant 'comm', # not set, and not too useful # XXX most of the keywords listed below mean "TODO" 'fsuid', 'ouid', # file events 'flags', 'fs_type', # mount 'namespace', # file_lock only?? (at least the tests don't contain this in other event types with namespace) 'net_local_addr', 'net_foreign_addr', 'net_local_port', 'net_foreign_port', # detailed network events 'peer', 'signal', # signal 'src_name', # pivotroot 'dbus_bus', 'dbus_interface', 'dbus_member', 'dbus_path', # dbus 'peer_pid', 'peer_profile', # dbus ]: pass elif parsed_items['operation'] == 'exec' and label in [ 'sock_type', 'family', 'protocol' ]: pass # XXX 'exec' + network? really? elif parsed_items[ 'operation'] == 'ptrace' and label == 'name2' and params.endswith( '/ptrace_garbage_lp1689667_1'): pass # libapparmor would better qualify this case as invalid event elif not parsed_items.get(label, None): raise Exception('parsed_items[%s] not set' % label) elif not expected.get(label, None): raise Exception('expected[%s] not set' % label) else: self.assertEqual(str(parsed_items[label]), expected[label], '%s differs' % label) elif expected: self.assertIsNone(parsed_event) # that's why we end up here self.assertEqual(dict(), expected, 'parsed_event is none' ) # effectively print the content of expected elif parsed_event: self.assertIsNone(expected) # that's why we end up here self.assertEqual(parsed_event, dict(), 'expected is none' ) # effectively print the content of parsed_event else: self.assertIsNone(expected) # that's why we end up here self.assertIsNone(parsed_event) # that's why we end up here self.assertEqual(parsed_event, expected) # both are None
def parse_test_profiles(file_with_path): '''parse the test-related headers of a profile (for example EXRESULT) and add the profile to the set of tests''' exresult = None exresult_found = False description = None todo = False disabled = False with open_file_read(file_with_path) as f_in: data = f_in.readlines() relfile = os.path.relpath(file_with_path, apparmor.profile_dir) for line in data: if line.startswith('#=EXRESULT '): exresult = line.split()[1] if exresult == 'PASS': exresult == True exresult_found = True elif exresult == 'FAIL': exresult = False exresult_found = True else: raise Exception('%s contains unknown EXRESULT %s' % (file_with_path, exresult)) elif line.upper().startswith('#=DESCRIPTION '): description = line.split()[1] elif line.rstrip() == '#=TODO': todo = True elif line.rstrip() == '#=DISABLED': disabled = True if not exresult_found: raise Exception('%s does not contain EXRESULT' % file_with_path) if not description: raise Exception('%s does not contain description' % file_with_path) tools_wrong = False if relfile in exception_not_raised: if exresult: raise Exception( "%s listed in exception_not_raised, but has EXRESULT PASS" % file_with_path) tools_wrong = 'EXCEPTION_NOT_RAISED' elif relfile.startswith(skip_startswith): return 1 # XXX *** SKIP *** those tests elif relfile in unknown_line: if not exresult: raise Exception( "%s listed in unknown_line, but has EXRESULT FAIL" % file_with_path) tools_wrong = 'UNKNOWN_LINE' elif relfile in syntax_failure: if not exresult: raise Exception( "%s listed in syntax_failure, but has EXRESULT FAIL" % file_with_path) tools_wrong = 'SYNTAX_FAILURE' params = { 'file': file_with_path, 'relfile': relfile, 'todo': todo, 'disabled': disabled, 'tools_wrong': tools_wrong, 'exresult': exresult, } TestParseParserTests.tests.append((params, exresult)) return 0
def __init__(self, dbname=None, default_rank=10): """Initialises the class object""" self.PROF_DIR = '/etc/apparmor.d' # The profile directory self.NOT_IMPLEMENTED = '_-*not*implemented*-_' # used for rule types that don't have severity ratings self.severity = dict() self.severity['DATABASENAME'] = dbname self.severity['CAPABILITIES'] = {} self.severity['FILES'] = {} self.severity['REGEXPS'] = {} self.severity['DEFAULT_RANK'] = default_rank # For variable expansions for the profile self.severity['VARIABLES'] = dict() if not dbname: raise AppArmorException("No severity db file given") with open_file_read(dbname) as database: # open(dbname, 'r') for lineno, line in enumerate(database, start=1): line = line.strip() # or only rstrip and lstrip? if line == '' or line.startswith('#'): continue if line.startswith('/'): try: path, read, write, execute = line.split() read, write, execute = int(read), int(write), int( execute) except ValueError: raise AppArmorException( "Insufficient values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) else: if read not in range(0, 11) or write not in range( 0, 11) or execute not in range(0, 11): raise AppArmorException( "Inappropriate values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) path = path.lstrip('/') if '*' not in path: self.severity['FILES'][path] = { 'r': read, 'w': write, 'x': execute } else: ptr = self.severity['REGEXPS'] pieces = path.split('/') for index, piece in enumerate(pieces): if '*' in piece: path = '/'.join(pieces[index:]) regexp = convert_regexp(path) ptr[regexp] = { 'AA_RANK': { 'r': read, 'w': write, 'x': execute } } break else: ptr[piece] = ptr.get(piece, {}) ptr = ptr[piece] elif line.startswith('CAP_'): try: resource, severity = line.split() severity = int(severity) except ValueError: error_message = 'No severity value present in file: %s\n\t[Line %s]: %s' % ( dbname, lineno, line) #error(error_message) raise AppArmorException(error_message) # from None else: if severity not in range(0, 11): raise AppArmorException( "Inappropriate severity value present in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) self.severity['CAPABILITIES'][resource] = severity else: raise AppArmorException( "Unexpected line in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
def write_shell(self, filepath, f_out, config): """Writes the config object in shell file format""" # All the options in the file options = [key for key in config[''].keys()] # If a previous file exists modify it keeping the comments if os.path.exists(self.input_file): with open_file_read(self.input_file) as f_in: for line in f_in: result = shlex.split(line, True) # If line is not empty or comment if result: # If option=value or option="value" type if '=' in result[0]: option, value = result[0].split('=') if '#' in line: comment = value.split('#', 1)[1] comment = '#' + comment else: comment = '' # If option exists in the new config file if option in options: # If value is different if value != config[''][option]: value_new = config[''][option] if value_new is not None: # Update value if '"' in line: value_new = '"' + value_new + '"' line = option + '=' + value_new + comment + '\n' else: # If option changed to option type from option=value type line = option + comment + '\n' f_out.write(line) # Remove from remaining options list options.remove(option) else: # If option type option = result[0] value = None # If option exists in the new config file if option in options: # If its no longer option type if config[''][option] is not None: value = config[''][option] line = option + '=' + value + '\n' f_out.write(line) # Remove from remaining options list options.remove(option) else: # If its empty or comment copy as it is f_out.write(line) # If any new options are present if options: for option in options: value = config[''][option] # option type entry if value is None: line = option + '\n' # option=value type entry else: line = option + '=' + value + '\n' f_out.write(line)
def write_configparser(self, filepath, f_out, config): """Writes/updates the given file with given config object""" # All the sections in the file sections = config.sections() write = True section = None options = [] # If a previous file exists modify it keeping the comments if os.path.exists(self.input_file): with open_file_read(self.input_file) as f_in: for line in f_in: # If its a section if line.lstrip().startswith('['): # If any options from preceding section remain write them if options: for option in options: line_new = ' ' + option + ' = ' + config[section][option] + '\n' f_out.write(line_new) options = [] if section in sections: # Remove the written section from the list sections.remove(section) section = line.strip()[1:-1] if section in sections: # enable write for all entries in that section write = True options = config.options(section) # write the section f_out.write(line) else: # disable writing until next valid section write = False # If write enabled elif write: value = shlex.split(line, True) # If the line is empty or a comment if not value: f_out.write(line) else: option, value = line.split('=', 1) try: # split any inline comments value, comment = value.split('#', 1) comment = '#' + comment except ValueError: comment = '' if option.strip() in options: if config[section][option.strip()] != value.strip(): value = value.replace(value, config[section][option.strip()]) line = option + '=' + value + comment f_out.write(line) options.remove(option.strip()) # If any options remain from the preceding section if options: for option in options: line = ' ' + option + ' = ' + config[section][option] + '\n' f_out.write(line) options = [] # If any new sections are present if section in sections: sections.remove(section) for section in sections: f_out.write('\n[%s]\n' % section) options = config.options(section) for option in options: line = ' ' + option + ' = ' + config[section][option] + '\n' f_out.write(line)
def write_configparser(self, filepath, f_out, config): """Writes/updates the given file with given config object""" # All the sections in the file sections = config.sections() write = True section = None options = [] # If a previous file exists modify it keeping the comments if os.path.exists(self.input_file): with open_file_read(self.input_file) as f_in: for line in f_in: # If its a section if line.lstrip().startswith('['): # If any options from preceding section remain write them if options: for option in options: line_new = ' ' + option + ' = ' + config[ section][option] + '\n' f_out.write(line_new) options = [] if section in sections: # Remove the written section from the list sections.remove(section) section = line.strip()[1:-1] if section in sections: # enable write for all entries in that section write = True options = config.options(section) # write the section f_out.write(line) else: # disable writing until next valid section write = False # If write enabled elif write: value = shlex.split(line, True) # If the line is empty or a comment if not value: f_out.write(line) else: option, value = line.split('=', 1) try: # split any inline comments value, comment = value.split('#', 1) comment = '#' + comment except ValueError: comment = '' if option.strip() in options: if config[section][ option.strip()] != value.strip(): value = value.replace( value, config[section][option.strip()]) line = option + '=' + value + comment f_out.write(line) options.remove(option.strip()) # If any options remain from the preceding section if options: for option in options: line = ' ' + option + ' = ' + config[section][option] + '\n' f_out.write(line) options = [] # If any new sections are present if section in sections: sections.remove(section) for section in sections: f_out.write('\n[%s]\n' % section) options = config.options(section) for option in options: line = ' ' + option + ' = ' + config[section][option] + '\n' f_out.write(line)