Merge Use "profile//hat" in storage

TL;DR: Replace `aa[profile][hat]` with `active_profiles['profile//hat']` as a preparation to get rid of `aa`'s limits, especially to enable handling nested childs.

Since this is an extremely shortened summary, I recommend to check the individual commits for a readable and understandable diff and more details.

Note that this MR is "just" a preparation - nested childs are not supported yet. Also, `include` still uses the old structure. Both will be separate MRs - this one is already big enough ;-)

MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1360
Approved-by: Georgia Garcia <georgia.garcia@canonical.com>
Merged-by: Georgia Garcia <georgia.garcia@canonical.com>
This commit is contained in:
Georgia Garcia 2024-12-18 12:07:01 +00:00
commit a3299ba133
11 changed files with 254 additions and 189 deletions

View file

@ -1,7 +1,7 @@
#! /usr/bin/python3
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2014-2018 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2014-2024 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@ -113,7 +113,7 @@ class Merge(object):
def ask_merge_questions(self):
other = self.base
log_dict = {'merge': apparmor.aa.split_to_merged(other.aa)}
log_dict = {'merge': other.active_profiles.get_all_profiles()}
apparmor.aa.loadincludes()

View file

@ -1,6 +1,6 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2014-2021 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2014-2024 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@ -69,6 +69,7 @@ use_abstractions = True
include = dict()
active_profiles = ProfileList()
original_profiles = ProfileList()
extra_profiles = ProfileList()
# To store the globs entered by users so they can be provided again
@ -78,9 +79,6 @@ user_globs = {}
# let ask_addhat() remember answers for already-seen change_hat events
transitions = {}
aa = {} # Profiles originally in sd, replace by aa
original_aa = {}
changed = dict()
created = []
helpers = dict() # Preserve this between passes # was our
@ -92,12 +90,11 @@ def reset_aa():
Used by aa-mergeprof and some tests.
"""
global aa, include, active_profiles, original_aa
global include, active_profiles, original_profiles
aa = {}
include = dict()
active_profiles = ProfileList()
original_aa = {}
original_profiles = ProfileList()
def on_exit():
@ -417,6 +414,7 @@ def create_new_profile(localfile, is_stub=False):
full_hat = combine_profname((localfile, hat))
if not local_profile.get(full_hat, False):
local_profile[full_hat] = ProfileStorage('NEW', hat, 'create_new_profile() required_hats')
local_profile[full_hat]['parent'] = localfile
local_profile[full_hat]['is_hat'] = True
local_profile[full_hat]['flags'] = 'complain'
@ -431,7 +429,7 @@ def create_new_profile(localfile, is_stub=False):
def get_profile(prof_name):
"""search for inactive/extra profile, and ask if it should be used"""
if not extra_profiles.profiles.get(prof_name, False):
if not extra_profiles.profile_exists(prof_name):
return None # no inactive profile found
# TODO: search based on the attachment, not (only?) based on the profile name
@ -504,14 +502,14 @@ def autodep(bin_name, pname=''):
file = get_profile_filename_from_profile_name(pname, True)
profile_data[pname]['filename'] = file # change filename from extra_profile_dir to /etc/apparmor.d/
attach_profile_data(aa, profile_data)
attach_profile_data(original_aa, profile_data)
for p in profile_data.keys():
original_profiles.add_profile(file, p, profile_data[p]['attachment'], deepcopy(profile_data[p]))
attachment = profile_data[pname]['attachment']
if not attachment and pname.startswith('/'):
attachment = pname # use name as name and attachment
active_profiles.add_profile(file, pname, attachment)
active_profiles.add_profile(file, pname, attachment, profile_data[pname])
if os.path.isfile(profile_dir + '/abi/4.0'):
active_profiles.add_abi(file, AbiRule('abi/4.0', False, True))
@ -561,6 +559,8 @@ def change_profile_flags(prof_filename, program, flag, set_flag):
for lineno, line in enumerate(f_in):
if RE_PROFILE_START.search(line):
depth += 1
# TODO: hand over profile and hat (= parent profile)
# (and find out why it breaks test-aa.py with several "a child profile inside another child profile is not allowed" errors when doing so)
(profile, hat, prof_storage) = ProfileStorage.parse(line, prof_filename, lineno, '', '')
old_flags = prof_storage['flags']
newflags = ', '.join(add_or_remove_flag(old_flags, flag, set_flag))
@ -581,6 +581,7 @@ def change_profile_flags(prof_filename, program, flag, set_flag):
line = '%s\n' % line[0]
elif RE_PROFILE_HAT_DEF.search(line):
depth += 1
# TODO: hand over profile and hat (= parent profile)
(profile, hat, prof_storage) = ProfileStorage.parse(line, prof_filename, lineno, '', '')
old_flags = prof_storage['flags']
newflags = ', '.join(add_or_remove_flag(old_flags, flag, set_flag))
@ -590,6 +591,7 @@ def change_profile_flags(prof_filename, program, flag, set_flag):
line = '%s\n' % line[0]
elif RE_PROFILE_END.search(line):
depth -= 1
# TODO: restore 'profile' and 'hat' to previous values (not really needed/used for aa-complain etc., but can't hurt)
f_out.write(line)
os.rename(temp_file.name, prof_filename)
@ -655,7 +657,7 @@ def ask_addhat(hashlog):
for full_hat in hashlog[aamode][profile]['change_hat']:
hat = full_hat.split('//')[-1]
if aa[profile].get(hat, False):
if active_profiles.profile_exists(full_hat):
continue # no need to ask if the hat already exists
default_hat = None
@ -692,18 +694,26 @@ def ask_addhat(hashlog):
transitions[context] = ans
filename = active_profiles.filename_from_profile_name(profile) # filename of parent profile, will be used for new hats
if ans == 'CMD_ADDHAT':
aa[profile][hat] = ProfileStorage(profile, hat, 'ask_addhat addhat')
aa[profile][hat]['flags'] = aa[profile][profile]['flags']
hashlog[aamode][full_hat]['final_name'] = '%s//%s' % (profile, hat)
hat_obj = ProfileStorage(profile, hat, 'ask_addhat addhat')
hat_obj['parent'] = profile
hat_obj['flags'] = active_profiles[profile]['flags']
new_full_hat = combine_profname([profile, hat])
active_profiles.add_profile(filename, new_full_hat, hat, hat_obj)
hashlog[aamode][full_hat]['final_name'] = new_full_hat
changed[profile] = True
elif ans == 'CMD_USEDEFAULT':
hat = default_hat
hashlog[aamode][full_hat]['final_name'] = '%s//%s' % (profile, default_hat)
if not aa[profile].get(hat, False):
new_full_hat = combine_profname([profile, hat])
hashlog[aamode][full_hat]['final_name'] = new_full_hat
if not active_profiles.profile_exists(full_hat):
# create default hat if it doesn't exist yet
aa[profile][hat] = ProfileStorage(profile, hat, 'ask_addhat default hat')
aa[profile][hat]['flags'] = aa[profile][profile]['flags']
hat_obj = ProfileStorage(profile, hat, 'ask_addhat default hat')
hat_obj['parent'] = profile
hat_obj['flags'] = active_profiles[profile]['flags']
active_profiles.add_profile(filename, new_full_hat, hat, hat_obj)
changed[profile] = True
elif ans == 'CMD_DENY':
# As unknown hat is denied no entry for it should be made
@ -726,14 +736,14 @@ def ask_exec(hashlog, default_ans=''):
raise AppArmorBug(
'exec permissions requested for directory %s (profile %s). This should not happen - please open a bugreport!' % (exec_target, full_profile))
if not aa.get(profile):
if not active_profiles.profile_exists(profile):
continue # ignore log entries for non-existing profiles
if not aa[profile].get(hat):
if not active_profiles.profile_exists(full_profile):
continue # ignore log entries for non-existing hats
exec_event = FileRule(exec_target, None, FileRule.ANY_EXEC, FileRule.ALL, owner=False, log_event=True)
if is_known_rule(aa[profile][hat], 'file', exec_event):
if is_known_rule(active_profiles[full_profile], 'file', exec_event):
continue
# nx is not used in profiles but in log files.
@ -886,7 +896,7 @@ def ask_exec(hashlog, default_ans=''):
file_perm = 'mr'
else:
if ans == 'CMD_DENY':
aa[profile][hat]['file'].add(FileRule(exec_target, None, 'x', FileRule.ALL, owner=False, log_event=True, deny=True))
active_profiles[full_profile]['file'].add(FileRule(exec_target, None, 'x', FileRule.ALL, owner=False, log_event=True, deny=True))
changed[profile] = True
if target_profile and hashlog[aamode].get(target_profile):
hashlog[aamode][target_profile]['final_name'] = ''
@ -899,7 +909,7 @@ def ask_exec(hashlog, default_ans=''):
else:
rule_to_name = FileRule.ALL
aa[profile][hat]['file'].add(FileRule(exec_target, file_perm, exec_mode, rule_to_name, owner=False, log_event=True))
active_profiles[full_profile]['file'].add(FileRule(exec_target, file_perm, exec_mode, rule_to_name, owner=False, log_event=True))
changed[profile] = True
@ -910,16 +920,16 @@ def ask_exec(hashlog, default_ans=''):
exec_target_rule = FileRule(exec_target, 'r', None, FileRule.ALL, owner=False)
interpreter_rule = FileRule(interpreter_path, None, 'ix', FileRule.ALL, owner=False)
if not is_known_rule(aa[profile][hat], 'file', exec_target_rule):
aa[profile][hat]['file'].add(exec_target_rule)
if not is_known_rule(aa[profile][hat], 'file', interpreter_rule):
aa[profile][hat]['file'].add(interpreter_rule)
if not is_known_rule(active_profiles[full_profile], 'file', exec_target_rule):
active_profiles[full_profile]['file'].add(exec_target_rule)
if not is_known_rule(active_profiles[full_profile], 'file', interpreter_rule):
active_profiles[full_profile]['file'].add(interpreter_rule)
if abstraction:
abstraction_rule = IncludeRule(abstraction, False, True)
if not aa[profile][hat]['inc_ie'].is_covered(abstraction_rule):
aa[profile][hat]['inc_ie'].add(abstraction_rule)
if not active_profiles[full_profile]['inc_ie'].is_covered(abstraction_rule):
active_profiles[full_profile]['inc_ie'].add(abstraction_rule)
# Update tracking info based on kind of change
@ -959,19 +969,21 @@ def ask_exec(hashlog, default_ans=''):
if to_name:
exec_target = to_name
if not aa[profile].get(exec_target, False):
full_exec_target = combine_profname([profile, exec_target])
if not active_profiles.profile_exists(full_exec_target):
ynans = 'y'
if 'i' in exec_mode:
ynans = aaui.UI_YesNo(_('A profile for %s does not exist.\nDo you want to create one?') % exec_target, 'n')
if ynans == 'y':
if not aa[profile].get(exec_target, False):
stub_profile = merged_to_split(create_new_profile(exec_target, True))
aa[profile][exec_target] = stub_profile[exec_target][exec_target]
if not active_profiles.profile_exists(full_exec_target):
stub_profile = create_new_profile(exec_target, True)
for p in stub_profile:
active_profiles.add_profile(prof_filename, p, stub_profile[p]['attachment'], stub_profile[p])
if profile != exec_target:
aa[profile][exec_target]['flags'] = aa[profile][profile]['flags']
active_profiles[full_exec_target]['flags'] = active_profiles[profile]['flags']
aa[profile][exec_target]['flags'] = 'complain'
active_profiles[full_exec_target]['flags'] = 'complain'
if target_profile and hashlog[aamode].get(target_profile):
hashlog[aamode][target_profile]['final_name'] = '%s//%s' % (profile, exec_target)
@ -1024,8 +1036,8 @@ def ask_the_questions(log_dict):
else:
sev_db.set_variables({})
if aa.get(profile): # only continue/ask if the parent profile exists
if not aa[profile].get(hat, {}).get('file'):
if active_profiles.profile_exists(profile): # only continue/ask if the parent profile exists # XXX check direct parent or top-level? Also, get rid of using "profile" here!
if not active_profiles.profile_exists(full_profile):
if aamode != 'merge':
# Ignore log events for a non-existing profile or child profile. Such events can occur
# after deleting a profile or hat manually, or when processing a foreign log.
@ -1058,18 +1070,21 @@ def ask_the_questions(log_dict):
continue # don't ask about individual rules if the user doesn't want the additional subprofile/hat
if log_dict[aamode][full_profile]['is_hat']:
aa[profile][hat] = ProfileStorage(profile, hat, 'mergeprof ask_the_questions() - missing hat')
aa[profile][hat]['is_hat'] = True
prof_obj = ProfileStorage(profile, hat, 'mergeprof ask_the_questions() - missing hat')
prof_obj['is_hat'] = True
else:
aa[profile][hat] = ProfileStorage(profile, hat, 'mergeprof ask_the_questions() - missing subprofile')
aa[profile][hat]['is_hat'] = False
prof_obj = ProfileStorage(profile, hat, 'mergeprof ask_the_questions() - missing subprofile')
prof_obj['is_hat'] = False
prof_obj['parent'] = profile
active_profiles.add_profile(prof_filename, full_profile, hat, prof_obj)
# check for and ask about conflicting exec modes
ask_conflict_mode(aa[profile][hat], log_dict[aamode][full_profile])
ask_conflict_mode(active_profiles[full_profile], log_dict[aamode][full_profile])
prof_changed, end_profiling = ask_rule_questions(
log_dict[aamode][full_profile], combine_name(profile, hat),
aa[profile][hat], ruletypes)
log_dict[aamode][full_profile], full_profile,
active_profiles[full_profile], ruletypes)
if prof_changed:
changed[profile] = True
if end_profiling:
@ -1082,7 +1097,7 @@ def ask_rule_questions(prof_events, profile_name, the_profile, r_types):
parameter typical value
prof_events log_dict[aamode][full_profile]
profile_name profile name (possible profile//hat)
the_profile aa[profile][hat] -- will be modified
the_profile active_profiles[full_profile] -- will be modified
r_types ruletypes
returns:
@ -1449,7 +1464,6 @@ def set_logfile(filename):
def do_logprof_pass(logmark='', out_dir=None):
# set up variables for this pass
global active_profiles
# aa = hasher()
# changed = dict()
aaui.UI_Info(_('Reading log entries from %s.') % logfile)
@ -1474,7 +1488,7 @@ def do_logprof_pass(logmark='', out_dir=None):
def save_profiles(is_mergeprof=False, out_dir=None):
# Ensure the changed profiles are actual active profiles
for prof_name in changed.keys():
if not aa.get(prof_name, False):
if not active_profiles.profile_exists(prof_name):
print("*** save_profiles(): removing %s" % prof_name)
print('*** This should not happen. Please open a bugreport!')
changed.pop(prof_name)
@ -1511,19 +1525,19 @@ def save_profiles(is_mergeprof=False, out_dir=None):
elif ans == 'CMD_VIEW_CHANGES':
oldprofile = None
if aa[profile_name][profile_name].get('filename', False):
oldprofile = aa[profile_name][profile_name]['filename']
if active_profiles[profile_name].get('filename', False):
oldprofile = active_profiles[profile_name]['filename']
else:
oldprofile = get_profile_filename_from_attachment(profile_name, True)
serialize_options = {'METADATA': True}
newprofile = serialize_profile(split_to_merged(aa), profile_name, serialize_options)
newprofile = serialize_profile(active_profiles, profile_name, serialize_options)
aaui.UI_Changes(oldprofile, newprofile, comments=True)
elif ans == 'CMD_VIEW_CHANGES_CLEAN':
oldprofile = serialize_profile(split_to_merged(original_aa), profile_name, {})
newprofile = serialize_profile(split_to_merged(aa), profile_name, {})
oldprofile = serialize_profile(original_profiles, profile_name, {})
newprofile = serialize_profile(active_profiles, profile_name, {})
aaui.UI_Changes(oldprofile, newprofile)
@ -1556,9 +1570,9 @@ def collapse_log(hashlog, ignore_null_profiles=True):
profile, hat = split_name(final_name) # XXX limited to two levels to avoid an Exception on nested child profiles or nested null-*
# TODO: support nested child profiles
# used to avoid to accidentally initialize aa[profile][hat] or calling is_known_rule() on events for a non-existing profile
# used to avoid calling is_known_rule() on events for a non-existing profile
hat_exists = False
if aa.get(profile) and aa[profile].get(hat):
if active_profiles.profile_exists(profile) and active_profiles.profile_exists(final_name): # we need to check for the target profile here
hat_exists = True
if not log_dict[aamode].get(final_name):
@ -1567,7 +1581,7 @@ def collapse_log(hashlog, ignore_null_profiles=True):
for ev_type, ev_class in ReadLog.ruletypes.items():
for rule in ev_class.from_hashlog(hashlog[aamode][full_profile][ev_type]):
if not hat_exists or not is_known_rule(aa[profile][hat], ev_type, rule):
if not hat_exists or not is_known_rule(active_profiles[full_profile], ev_type, rule):
log_dict[aamode][final_name][ev_type].add(rule)
return log_dict
@ -1587,9 +1601,8 @@ def read_profiles(ui_msg=False, skip_profiles=()):
#
# The skip_profiles parameter should only be specified by tests.
global aa, original_aa
aa = {}
original_aa = {}
global original_profiles
original_profiles = ProfileList()
if ui_msg:
aaui.UI_Info(_('Updating AppArmor profiles in %s.') % profile_dir)
@ -1643,7 +1656,7 @@ def read_inactive_profiles(skip_profiles=()):
read_profile(full_file, False)
def read_profile(file, active_profile, read_error_fatal=False):
def read_profile(file, is_active_profile, read_error_fatal=False):
data = None
try:
with open_file_read(file) as f_in:
@ -1661,30 +1674,17 @@ def read_profile(file, active_profile, read_error_fatal=False):
if not profile_data:
return
if active_profile:
attach_profile_data(aa, profile_data)
attach_profile_data(original_aa, profile_data)
for profile in profile_data:
attachment = profile_data[profile]['attachment']
filename = profile_data[profile]['filename']
for profile in profile_data:
if '//' in profile:
continue # TODO: handle hats/child profiles independent of main profiles
attachment = profile_data[profile]['attachment']
filename = profile_data[profile]['filename']
if not attachment and profile.startswith('/'):
attachment = profile # use profile as name and attachment
active_profiles.add_profile(filename, profile, attachment)
else:
for profile in profile_data:
attachment = profile_data[profile]['attachment']
filename = profile_data[profile]['filename']
if not attachment and profile.startswith('/'):
attachment = profile # use profile as name and attachment
if not attachment and profile.startswith('/'):
attachment = profile # use profile as name and attachment
if is_active_profile:
active_profiles.add_profile(filename, profile, attachment, profile_data[profile])
original_profiles.add_profile(filename, profile, attachment, deepcopy(profile_data[profile]))
else:
extra_profiles.add_profile(filename, profile, attachment, profile_data[profile])
@ -1862,6 +1862,7 @@ def parse_profile_data(data, file, do_include, in_preamble):
profname = combine_profname((parsed_prof, hat))
if not profile_data.get(profname, False):
profile_data[profname] = ProfileStorage(parsed_prof, hat, 'parse_profile_data() required_hats')
profile_data[profname]['parent'] = parsed_prof
profile_data[profname]['is_hat'] = True
# End of file reached but we're stuck in a profile
@ -1931,23 +1932,6 @@ def merged_to_split(profile_data):
return compat
def split_to_merged(profile_data):
"""(temporary) helper function to convert a traditional compat['foo']['bar'] to a profile['foo//bar'] list"""
merged = {}
for profile in profile_data:
for hat in profile_data[profile]:
if profile == hat:
merged_name = profile
else:
merged_name = combine_profname((profile, hat))
merged[merged_name] = profile_data[profile][hat]
return merged
def write_piece(profile_data, depth, name, nhat):
pre = ' ' * depth
data = []
@ -1996,7 +1980,8 @@ def write_piece(profile_data, depth, name, nhat):
def serialize_profile(profile_data, name, options):
string = ''
''' combine the preamble and profiles in a file to a string (to be written to the profile file) '''
data = []
if not isinstance(options, dict):
@ -2005,12 +1990,11 @@ def serialize_profile(profile_data, name, options):
include_metadata = options.get('METADATA', False)
if include_metadata:
string = '# Last Modified: %s\n' % time.asctime()
data.extend(['# Last Modified: %s' % time.asctime()])
# if profile_data[name].get('initial_comment', False):
# comment = profile_data[name]['initial_comment']
# comment.replace('\\n', '\n')
# string += comment + '\n'
# data.append(comment)
if options.get('is_attachment'):
prof_filename = get_profile_filename_from_attachment(name, True)
@ -2021,23 +2005,29 @@ def serialize_profile(profile_data, name, options):
# Here should be all the profiles from the files added write after global/common stuff
for prof in sorted(active_profiles.profiles_in_file(prof_filename)):
if active_profiles.profiles[prof]['parent']:
continue # child profile or hat, already part of its parent profile
# aa-logprof asks to save each file separately. Therefore only update the given profile, and keep the original version of other profiles in the file
if prof != name:
if original_aa.get(prof, {}).get(prof, {}).get('initial_comment', False):
comment = original_aa[prof][prof]['initial_comment']
comment.replace('\\n', '\n')
data.append(comment + '\n')
data.extend(write_piece(split_to_merged(original_aa), 0, prof, prof))
if original_profiles.profile_exists(prof) and original_profiles[prof].get('initial_comment'):
comment = original_profiles[prof]['initial_comment']
data.extend([comment, ''])
data.extend(write_piece(original_profiles.get_profile_and_childs(prof), 0, prof, prof))
else:
if profile_data[name].get('initial_comment', False):
comment = profile_data[name]['initial_comment']
comment.replace('\\n', '\n')
data.append(comment + '\n')
data.extend([comment, ''])
data.extend(write_piece(profile_data, 0, name, name))
# write_piece() expects a dict, not a ProfileList - TODO: change write_piece()?
if type(profile_data) is dict:
data.extend(write_piece(profile_data, 0, name, name))
else:
data.extend(write_piece(profile_data.get_profile_and_childs(name), 0, name, name))
string += '\n'.join(data)
return string + '\n'
return '\n'.join(data) + '\n'
def write_profile_ui_feedback(profile, is_attachment=False, out_dir=None):
@ -2046,15 +2036,15 @@ def write_profile_ui_feedback(profile, is_attachment=False, out_dir=None):
def write_profile(profile, is_attachment=False, out_dir=None):
if aa[profile][profile].get('filename', False):
prof_filename = aa[profile][profile]['filename']
if active_profiles[profile]['filename']:
prof_filename = active_profiles[profile]['filename']
elif is_attachment:
prof_filename = get_profile_filename_from_attachment(profile, True)
else:
prof_filename = get_profile_filename_from_profile_name(profile, True)
serialize_options = {'METADATA': True, 'is_attachment': is_attachment}
profile_string = serialize_profile(split_to_merged(aa), profile, serialize_options)
profile_string = serialize_profile(active_profiles, profile, serialize_options)
try:
with NamedTemporaryFile('w', suffix='~', delete=False, dir=out_dir or profile_dir) as newprof:
@ -2079,7 +2069,9 @@ def write_profile(profile, is_attachment=False, out_dir=None):
else:
debug_logger.info("Unchanged profile written: %s (not listed in 'changed' list)", profile)
original_aa[profile] = deepcopy(aa[profile])
for full_profile in active_profiles.get_profile_and_childs(profile):
if profile == full_profile or active_profiles[full_profile]['parent']: # copy main profile and childs, but skip external hats
original_profiles.replace_profile(full_profile, deepcopy(active_profiles[full_profile]))
def include_list_recursive(profile, in_preamble=False):

View file

@ -1,6 +1,6 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2014-2015 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2014-2024 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@ -18,7 +18,6 @@ import apparmor.aa as apparmor
class Prof:
def __init__(self, filename):
apparmor.init_aa()
self.aa = apparmor.aa
self.active_profiles = apparmor.active_profiles
self.include = apparmor.include
self.filename = filename
@ -36,7 +35,7 @@ class CleanProf:
deleted += self.other.active_profiles.delete_preamble_duplicates(self.other.filename)
for profile in self.profile.aa.keys():
for profile in self.profile.active_profiles.get_all_profiles():
deleted += self.remove_duplicate_rules(profile)
return deleted
@ -50,22 +49,22 @@ class CleanProf:
deleted += self.profile.active_profiles.delete_preamble_duplicates(self.profile.filename)
# Process every hat in the profile individually
for hat in sorted(self.profile.aa[program].keys()):
includes = self.profile.aa[program][hat]['inc_ie'].get_all_full_paths(apparmor.profile_dir)
for full_profile in sorted(self.profile.active_profiles.get_profile_and_childs(program)):
includes = self.profile.active_profiles[full_profile]['inc_ie'].get_all_full_paths(apparmor.profile_dir)
# Clean up superfluous rules from includes in the other profile
for inc in includes:
if not self.profile.include.get(inc, {}).get(inc, False):
apparmor.load_include(inc)
if self.other.aa[program].get(hat): # carefully avoid to accidentally initialize self.other.aa[program][hat]
deleted += apparmor.delete_all_duplicates(self.other.aa[program][hat], inc, apparmor.ruletypes)
if self.other.active_profiles.profile_exists(full_profile):
deleted += apparmor.delete_all_duplicates(self.other.active_profiles[full_profile], inc, apparmor.ruletypes)
# Clean duplicate rules in other profile
for ruletype in apparmor.ruletypes:
if not self.same_file:
if self.other.aa[program].get(hat): # carefully avoid to accidentally initialize self.other.aa[program][hat]
deleted += self.other.aa[program][hat][ruletype].delete_duplicates(self.profile.aa[program][hat][ruletype])
if self.other.active_profiles.profile_exists(full_profile):
deleted += self.other.active_profiles[full_profile][ruletype].delete_duplicates(self.profile.active_profiles[full_profile][ruletype])
else:
deleted += self.other.aa[program][hat][ruletype].delete_duplicates(None)
deleted += self.other.active_profiles[full_profile][ruletype].delete_duplicates(None)
return deleted

View file

@ -1,5 +1,5 @@
# ----------------------------------------------------------------------
# Copyright (C) 2018-2020 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2018-2024 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@ -52,6 +52,12 @@ class ProfileList:
name = type(self).__name__
return '\n<%s>\n%s\n</%s>\n' % (name, '\n'.join(self.files), name)
def __getitem__(self, key):
if key in self.profiles:
return self.profiles[key]
else:
raise AppArmorBug('attempt to read unknown profile %s' % key)
def init_file(self, filename):
if self.files.get(filename):
return # don't re-initialize / overwrite existing data
@ -63,7 +69,7 @@ class ProfileList:
for rule in preamble_ruletypes:
self.files[filename][rule] = preamble_ruletypes[rule]['ruleset']()
def add_profile(self, filename, profile_name, attachment, prof_storage=None):
def add_profile(self, filename, profile_name, attachment, prof_storage):
"""Add the given profile and attachment to the list"""
if not filename:
@ -72,7 +78,7 @@ class ProfileList:
if not profile_name and not attachment:
raise AppArmorBug('Neither profile name or attachment given')
if type(prof_storage) is not ProfileStorage and prof_storage is not None:
if type(prof_storage) is not ProfileStorage:
raise AppArmorBug('Invalid profile type: %s' % type(prof_storage))
if profile_name in self.profile_names:
@ -101,6 +107,21 @@ class ProfileList:
self.files[filename]['profiles'].append(attachment)
self.profiles[attachment] = prof_storage
def replace_profile(self, profile_name, prof_storage):
"""Replace the given profile in the profile list"""
if profile_name not in self.profiles:
raise AppArmorBug('Attempt to replace non-existing profile %s' % profile_name)
if type(prof_storage) is not ProfileStorage:
raise AppArmorBug('Invalid profile type: %s' % type(prof_storage))
# we might lift this restriction later, but for now, forbid changing the attachment instead of updating self.attachments
if self.profiles[profile_name]['attachment'] != prof_storage['attachment']:
raise AppArmorBug('Attempt to change atttachment while replacing profile %s - original: %s, new: %s' % (profile_name, self.profiles[profile_name]['attachment'], prof_storage['attachment']))
self.profiles[profile_name] = prof_storage
def add_rule(self, filename, ruletype, rule):
"""Store the given rule for the given profile filename preamble"""
@ -168,6 +189,9 @@ class ProfileList:
return deleted
def get_all_profiles(self):
return self.profiles
def get_profile_and_childs(self, profile_name):
found = {}
for prof in self.profiles:
@ -283,6 +307,9 @@ class ProfileList:
return merged_variables
def profile_exists(self, profile_name):
return profile_name in self.profiles
def profiles_in_file(self, filename):
"""Return list of profiles in the given file"""
if not self.files.get(filename):

View file

@ -1,6 +1,6 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2014-2021 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2014-2024 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@ -77,6 +77,7 @@ class ProfileStorage:
data['filename'] = ''
data['logprof_suggest'] = '' # set in abstractions that should be suggested by aa-logprof
data['parent'] = '' # parent profile, or '' for top-level profiles and external hats
data['name'] = ''
data['attachment'] = ''
data['xattrs'] = ''
@ -221,11 +222,13 @@ class ProfileStorage:
_('%(profile)s profile in %(file)s contains syntax errors in line %(line)s: a child profile inside another child profile is not allowed.')
% {'profile': profile, 'file': file, 'line': lineno + 1})
parent = profile
hat = matches['profile']
prof_or_hat_name = hat
pps_set_hat_external = False
else: # stand-alone profile
parent = ''
profile = matches['profile']
prof_or_hat_name = profile
if len(profile.split('//')) > 2:
@ -241,6 +244,7 @@ class ProfileStorage:
prof_storage = cls(profile, hat, cls.__name__ + '.parse()')
prof_storage['parent'] = parent
prof_storage['name'] = prof_or_hat_name
prof_storage['filename'] = file
prof_storage['external'] = pps_set_hat_external

View file

@ -1,6 +1,6 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2015-2023 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2015-2024 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@ -64,7 +64,7 @@ class aa_tools:
prof_filename = apparmor.get_profile_filename_from_attachment(fq_path, True)
else:
which_ = which(p)
if self.name == 'cleanprof' and p in apparmor.aa:
if self.name == 'cleanprof' and apparmor.active_profiles.profile_exists(p):
program = p # not really correct, but works
profile = p
prof_filename = apparmor.get_profile_filename_from_profile_name(profile)
@ -104,14 +104,14 @@ class aa_tools:
if program is None:
program = profile
if not program or not (os.path.exists(program) or profile in apparmor.aa):
if not program or not (os.path.exists(program) or apparmor.active_profiles.profile_exists(profile)):
if program and not program.startswith('/'):
program = aaui.UI_GetString(_('The given program cannot be found, please try with the fully qualified path name of the program: '), '')
else:
aaui.UI_Info(_("%s does not exist, please double-check the path.") % program)
sys.exit(1)
if program and profile in apparmor.aa:
if program and apparmor.active_profiles.profile_exists(profile):
self.clean_profile(program, profile, prof_filename)
else:
@ -207,8 +207,8 @@ class aa_tools:
apparmor.write_profile_ui_feedback(profile)
self.reload_profile(prof_filename)
elif ans == 'CMD_VIEW_CHANGES':
# oldprofile = apparmor.serialize_profile(apparmor.split_to_merged(apparmor.original_aa), profile, {})
newprofile = apparmor.serialize_profile(apparmor.split_to_merged(apparmor.aa), profile, {}) # , {'is_attachment': True})
# oldprofile = apparmor.serialize_profile(apparmor.original_profiles, profile, {})
newprofile = apparmor.serialize_profile(apparmor.active_profiles, profile, {}) # , {'is_attachment': True})
aaui.UI_Changes(prof_filename, newprofile, comments=True)
def unload_profile(self, prof_filename):

View file

@ -35,9 +35,9 @@ def add_to_profile(rule, profile_name):
rule_obj = rule_class.create_instance(rule)
if profile_name not in aa.aa or profile_name not in aa.aa[profile_name]:
if not aa.active_profiles.profile_exists(profile_name):
exit(_('Cannot find {} in profiles').format(profile_name))
aa.aa[profile_name][profile_name][rule_type].add(rule_obj, cleanup=True)
aa.active_profiles[profile_name][rule_type].add(rule_obj, cleanup=True)
# Save changes
aa.write_profile_ui_feedback(profile_name)

View file

@ -1,7 +1,7 @@
#! /usr/bin/python3
# ------------------------------------------------------------------
#
# Copyright (C) 2014-2021 Christian Boltz
# Copyright (C) 2014-2024 Christian Boltz
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@ -16,7 +16,7 @@ import unittest
import apparmor.aa # needed to set global vars in some tests
from apparmor.aa import (
change_profile_flags, check_for_apparmor, create_new_profile, get_file_perms, get_interpreter_and_abstraction, get_profile_flags,
merged_to_split, parse_profile_data, propose_file_rules, set_options_audit_mode, set_options_owner_mode, split_to_merged)
merged_to_split, parse_profile_data, propose_file_rules, set_options_audit_mode, set_options_owner_mode)
from apparmor.aare import AARE
from apparmor.common import AppArmorBug, AppArmorException, is_skippable_file
from apparmor.rule.file import FileRule
@ -515,6 +515,21 @@ class AaTest_parse_profile_data(AATest):
self.assertEqual(prof['/foo']['filename'], 'somefile')
self.assertEqual(prof['/foo']['flags'], None)
def test_parse_parent_and_child(self):
prof = parse_profile_data('profile /foo {\nprofile /bar {\n}\n}\n'.split(), 'somefile', False, False)
self.assertEqual(list(prof.keys()), ['/foo', '/foo///bar'])
self.assertEqual(prof['/foo']['parent'], '')
self.assertEqual(prof['/foo']['name'], '/foo')
self.assertEqual(prof['/foo']['filename'], 'somefile')
self.assertEqual(prof['/foo']['flags'], None)
self.assertEqual(prof['/foo///bar']['parent'], '/foo')
self.assertEqual(prof['/foo///bar']['name'], '/bar')
self.assertEqual(prof['/foo///bar']['filename'], 'somefile')
self.assertEqual(prof['/foo///bar']['flags'], None)
def test_parse_duplicate_profile(self):
with self.assertRaises(AppArmorException):
# file contains two profiles with the same name
@ -746,25 +761,6 @@ class AaTest_merged_to_split(AATest):
self.assertTrue(result[profile][hat])
class AaTest_split_to_merged(AATest):
tests = (
(("foo", "foo"), "foo"),
(("foo", "bar"), "foo//bar"),
)
def _run_test(self, params, expected):
old = {}
profile = params[0]
hat = params[1]
old[profile] = {}
old[profile][hat] = True # simplified, but enough for this test
result = split_to_merged(old)
self.assertEqual(list(result.keys()), [expected])
self.assertTrue(result[expected])
setup_aa(apparmor.aa)
setup_all_loops(__name__)
if __name__ == '__main__':

View file

@ -219,7 +219,8 @@ def logfile_to_profile(logfile):
apparmor.aa.load_sev_db()
profile, hat = split_name(parsed_event['profile'])
full_profile = parsed_event['profile']
profile, hat = split_name(full_profile)
dummy_prof = apparmor.aa.ProfileStorage('TEST DUMMY for active_profiles', profile_dummy_file, 'logprof_to_profile()')
@ -227,10 +228,10 @@ def logfile_to_profile(logfile):
# if profile.startswith('/'):
# apparmor.aa.active_profiles.add_profile(profile_dummy_file, profile, profile, dummy_prof)
# else:
apparmor.aa.active_profiles.add_profile(profile_dummy_file, profile, '', dummy_prof)
apparmor.aa.aa[profile] = {}
apparmor.aa.aa[profile][hat] = dummy_prof
# create (only) the main/parent profile in active_profiles so that ask_exec() can add an exec rule to it
# If we ever add tests that create an exec rule in a child profile (for nested childs), we'll have to create the child profile that will get the grandchild exec rule
apparmor.aa.active_profiles.add_profile(profile_dummy_file, profile, '', dummy_prof)
log_reader = ReadLog(logfile, apparmor.aa.active_profiles, '')
hashlog = log_reader.read_log('')
@ -243,13 +244,14 @@ def logfile_to_profile(logfile):
# ask_exec modifies 'aa', not log_dict. "transfer" exec rules from 'aa' to log_dict
for tmpaamode in hashlog:
for tmpprofile in hashlog[tmpaamode]:
for rule_obj in apparmor.aa.aa[profile][hat]['file'].rules:
for rule_obj in apparmor.aa.active_profiles[profile]['file'].rules: # when the log contains an exec event, the exec event/rule will be in the parent profile, therefore check 'profile', not 'full_profile'.
# Also, at this point, tmpprofile might contain a child profile - which we didn't create in active_profiles, so trying to read it would trigger an error.
log_dict[tmpaamode][tmpprofile]['file'].add(rule_obj)
if list(log_dict[aamode].keys()) != [parsed_event['profile']]:
raise Exception('log_dict[{}] contains unexpected keys. Logfile: {}, keys {}'.format(aamode, logfile, log_dict.keys()))
if '//' in parsed_event['profile']:
if '//' in full_profile:
# log event for a child profile means log_dict only contains the child profile
# initialize parent profile in log_dict as ProfileStorage to ensure writing the profile doesn't fail
# (in "normal" usage outside of this test, log_dict will not be handed over to serialize_profile())

View file

@ -1,7 +1,7 @@
#! /usr/bin/python3
# ------------------------------------------------------------------
#
# Copyright (C) 2018 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2018-2024 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@ -12,6 +12,7 @@
import os
import shutil
import unittest
from copy import deepcopy
import apparmor.aa
from apparmor.common import AppArmorBug, AppArmorException
@ -37,29 +38,50 @@ class TestAdd_profile(AATest):
self.assertEqual(str(self.pl), "\n".join(['', '<ProfileList>', '', '</ProfileList>', '']))
def testAdd_profile_1(self):
self.assertFalse(self.pl.profile_exists('foo'))
self.assertFalse(self.pl.profile_exists('/bin/foo'))
self.pl.add_profile('/etc/apparmor.d/bin.foo', 'foo', '/bin/foo', self.dummy_profile)
self.assertTrue(self.pl.profile_exists('foo'))
self.assertFalse(self.pl.profile_exists('/bin/foo'))
self.assertEqual(self.pl.profile_names, {'foo': '/etc/apparmor.d/bin.foo'})
self.assertEqual(self.pl.attachments, {'/bin/foo': {'f': '/etc/apparmor.d/bin.foo', 'p': 'foo', 're': AARE('/bin/foo', True)}})
self.assertEqual(self.pl.profiles_in_file('/etc/apparmor.d/bin.foo'), ['foo'])
self.assertEqual(str(self.pl), '\n<ProfileList>\n/etc/apparmor.d/bin.foo\n</ProfileList>\n')
# test __getitem__()
self.assertEqual(self.pl['foo'], self.dummy_profile)
with self.assertRaises(AppArmorBug):
self.pl['does_not_exist']
def testAdd_profile_2(self):
self.assertFalse(self.pl.profile_exists('foo'))
self.assertFalse(self.pl.profile_exists('/bin/foo'))
self.pl.add_profile('/etc/apparmor.d/bin.foo', None, '/bin/foo', self.dummy_profile)
self.assertFalse(self.pl.profile_exists('foo'))
self.assertTrue(self.pl.profile_exists('/bin/foo'))
self.assertEqual(self.pl.profile_names, {})
self.assertEqual(self.pl.attachments, {'/bin/foo': {'f': '/etc/apparmor.d/bin.foo', 'p': '/bin/foo', 're': AARE('/bin/foo', True)}})
self.assertEqual(self.pl.profiles_in_file('/etc/apparmor.d/bin.foo'), ['/bin/foo'])
self.assertEqual(str(self.pl), '\n<ProfileList>\n/etc/apparmor.d/bin.foo\n</ProfileList>\n')
def testAdd_profile_3(self):
self.assertFalse(self.pl.profile_exists('foo'))
self.assertFalse(self.pl.profile_exists('/bin/foo'))
self.pl.add_profile('/etc/apparmor.d/bin.foo', 'foo', None, self.dummy_profile)
self.assertTrue(self.pl.profile_exists('foo'))
self.assertFalse(self.pl.profile_exists('/bin/foo'))
self.assertEqual(self.pl.profile_names, {'foo': '/etc/apparmor.d/bin.foo'})
self.assertEqual(self.pl.attachments, {})
self.assertEqual(self.pl.profiles_in_file('/etc/apparmor.d/bin.foo'), ['foo'])
self.assertEqual(str(self.pl), '\n<ProfileList>\n/etc/apparmor.d/bin.foo\n</ProfileList>\n')
def testAdd_profileError_1(self):
self.assertFalse(self.pl.profile_exists('foo'))
self.assertFalse(self.pl.profile_exists('/bin/foo'))
with self.assertRaises(AppArmorBug):
self.pl.add_profile('', 'foo', '/bin/foo', self.dummy_profile) # no filename
self.assertFalse(self.pl.profile_exists('foo'))
self.assertFalse(self.pl.profile_exists('/bin/foo'))
def testAdd_profileError_2(self):
with self.assertRaises(AppArmorBug):
@ -99,6 +121,25 @@ class TestAdd_profile(AATest):
with self.assertRaises(AppArmorBug):
self.pl.add_profile('/etc/apparmor.d/bin.foo', 'foo', '/bin/foo', 'wrong_type')
def testReplace_profile_1(self):
self.pl.add_profile('/etc/apparmor.d/bin.foo', 'foo', '/bin/foo', self.dummy_profile)
# test if replacement works (but without checking if the content of the actual profile really changed)
self.pl.replace_profile('foo', self.dummy_profile)
with self.assertRaises(AppArmorBug):
self.pl.replace_profile('/bin/foo', self.dummy_profile)
def testReplace_profile_error_1(self):
self.pl.add_profile('/etc/apparmor.d/bin.foo', 'foo', '/bin/foo', self.dummy_profile)
dummy2 = deepcopy(self.dummy_profile)
dummy2['attachment'] = 'changed'
with self.assertRaises(AppArmorBug):
self.pl.replace_profile('foo', dummy2) # changed attachment
def testReplace_profile_error_2(self):
self.pl.add_profile('/etc/apparmor.d/bin.foo', 'foo', '/bin/foo', self.dummy_profile)
with self.assertRaises(AppArmorBug):
self.pl.replace_profile('foo', []) # [] is wrong type
class TestFilename_from_profile_name(AATest):
tests = (
@ -467,9 +508,12 @@ class TestGet_profile_and_childs(AATest):
self.pl.add_profile('/etc/apparmor.d/bin.foo', 'foo//xy', '/bin/foo//xy', self.dummy_profile)
expected = ['foo', 'foo//bar', 'foo//xy']
self.assertEqual(list(self.pl.get_profile_and_childs('foo')), expected)
# while on it, also test get_all_profiles()
all_profiles = ['bafoo', 'foo', 'foobar', 'foo//bar', 'foo//xy']
self.assertEqual(list(self.pl.get_all_profiles()), all_profiles)
setup_aa(apparmor.aa)
setup_all_loops(__name__)

View file

@ -1,7 +1,7 @@
#! /usr/bin/python3
# ------------------------------------------------------------------
#
# Copyright (C) 2017-2021 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2017-2024 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@ -141,29 +141,30 @@ class AaTest_repr(AATest):
class AaTest_parse_profile_start(AATest):
tests = (
# profile start line profile hat name profile hat attachment xattrs flags pps_set_hat_external
(('/foo {', None, None), ('/foo', '/foo', '/foo', '', '', None, False)),
(('/foo (complain) {', None, None), ('/foo', '/foo', '/foo', '', '', 'complain', False)),
(('profile foo /foo {', None, None), ('foo', 'foo', 'foo', '/foo', '', None, False)), # named profile
(('profile /foo {', '/bar', None), ('/foo', '/bar', '/foo', '', '', None, False)), # child profile
(('/foo//bar {', None, None), ('/foo//bar', '/foo', 'bar', '', '', None, True)), # external hat
(('profile "/foo" (complain) {', None, None), ('/foo', '/foo', '/foo', '', '', 'complain', False)),
(('profile "/foo" xattrs=(user.bar=bar) {', None, None), ('/foo', '/foo', '/foo', '', 'user.bar=bar', None, False)),
(('profile "/foo" xattrs=(user.bar=bar user.foo=*) {', None, None), ('/foo', '/foo', '/foo', '', 'user.bar=bar user.foo=*', None, False)),
(('/usr/bin/xattrs-test xattrs=(myvalue="foo.bar") {', None, None), ('/usr/bin/xattrs-test', '/usr/bin/xattrs-test', '/usr/bin/xattrs-test', '', 'myvalue="foo.bar"', None, False)),
# profile start line profile hat parent name profile hat attachment xattrs flags pps_set_hat_external
(('/foo {', None, None), ('', '/foo', '/foo', '/foo', '', '', None, False)),
(('/foo (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', '', 'complain', False)),
(('profile foo /foo {', None, None), ('', 'foo', 'foo', 'foo', '/foo', '', None, False)), # named profile
(('profile /foo {', '/bar', None), ('/bar', '/foo', '/bar', '/foo', '', '', None, False)), # child profile
(('/foo//bar {', None, None), ('', '/foo//bar', '/foo', 'bar', '', '', None, True)), # external hat
(('profile "/foo" (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', '', 'complain', False)),
(('profile "/foo" xattrs=(user.bar=bar) {', None, None), ('', '/foo', '/foo', '/foo', '', 'user.bar=bar', None, False)),
(('profile "/foo" xattrs=(user.bar=bar user.foo=*) {', None, None), ('', '/foo', '/foo', '/foo', '', 'user.bar=bar user.foo=*', None, False)),
(('/usr/bin/xattrs-test xattrs=(myvalue="foo.bar") {', None, None), ('', '/usr/bin/xattrs-test', '/usr/bin/xattrs-test', '/usr/bin/xattrs-test', '', 'myvalue="foo.bar"', None, False)),
)
def _run_test(self, params, expected):
(profile, hat, prof_storage) = ProfileStorage.parse(params[0], 'somefile', 1, params[1], params[2])
self.assertEqual(prof_storage['name'], expected[0])
self.assertEqual(profile, expected[1])
self.assertEqual(hat, expected[2])
self.assertEqual(prof_storage['attachment'], expected[3])
self.assertEqual(prof_storage['xattrs'], expected[4])
self.assertEqual(prof_storage['flags'], expected[5])
self.assertEqual(prof_storage['parent'], expected[0])
self.assertEqual(prof_storage['name'], expected[1])
self.assertEqual(profile, expected[2])
self.assertEqual(hat, expected[3])
self.assertEqual(prof_storage['attachment'], expected[4])
self.assertEqual(prof_storage['xattrs'], expected[5])
self.assertEqual(prof_storage['flags'], expected[6])
self.assertEqual(prof_storage['is_hat'], False)
self.assertEqual(prof_storage['external'], expected[6])
self.assertEqual(prof_storage['external'], expected[7])
class AaTest_parse_profile_start_errors(AATest):