2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-08-22 10:07:12 +00:00

Merge branch apparmor:master into master

This commit is contained in:
Julia Sarris 2025-06-02 13:50:13 +00:00
commit 3356f5ea0a
22 changed files with 868 additions and 201 deletions

View File

@ -1,3 +1,9 @@
spec:
inputs:
build-test-images:
default: false
type: boolean
description: Explicitly build virtual machine images used by integration tests.
--- ---
image: ubuntu:latest image: ubuntu:latest
@ -269,8 +275,7 @@ image-ubuntu-cloud-24.04-x86_64:
- .image-garden.mk - .image-garden.mk
- .gitlab-ci.yml - .gitlab-ci.yml
compare_to: "refs/heads/master" compare_to: "refs/heads/master"
- if: $CI_COMMIT_BRANCH - if: $CI_COMMIT_BRANCH && "$[[ inputs.build-test-images ]]" == "true"
when: manual
.spread-x86_64: .spread-x86_64:
extends: .image-garden-x86_64 extends: .image-garden-x86_64

View File

@ -15,6 +15,7 @@ PYTHON_DIST_BUILD_PATH = '$(builddir)/../build/$$($(PYTHON) buildpath.py)'
TESTS = test_python.py TESTS = test_python.py
TESTS_ENVIRONMENT = \ TESTS_ENVIRONMENT = \
LD_LIBRARY_PATH='$(top_builddir)/src/.libs:$(PYTHON_DIST_BUILD_PATH)' \ LD_LIBRARY_PATH='$(top_builddir)/src/.libs:$(PYTHON_DIST_BUILD_PATH)' \
PYTHONPATH='$(PYTHON_DIST_BUILD_PATH)' PYTHONPATH='$(PYTHON_DIST_BUILD_PATH)' \
PYTHONDONTWRITEBYTECODE='1'
endif endif

46
profiles/apparmor.d/dig Normal file
View File

@ -0,0 +1,46 @@
#------------------------------------------------------------------
# Copyright (C) 2025 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License published by the Free Software Foundation.
#------------------------------------------------------------------
# vim: ft=apparmor
#
abi <abi/4.0>,
include <tunables/global>
profile dig /usr/bin/dig {
include <abstractions/base>
include <abstractions/nameservice-strict>
include <abstractions/private-files-strict>
include <abstractions/ssl_certs>
/usr/bin/dig mr,
network inet dgram,
network inet6 dgram,
network inet stream,
network inet6 stream,
capability dac_override,
capability dac_read_search,
# +trace
network (create,bind,getattr,send,receive) netlink raw,
file r /proc/version_signature,
# -f, -k, +tls-ca, +tls-certfile, +tls-keyfile
file r @{HOME}/[^.]**,
owner rw @{HOME}/.dig/**,
## denied by private-files-strict
priority=1 owner r @{HOME}/.digrc,
# Site-specific additions and overrides. See local/README for details.
include if exists <local/dig>
}

View File

@ -11,11 +11,12 @@ profile fusermount3 /usr/bin/fusermount3 {
# Allow both rw and ro type mounts (e.g. AppImage uses ro) # Allow both rw and ro type mounts (e.g. AppImage uses ro)
#MS_DIRSYNC, MS_NOATIME, MS_NODIRATIME, MS_NOEXEC, MS_SYNCHRONOUS, MS_NOSYMFOLLOW #MS_DIRSYNC, MS_NOATIME, MS_NODIRATIME, MS_NOEXEC, MS_SYNCHRONOUS, MS_NOSYMFOLLOW
mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,sync) -> @{HOME}/**/, # Below broad mount flags should be revisited once we have rule delegation
mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,sync) -> /mnt/{,**/}, mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,noexec,sync) -> @{HOME}/**/,
mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,sync) -> @{run}/user/@{uid}/**/, mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,noexec,sync) -> /mnt/{,**/},
mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,sync) -> /media/**/, mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,noexec,sync) -> @{run}/user/@{uid}/**/,
mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,sync) -> /tmp/**/, mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,noexec,sync) -> /media/**/,
mount fstype=@{fuse_types} options=(nosuid,nodev) options in (ro,rw,noatime,dirsync,nodiratime,noexec,sync) -> /tmp/**/,
# Cern VM fs is special and only uses these exact flags # Cern VM fs is special and only uses these exact flags
mount fstype=@{fuse_types} options=(nosuid,nodev,ro) -> /cvmfs/**/, mount fstype=@{fuse_types} options=(nosuid,nodev,ro) -> /cvmfs/**/,

View File

@ -17,6 +17,7 @@ profile hwctl /usr/bin/hwctl {
include <abstractions/nameservice-strict> include <abstractions/nameservice-strict>
include <abstractions/openssl> include <abstractions/openssl>
include <abstractions/ssl_certs> include <abstractions/ssl_certs>
include <abstractions/consoles>
network inet dgram, network inet dgram,
network inet6 dgram, network inet6 dgram,
@ -28,21 +29,14 @@ profile hwctl /usr/bin/hwctl {
/sys/firmware/dmi/tables/* r, # for collecting SMBIOS info /sys/firmware/dmi/tables/* r, # for collecting SMBIOS info
/sys/devices/system/cpu/cpufreq/policy*/cpuinfo_max_freq r, /sys/devices/system/cpu/cpufreq/policy*/cpuinfo_max_freq r,
/sys/fs/cgroup/**/cpu.max r, /sys/fs/cgroup/{,**/}cpu.max r,
@{PROC}/version r, @{PROC}/version r,
@{PROC}/@{pid}/cgroup r, @{PROC}/@{pid}/cgroup r,
# for collecting OS information # for collecting OS information
/usr/bin/{dpkg,kmod} cx, /usr/bin/kmod cx,
/usr/bin/lsb_release Px -> lsb_release, /etc/os-release r,
profile dpkg /usr/bin/dpkg {
include <abstractions/base>
@{exec_path} r,
/etc/dpkg/** r,
}
profile kmod /usr/bin/kmod { profile kmod /usr/bin/kmod {
include <abstractions/base> include <abstractions/base>

View File

@ -212,6 +212,18 @@ test_nonfs_options_equals_in() {
remove_mnt remove_mnt
} }
test_nonfs_options_equals_in_ext() {
# args: option=$1, option in $2, $3=pass/fail, mount -o $4
if [ "$(parser_supports "mount options=($1) options in ($2),")" != "true" ] ; then
echo " not supported by parser - skipping mount options=($1) options in ($2),"
return
fi
genprofile cap:sys_admin "mount:options=($1) options in ($2)"
runchecktest "MOUNT (confined cap mount option=$1 option in $2 ($4))" $3 mount ${loop_device} ${mount_point} -o $4
remove_mnt
}
test_dir_options() { test_dir_options() {
if [ "$(parser_supports "mount options=($1),")" != "true" ] ; then if [ "$(parser_supports "mount options=($1),")" != "true" ] ; then
echo " not supported by parser - skipping mount option=($1)," echo " not supported by parser - skipping mount option=($1),"
@ -288,6 +300,9 @@ test_options() {
# TODO: expand this to cover more mount flag combinations # TODO: expand this to cover more mount flag combinations
test_nonfs_options_equals_in 'nosuid,nodev' 'noatime,noexec' test_nonfs_options_equals_in 'nosuid,nodev' 'noatime,noexec'
test_nonfs_options_equals_in_ext 'nosuid,nodev' 'noatime' 'fail' 'nosuid,nodev,noexec'
test_nonfs_options_equals_in_ext 'nosuid,nodev' 'noatime' 'fail' 'nosuid,nodev,noatime,noexec'
for i in "bind" "rbind" "move"; do for i in "bind" "rbind" "move"; do
test_dir_options $i test_dir_options $i
done done

View File

@ -22,7 +22,7 @@ include $(COMMONDIR)/Make.rules
PYTOOLS = aa-easyprof aa-genprof aa-logprof aa-cleanprof aa-mergeprof \ PYTOOLS = aa-easyprof aa-genprof aa-logprof aa-cleanprof aa-mergeprof \
aa-autodep aa-audit aa-complain aa-enforce aa-disable \ aa-autodep aa-audit aa-complain aa-enforce aa-disable \
aa-notify aa-unconfined aa-notify aa-unconfined aa-show-usage
TOOLS = ${PYTOOLS} aa-decode aa-remove-unknown TOOLS = ${PYTOOLS} aa-decode aa-remove-unknown
PYSETUP = python-tools-setup.py PYSETUP = python-tools-setup.py
PYMODULES = $(wildcard apparmor/*.py apparmor/rule/*.py) PYMODULES = $(wildcard apparmor/*.py apparmor/rule/*.py)

View File

@ -850,7 +850,7 @@ def main():
filter_group.add_argument('--filter.family', metavar='FAMILY', help=_('regular expression to match the network family')) filter_group.add_argument('--filter.family', metavar='FAMILY', help=_('regular expression to match the network family'))
filter_group.add_argument('--filter.socket', metavar='SOCKET', help=_('regular expression to match the network socket type')) filter_group.add_argument('--filter.socket', metavar='SOCKET', help=_('regular expression to match the network socket type'))
# If a TTY then assume running in test mode and fix output width # If not a TTY then assume running in test mode and fix output width
if not sys.stdout.isatty(): if not sys.stdout.isatty():
parser.formatter_class = lambda prog: argparse.HelpFormatter(prog, width=80) parser.formatter_class = lambda prog: argparse.HelpFormatter(prog, width=80)

155
utils/aa-show-usage Executable file
View File

@ -0,0 +1,155 @@
#! /usr/bin/python3
# ----------------------------------------------------------------------
# Copyright (C) 2025 Maxime Bélair <maxime.belair@canonical.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
#
import argparse
import glob
import re
import json
import sys
import os
from apparmor import aa
from apparmor.translations import init_translation
from apparmor.regex import expand_braces, resolve_variables
_ = init_translation()
MAX_RECURSION = 10
def has_matching_file(pattern, xattrs=None):
for p in expand_braces(pattern):
for path in glob.iglob(p, recursive=True):
if os.path.realpath(path) != os.path.abspath(path): # remove symlinks
continue
if not xattrs or all(os.getxattr(path, name) == val for name, val in xattrs.items()):
return path
return None
def display_profile_text(used, unused, show_matching_path):
if used:
print(_('Used profiles:'))
for (name, attach, path, match) in used:
print(_(' Profile {} for {} ({}) {}').format(name, attach, path, ('→ ' + match) if show_matching_path else ''))
if unused:
print(_('Unused profiles:'))
for (name, attach, path, match) in unused:
print(_(' Profile {} for {} ({}) ').format(name, attach, path))
def profiles_to_json(profiles):
result = []
for profile_name, attach, path, matching_path in profiles:
entry = {'name': profile_name, 'attach': attach, 'path': path}
if matching_path:
entry['matching_path'] = matching_path
result.append(entry)
return result
def display_profile_json(used, unused):
profiles = {}
profiles['version'] = 1 # JSON format version - increase if you change the json structure
if used:
profiles['used'] = profiles_to_json(used)
if unused:
profiles['unused'] = profiles_to_json(unused)
print(json.dumps(profiles, indent=2))
def filter_profile(path, profile_name, attach, prof_filter):
if prof_filter['flags'] and not prof_filter['flags'].match(aa.active_profiles.profiles[profile_name].data['flags'] or ''):
return False
if prof_filter['name'] and not prof_filter['name'].match(profile_name or ''):
return False
if prof_filter['attach'] and not prof_filter['attach'].match(attach or ''):
return False
if prof_filter['path'] and not prof_filter['path'].match(path or ''):
return False
return True
def get_used_profiles(args, prof_filter):
aa.init_aa(confdir=args.configdir or os.getenv('__AA_CONFDIR'), profiledir=args.dir)
aa.read_profiles()
used = []
unused = []
for a, v in aa.active_profiles.attachments.items():
filename = v['f']
profile_name = v['p']
if not filter_profile(filename, profile_name, a, prof_filter):
continue
var_dict = aa.active_profiles.get_all_merged_variables(filename, aa.include_list_recursive(aa.active_profiles.files[filename], True))
resolved = resolve_variables(a, var_dict)
matching_path = None
for entry in resolved:
matching_path = has_matching_file(entry)
if matching_path:
break
if matching_path and args.show_type != 'unused':
used.append((profile_name, a, filename, matching_path))
if not matching_path and args.show_type != 'used':
unused.append((profile_name, a, filename, matching_path))
return used, unused
def main():
parser = argparse.ArgumentParser(description=_('Check which profiles are used'))
parser.add_argument('-s', '--show-type', type=str, default='all', choices=['all', 'used', 'unused'], help=_('Type of profiles to show'))
parser.add_argument('-j', '--json', action='store_true', help=_('Output in JSON'))
parser.add_argument('-d', '--dir', type=str, help=_('Path to profiles'))
parser.add_argument('--show-matching-path', action='store_true', help=_('Show the path of a file matching the profile'))
parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
filter_group = parser.add_argument_group(_('Filtering options'),
description=(_('Filters are used to reduce the output of information to only '
'those entries that will match the filter. Filters use Python\'s regular '
'expression syntax.')))
filter_group.add_argument('--filter.flags', dest='filter_flags', metavar='FLAGS', help=_('Filter by flags'))
filter_group.add_argument('--filter.profile_name', dest='filter_name', metavar='PROFILE_NAME', help=_('Filter by profile name'))
filter_group.add_argument('--filter.profile_attach', dest='filter_attach', metavar='PROFILE_ATTACH', help=_('Filter by profile attachment'))
filter_group.add_argument('--filter.profile_path', dest='filter_path', metavar='PROFILE_PATH', help=_('Filter by profile path'))
# If not a TTY then assume running in test mode and fix output width
if not sys.stdout.isatty():
parser.formatter_class = lambda prog: argparse.HelpFormatter(prog, width=80)
args = parser.parse_args()
prof_filter = {
'flags': re.compile(args.filter_flags) if args.filter_flags else None,
'name': re.compile(args.filter_name) if args.filter_name else None,
'attach': re.compile(args.filter_attach) if args.filter_attach else None,
'path': re.compile(args.filter_path) if args.filter_path else None,
}
used, unused = get_used_profiles(args, prof_filter)
if args.json:
display_profile_json(used, unused)
else:
display_profile_text(used, unused, args.show_matching_path)
if __name__ == '__main__':
main()

106
utils/aa-show-usage.pod Normal file
View File

@ -0,0 +1,106 @@
# This publication is intellectual property of Canonical Ltd. Its contents
# can be duplicated, either in part or in whole, provided that a copyright
# label is visibly located on each copy.
#
# All information found in this book has been compiled with utmost
# attention to detail. However, this does not guarantee complete accuracy.
# Neither Canonical Ltd, the authors, nor the translators shall be held
# liable for possible errors or the consequences thereof.
#
# Many of the software and hardware descriptions cited in this book
# are registered trademarks. All trade names are subject to copyright
# restrictions and may be registered trade marks. Canonical Ltd
# essentially adheres to the manufacturer's spelling.
#
# Names of products and trademarks appearing in this book (with or without
# specific notation) are likewise subject to trademark and trade protection
# laws and may thus fall under copyright restrictions.
#
=pod
=head1 NAME
aa-show-usage - Check which profiles are used
=head1 SYNOPSIS
B<aa-show-usage> [option]
=head1 DESCRIPTION
B<aa-show-usage> will display data on the usage of the profiles of the system
=head1 OPTIONS
B<aa-show-usage> accepts the following arguments:
=over 4
=item -s, --show-type {all,used,unused}
Type of profiles to show
=item -j, --json
Output in JSON
=item -d, --dir
Path to profiles
=item --show-matching-path
Show the path of a file matching the profile. Only the first matching path of an executable is shown (not the whole list).
=back
=head1 FILTERING OPTIONS
B<aa-show-usage> accepts the following filters to reduce the output of information to only those entries that will match the filter. Filters use Python's regular expression syntax.
=over 4
=item --filter.flags FLAGS
Filter by flags
=item --filter.profile_name PROFILE_NAME
Filter by profile name
=item --filter.profile_attach PROFILE_ATTACH
Filter by profile attachement (i.e. by path of the executable to which this profile applies)
=item --filter.profile_path PROFILE_PATH
Filter by profile path
=back
=head1 EXAMPLES
Show both the list of used and unused profiles in your system.
$ aa-show-usage
Show the list of unconfined profiles currently used by your system.
$ aa-show-usage --show-type=used --filter.flags=unconfined
=head1 BUGS
B<aa-show-usage> needs to be able to read profiles to tell whether they are used in practice.
B<aa-show-usage> will only report directly used profiles. Profiles used via profile transitions, systemd's B<AppArmorProfile=>, or API call such as B<aa-exec> are not shown as used.
If you find any additional bugs, please report them at L<https://gitlab.com/apparmor/apparmor/-/issues>.
=head1 SEE ALSO
apparmor(7)
=cut

View File

@ -37,7 +37,7 @@ from apparmor.profile_storage import ProfileStorage, add_or_remove_flag, ruletyp
from apparmor.regex import ( from apparmor.regex import (
RE_HAS_COMMENT_SPLIT, RE_PROFILE_CHANGE_HAT, RE_PROFILE_CONDITIONAL, RE_HAS_COMMENT_SPLIT, RE_PROFILE_CHANGE_HAT, RE_PROFILE_CONDITIONAL,
RE_PROFILE_CONDITIONAL_BOOLEAN, RE_PROFILE_CONDITIONAL_VARIABLE, RE_PROFILE_END, RE_PROFILE_CONDITIONAL_BOOLEAN, RE_PROFILE_CONDITIONAL_VARIABLE, RE_PROFILE_END,
RE_PROFILE_HAT_DEF, RE_PROFILE_START, RE_PROFILE_HAT_DEF, RE_PROFILE_START, RE_METADATA_LOGPROF_SUGGEST,
RE_RULE_HAS_COMMA, parse_profile_start_line, re_match_include) RE_RULE_HAS_COMMA, parse_profile_start_line, re_match_include)
from apparmor.rule.abi import AbiRule from apparmor.rule.abi import AbiRule
from apparmor.rule.file import FileRule from apparmor.rule.file import FileRule
@ -1420,8 +1420,20 @@ def match_includes(profile, rule_type, rule_obj):
# XXX type check should go away once we init all profiles correctly # XXX type check should go away once we init all profiles correctly
if valid_include(incname) and include[incname][incname][rule_type].is_covered(rule_obj): if valid_include(incname) and include[incname][incname][rule_type].is_covered(rule_obj):
if include[incname][incname]['logprof_suggest'] != 'no': sug = include[incname][incname]['logprof_suggest'].split()
if sug == []:
newincludes.append(rel_incname) newincludes.append(rel_incname)
elif sug[0] == 'no':
continue
else:
for s in sug:
try:
if re.match(s, profile.data['name']):
newincludes.append(rel_incname)
break
except re.error as err:
aaui.UI_Important(_('WARNING: Invalid regex \'%s\' in abstraction %s: %s.'
% (s, rel_incname, err)))
return newincludes return newincludes
@ -1832,10 +1844,11 @@ def parse_profile_data(data, file, do_include, in_preamble):
else: else:
initial_comment = initial_comment + line + '\n' initial_comment = initial_comment + line + '\n'
if line.startswith('# LOGPROF-SUGGEST:'): # TODO: allow any number of spaces/tabs after '#' if RE_METADATA_LOGPROF_SUGGEST.search(line):
parts = line.split() # - logprof_suggest is a set of space-separated regexes
if len(parts) > 2: # - If this metadata is present, the abstraction is only proposed to logprof if at least one regex is matched
profile_data[profname]['logprof_suggest'] = parts[2] # - If this abstraction should not be proposed to any profile, it is possible to tell #LOGPROF-SUGGEST: no
profile_data[profname]['logprof_suggest'] = RE_METADATA_LOGPROF_SUGGEST.search(line).group('suggest')
# keep line as part of initial_comment (if we ever support writing abstractions, we should update serialize_profile()) # keep line as part of initial_comment (if we ever support writing abstractions, we should update serialize_profile())
initial_comment = initial_comment + line + '\n' initial_comment = initial_comment + line + '\n'

View File

@ -15,6 +15,7 @@ import logging
import os import os
import re import re
import subprocess import subprocess
import signal
import sys import sys
import termios import termios
import tty import tty
@ -111,18 +112,35 @@ def recursive_print(src, dpth=0, key=''):
print(tabs + '- %s' % src) print(tabs + '- %s' % src)
def cmd(command): def subprocess_setup():
"""Try to execute the given command.""" # Python installs a SIGPIPE handler by default. This is usually not what
# non-Python subprocesses expect.
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
def cmd_pipe_stderr(command, stdin=None):
return cmd(command, stderr=subprocess.PIPE, output_fmt='utf-8', stdin=stdin)
def cmd(command, stderr=subprocess.STDOUT, output_fmt='ascii', stdin=None):
"""Execute the given command and return its output as a tuple (returncode, output)."""
debug(command) debug(command)
try: try:
sp = subprocess.Popen(command, stdout=subprocess.PIPE, sp = subprocess.Popen(command, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr, close_fds=True, preexec_fn=subprocess_setup)
stderr=subprocess.STDOUT)
except OSError as ex: except OSError as ex:
return [127, str(ex)] return [127, str(ex)]
out = sp.communicate()[0].decode('ascii', 'ignore') out, err = sp.communicate() # TODO input???
return [sp.returncode, out] if err:
out = err
if output_fmt == 'ascii':
decoded = out.decode(output_fmt, 'ignore')
else:
decoded = out.decode(output_fmt)
return [sp.returncode, decoded]
def cmd_pipe(command1, command2): def cmd_pipe(command1, command2):

View File

@ -14,12 +14,11 @@ import json
import optparse import optparse
import os import os
import re import re
import subprocess
import sys import sys
from shutil import which from shutil import which
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from apparmor.common import AppArmorException, open_file_read from apparmor.common import AppArmorException, open_file_read, cmd
DEBUGGING = False DEBUGGING = False
@ -55,19 +54,6 @@ def msg(out, output=sys.stdout):
pass pass
def cmd(command):
"""Try to execute the given command."""
debug(command)
try:
sp = subprocess.Popen(command, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
except OSError as ex:
return [127, str(ex)]
out = sp.communicate()[0]
return [sp.returncode, out]
def debug(out): def debug(out):
"""Print debug message""" """Print debug message"""
if DEBUGGING: if DEBUGGING:

View File

@ -13,9 +13,10 @@
# #
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
from types import NoneType
from apparmor.common import AppArmorBug, AppArmorException from apparmor.common import AppArmorBug, AppArmorException
from apparmor.regex import parse_profile_start_line from apparmor.regex import parse_profile_start_line, re_print_dict
from apparmor.rule import quote_if_needed from apparmor.rule import quote_if_needed
from apparmor.rule.abi import AbiRule, AbiRuleset from apparmor.rule.abi import AbiRule, AbiRuleset
from apparmor.rule.all import AllRule, AllRuleset from apparmor.rule.all import AllRule, AllRuleset
@ -79,7 +80,7 @@ class ProfileStorage:
data['parent'] = '' # parent profile, or '' for top-level profiles and external hats data['parent'] = '' # parent profile, or '' for top-level profiles and external hats
data['name'] = '' data['name'] = ''
data['attachment'] = '' data['attachment'] = ''
data['xattrs'] = '' data['xattrs'] = {}
data['flags'] = '' data['flags'] = ''
data['external'] = False data['external'] = False
data['header_comment'] = '' # comment in the profile/hat start line data['header_comment'] = '' # comment in the profile/hat start line
@ -100,28 +101,16 @@ class ProfileStorage:
if key not in self.data: if key not in self.data:
raise AppArmorBug('attempt to set unknown key %s' % key) raise AppArmorBug('attempt to set unknown key %s' % key)
# allow writing bool values allowed_types = {bool, str, dict, None, NoneType}
if isinstance(self.data[key], bool): old_type = type(self.data[key])
if isinstance(value, bool): if old_type in allowed_types:
if key in {'flags', 'filename'} and type(value) in {str, NoneType}:
self.data[key] = value
elif isinstance(value, old_type):
self.data[key] = value self.data[key] = value
else: else:
raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, type(self.data[key]), type(value), value)) raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, old_type, type(value), value))
# allow writing str or None to some keys
elif key in ('flags', 'filename'):
if isinstance(value, str) or value is None:
self.data[key] = value self.data[key] = value
else:
raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, type(self.data[key]), type(value), value))
# allow writing str values
elif isinstance(self.data[key], str):
if isinstance(value, str):
self.data[key] = value
else:
raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, type(self.data[key]), type(value), value))
# don't allow overwriting of other types
else: else:
raise AppArmorBug('Attempt to overwrite "%s" with %s, type %s' % (key, value, type(value))) raise AppArmorBug('Attempt to overwrite "%s" with %s, type %s' % (key, value, type(value)))
@ -168,7 +157,7 @@ class ProfileStorage:
xattrs = '' xattrs = ''
if self.data['xattrs']: if self.data['xattrs']:
xattrs = ' xattrs=(%s)' % self.data['xattrs'] xattrs = ' xattrs=(%s)' % re_print_dict(self.data['xattrs'])
flags = '' flags = ''
if self.data['flags']: if self.data['flags']:
@ -263,7 +252,7 @@ class ProfileStorage:
else: else:
prof_storage['profile_keyword'] = matches['profile_keyword'] prof_storage['profile_keyword'] = matches['profile_keyword']
prof_storage['attachment'] = matches['attachment'] or '' prof_storage['attachment'] = matches['attachment'] or ''
prof_storage['xattrs'] = matches['xattrs'] or '' prof_storage['xattrs'] = matches['xattrs'] or {}
return (profile, hat, prof_storage) return (profile, hat, prof_storage)

View File

@ -14,6 +14,7 @@
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
import re import re
import itertools
from apparmor.common import AppArmorBug, AppArmorException from apparmor.common import AppArmorBug, AppArmorException
from apparmor.translations import init_translation from apparmor.translations import init_translation
@ -27,12 +28,16 @@ RE_COMMA_EOL = r'\s*,' + RE_EOL # optional whitespace, comma + RE_EOL
RE_PROFILE_NAME = r'(?P<%s>(\S+|"[^"]+"))' # string without spaces, or quoted string. %s is the match group name RE_PROFILE_NAME = r'(?P<%s>(\S+|"[^"]+"))' # string without spaces, or quoted string. %s is the match group name
RE_PATH = r'/\S*|"/[^"]*"' # filename (starting with '/') without spaces, or quoted filename. RE_PATH = r'/\S*|"/[^"]*"' # filename (starting with '/') without spaces, or quoted filename.
RE_VAR = r'@{[^}\s]+}'
RE_DICT_ENTRY = r'\s*(?P<key>[^,\s=]+)(?:=(?P<value>[^,\s=]+))?\s*'
RE_PROFILE_PATH = '(?P<%s>(' + RE_PATH + '))' # quoted or unquoted filename. %s is the match group name RE_PROFILE_PATH = '(?P<%s>(' + RE_PATH + '))' # quoted or unquoted filename. %s is the match group name
RE_PROFILE_PATH_OR_VAR = '(?P<%s>(' + RE_PATH + r'|@{\S+}\S*|"@{\S+}[^"]*"))' # quoted or unquoted filename or variable. %s is the match group name RE_PROFILE_PATH_OR_VAR = '(?P<%s>(' + RE_PATH + '|' + RE_VAR + r'\S*|"' + RE_VAR + '[^"]*"))' # quoted or unquoted filename or variable. %s is the match group name
RE_SAFE_OR_UNSAFE = '(?P<execmode>(safe|unsafe))' RE_SAFE_OR_UNSAFE = '(?P<execmode>(safe|unsafe))'
RE_XATTRS = r'(\s+xattrs\s*=\s*\((?P<xattrs>([^)=]+(=[^)=]+)?\s?)+)\)\s*)?' RE_XATTRS = r'(\s+xattrs\s*=\s*\((?P<xattrs>([^)=]+(=[^)=]+)?\s?)*)\)\s*)?'
RE_FLAGS = r'(\s+(flags\s*=\s*)?\((?P<flags>[^)]+)\))?' RE_FLAGS = r'(\s+(flags\s*=\s*)?\((?P<flags>[^)]+)\))?'
RE_VARIABLE = re.compile(RE_VAR)
RE_PROFILE_END = re.compile(r'^\s*\}' + RE_EOL) RE_PROFILE_END = re.compile(r'^\s*\}' + RE_EOL)
RE_PROFILE_ALL = re.compile(RE_PRIORITY_AUDIT_DENY + r'all' + RE_COMMA_EOL) RE_PROFILE_ALL = re.compile(RE_PRIORITY_AUDIT_DENY + r'all' + RE_COMMA_EOL)
RE_PROFILE_CAP = re.compile(RE_PRIORITY_AUDIT_DENY + r'capability(?P<capability>(\s+\S+)+)?' + RE_COMMA_EOL) RE_PROFILE_CAP = re.compile(RE_PRIORITY_AUDIT_DENY + r'capability(?P<capability>(\s+\S+)+)?' + RE_COMMA_EOL)
@ -56,6 +61,8 @@ RE_PROFILE_USERNS = re.compile(RE_PRIORITY_AUDIT_DENY + r'(userns\s*,|userns(?P<
RE_PROFILE_MQUEUE = re.compile(RE_PRIORITY_AUDIT_DENY + r'(mqueue\s*,|mqueue(?P<details>\s+[^#]*)\s*,)' + RE_EOL) RE_PROFILE_MQUEUE = re.compile(RE_PRIORITY_AUDIT_DENY + r'(mqueue\s*,|mqueue(?P<details>\s+[^#]*)\s*,)' + RE_EOL)
RE_PROFILE_IO_URING = re.compile(RE_PRIORITY_AUDIT_DENY + r'(io_uring\s*,|io_uring(?P<details>\s+[^#]*)\s*,)' + RE_EOL) RE_PROFILE_IO_URING = re.compile(RE_PRIORITY_AUDIT_DENY + r'(io_uring\s*,|io_uring(?P<details>\s+[^#]*)\s*,)' + RE_EOL)
RE_METADATA_LOGPROF_SUGGEST = re.compile(r'^\s*#\s*LOGPROF-SUGGEST\s*:\s*(?P<suggest>.*)$')
# match anything that's not " or #, or matching quotes with anything except quotes inside # match anything that's not " or #, or matching quotes with anything except quotes inside
__re_no_or_quoted_hash = '([^#"]|"[^"]*")*' __re_no_or_quoted_hash = '([^#"]|"[^"]*")*'
@ -162,6 +169,10 @@ def parse_profile_start_line(line, filename):
else: else:
result['profile'] = result['namedprofile'] result['profile'] = result['namedprofile']
result['profile_keyword'] = True result['profile_keyword'] = True
if 'xattrs' in result:
result['xattrs'] = re_parse_dict(result['xattrs'])
else:
result['xattrs'] = {}
return result return result
@ -235,6 +246,30 @@ def re_match_include(line):
return None return None
def re_parse_dict(raw):
"""returns a dict where entries are comma or space separated"""
result = {}
if not raw:
return result
for key, value in re.findall(RE_DICT_ENTRY, raw):
if value == '':
value = None
result[key] = value
return result
def re_print_dict(d):
parts = []
for k, v in sorted(d.items()):
if v:
parts.append("{}={}".format(k, v))
else:
parts.append(k)
return " ".join(parts)
def strip_parenthesis(data): def strip_parenthesis(data):
"""strips parenthesis from the given string and returns the strip()ped result. """strips parenthesis from the given string and returns the strip()ped result.
The parenthesis must be the first and last char, otherwise they won't be removed. The parenthesis must be the first and last char, otherwise they won't be removed.
@ -251,3 +286,93 @@ def strip_quotes(data):
return data[1:-1] return data[1:-1]
else: else:
return data return data
def expand_var(var, var_dict, seen_vars):
if var in seen_vars:
raise AppArmorException(_('Circular dependency detected for variable {}').format(var))
if var not in var_dict:
raise AppArmorException(_('Trying to reference non-existing variable {}').format(var))
resolved = []
for val in var_dict[var]:
resolved.extend(expand_string(val, var_dict, seen_vars | {var}))
return resolved
def expand_string(s, var_dict, seen_vars):
matches = list(RE_VARIABLE.finditer(s))
if not matches:
return [s]
parts = []
last_idx = 0
for match in matches:
start, end = match.span()
if start > last_idx:
parts.append([s[last_idx:start]])
var_name = match.group(0)
parts.append(expand_var(var_name, var_dict, seen_vars))
last_idx = end
if last_idx < len(s):
parts.append([s[last_idx:]])
return [''.join(p) for p in itertools.product(*parts)]
def resolve_variables(s, var_dict):
return expand_string(s, var_dict, set())
# This function could be replaced by braceexpand.braceexpand
# It exists to avoid relying on an external python package.
def expand_braces(s):
i = s.find('{')
if i == -1:
if '}' in s:
raise AppArmorException('Unbalanced braces in pattern {}'.format(s))
return [s]
level = 0
for j in range(i, len(s)):
if s[j] == '{':
level += 1
elif s[j] == '}':
level -= 1
if level == 0:
break
else:
raise AppArmorException('Unbalanced braces in pattern {}'.format(s))
prefix = s[:i]
group = s[i + 1:j]
suffix = s[j + 1:]
# Split group on commas at the top level (i.e. not inside nested braces)
alts = []
curr = ''
nested = 0
for char in group:
if char == ',' and nested == 0:
alts.append(curr)
curr = ""
else:
if char == '{':
nested += 1
elif char == '}':
nested -= 1
curr += char
alts.append(curr)
# Recursively combine prefix, each alternative, and suffix
results = []
for alt in alts:
for expansion in expand_braces(prefix + alt + suffix):
results.append(expansion)
if len(results) <= 1:
raise AppArmorException('Braces should provide at least two alternatives, found {}: {}'.format(len(results), s))
return results

View File

@ -78,7 +78,7 @@ $bar = true
allow /home/foo/bar r, allow /home/foo/bar r,
} }
/what/ever/xattr xattrs=( foo=bar ) flags=( complain ) { /what/ever/xattr xattrs=(foo=bar) flags=( complain ) {
/what/ever r, /what/ever r,
} }

View File

@ -10,62 +10,22 @@
# ------------------------------------------------------------------ # ------------------------------------------------------------------
import os import os
import signal
import subprocess
import unittest import unittest
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from apparmor.common import cmd_pipe_stderr
# The location of the aa-decode utility can be overridden by setting # The location of the aa-decode utility can be overridden by setting
# the APPARMOR_DECODE environment variable; this is useful for running # the APPARMOR_DECODE environment variable; this is useful for running
# these tests in an installed environment # these tests in an installed environment
aadecode_bin = "../aa-decode" aadecode_bin = "../aa-decode"
# http://www.chiark.greenend.org.uk/ucgi/~cjwatson/blosxom/2009-07-02-python-sigpipe.html
# This is needed so that the subprocesses that produce endless output
# actually quit when the reader goes away.
def subprocess_setup():
# Python installs a SIGPIPE handler by default. This is usually not what
# non-Python subprocesses expect.
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
# Define only arguments that are actually ever used: command and stdin
def cmd(command, stdin=None):
"""Try to execute given command (array) and return its stdout, or return
a textual error if it failed."""
try:
sp = subprocess.Popen(
command,
stdin=stdin,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=True,
preexec_fn=subprocess_setup
)
except OSError as e:
return 127, str(e)
stdout, stderr = sp.communicate(input)
# If there was some error output, show that instead of stdout to ensure
# test fails and does not mask potentially major warnings and errors.
if stderr:
out = stderr
else:
out = stdout
return sp.returncode, out.decode('utf-8')
class AADecodeTest(unittest.TestCase): class AADecodeTest(unittest.TestCase):
def test_help(self): def test_help(self):
"""Test --help argument""" """Test --help argument"""
expected = 0 expected = 0
rc, report = cmd((aadecode_bin, "--help")) rc, report = cmd_pipe_stderr((aadecode_bin, "--help"))
result = 'Got exit code {}, expected {}\n'.format(rc, expected) result = 'Got exit code {}, expected {}\n'.format(rc, expected)
self.assertEqual(expected, rc, result + report) self.assertEqual(expected, rc, result + report)
@ -80,7 +40,7 @@ class AADecodeTest(unittest.TestCase):
temp_file.write(content) temp_file.write(content)
temp_file.flush() temp_file.flush()
temp_file.seek(0) temp_file.seek(0)
rc, report = cmd((aadecode_bin,), stdin=temp_file) rc, report = cmd_pipe_stderr(aadecode_bin, stdin=temp_file)
result = 'Got exit code {}, expected {}\n'.format(rc, expected_return_code) result = 'Got exit code {}, expected {}\n'.format(rc, expected_return_code)
self.assertEqual(expected_return_code, rc, result + report) self.assertEqual(expected_return_code, rc, result + report)
@ -95,7 +55,7 @@ class AADecodeTest(unittest.TestCase):
expected_output = 'Decoded: /tmp/foo bar' expected_output = 'Decoded: /tmp/foo bar'
test_code = '2F746D702F666F6F20626172' test_code = '2F746D702F666F6F20626172'
rc, report = cmd((aadecode_bin, test_code)) rc, report = cmd_pipe_stderr((aadecode_bin, test_code))
result = 'Got exit code {}, expected {}\n'.format(rc, expected) result = 'Got exit code {}, expected {}\n'.format(rc, expected)
self.assertEqual(expected, rc, result + report) self.assertEqual(expected, rc, result + report)
result = 'Got output "{}", expected "{}"\n'.format(report, expected_output) result = 'Got output "{}", expected "{}"\n'.format(report, expected_output)

View File

@ -13,7 +13,6 @@
import os import os
import pwd import pwd
import signal import signal
import subprocess
import sys import sys
import time import time
import unittest import unittest
@ -21,6 +20,7 @@ from tempfile import NamedTemporaryFile
from datetime import datetime from datetime import datetime
import apparmor.aa as aa import apparmor.aa as aa
from apparmor.common import cmd_pipe_stderr
from common_test import AATest, setup_aa, setup_all_loops from common_test import AATest, setup_aa, setup_all_loops
# The location of the aa-notify utility can be overridden by setting # The location of the aa-notify utility can be overridden by setting
@ -38,34 +38,6 @@ def subprocess_setup():
signal.signal(signal.SIGPIPE, signal.SIG_DFL) signal.signal(signal.SIGPIPE, signal.SIG_DFL)
def cmd(command):
"""Try to execute given command (array) and return its stdout, or return
a textual error if it failed."""
try:
sp = subprocess.Popen(
command,
stdin=None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=True,
preexec_fn=subprocess_setup
)
except OSError as e:
return 127, str(e)
stdout, stderr = sp.communicate(input)
# If there was some error output, show that instead of stdout to ensure
# test fails and does not mask potentially major warnings and errors.
if stderr:
out = stderr
else:
out = stdout
return sp.returncode, out.decode('utf-8')
class AANotifyBase(AATest): class AANotifyBase(AATest):
def create_logfile_contents(_time): def create_logfile_contents(_time):
@ -141,7 +113,7 @@ Feb 4 13:40:38 XPS-13-9370 kernel: [128552.880347] audit: type=1400 audit({epoc
if 'SUDO_USER' in os.environ: if 'SUDO_USER' in os.environ:
username = os.environ.get('SUDO_USER') username = os.environ.get('SUDO_USER')
return_code, output = cmd(['last', username, '--fullnames', '--time-format', 'iso']) return_code, output = cmd_pipe_stderr(['last', username, '--fullnames', '--time-format', 'iso'])
output = output.split('\n')[0] # the first line is enough output = output.split('\n')[0] # the first line is enough
# example of output (util-linux last command): # example of output (util-linux last command):
# ubuntu tty7 :0 2024-01-05T14:29:11-03:00 gone - no logout # ubuntu tty7 :0 2024-01-05T14:29:11-03:00 gone - no logout
@ -184,7 +156,7 @@ class AANotifyTest(AANotifyBase):
expected_return_code = 0 expected_return_code = 0
expected_output_has = 'usage: aa-notify' expected_output_has = 'usage: aa-notify'
return_code, output = cmd(aanotify_bin) return_code, output = cmd_pipe_stderr(aanotify_bin)
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
self.assertEqual(expected_return_code, return_code, result + output) self.assertEqual(expected_return_code, return_code, result + output)
result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has) result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has)
@ -262,7 +234,7 @@ Filtering options:
for patch in patches: for patch in patches:
expected_output_2 = expected_output_2.replace(patch[0], patch[1]) expected_output_2 = expected_output_2.replace(patch[0], patch[1])
return_code, output = cmd(aanotify_bin + ['--help']) return_code, output = cmd_pipe_stderr(aanotify_bin + ['--help'])
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
self.assertEqual(expected_return_code, return_code, result + output) self.assertEqual(expected_return_code, return_code, result + output)
@ -275,7 +247,7 @@ Filtering options:
expected_return_code = 0 expected_return_code = 0
expected_output_has = 'AppArmor denials: 20 (since' expected_output_has = 'AppArmor denials: 20 (since'
return_code, output = cmd(aanotify_bin + ['-f', self.test_logfile_current, '-s', '100']) return_code, output = cmd_pipe_stderr(aanotify_bin + ['-f', self.test_logfile_current, '-s', '100'])
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
self.assertEqual(expected_return_code, return_code, result + output) self.assertEqual(expected_return_code, return_code, result + output)
result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has) result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has)
@ -288,7 +260,7 @@ Filtering options:
expected_return_code = 0 expected_return_code = 0
expected_output_has = 'AppArmor denials: 10 (since' expected_output_has = 'AppArmor denials: 10 (since'
return_code, output = cmd(aanotify_bin + ['-f', self.test_logfile_last_login, '-l']) return_code, output = cmd_pipe_stderr(aanotify_bin + ['-f', self.test_logfile_last_login, '-l'])
if "ERROR: Could not find last login" in output: if "ERROR: Could not find last login" in output:
self.skipTest('Could not find last login') self.skipTest('Could not find last login')
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
@ -364,7 +336,7 @@ Logfile: {logfile}
AppArmor denials: 10 (since'''.format(logfile=self.test_logfile_last_login) # noqa: E128 AppArmor denials: 10 (since'''.format(logfile=self.test_logfile_last_login) # noqa: E128
return_code, output = cmd(aanotify_bin + ['-f', self.test_logfile_last_login, '-l', '-v']) return_code, output = cmd_pipe_stderr(aanotify_bin + ['-f', self.test_logfile_last_login, '-l', '-v'])
if "ERROR: Could not find last login" in output: if "ERROR: Could not find last login" in output:
self.skipTest('Could not find last login') self.skipTest('Could not find last login')
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
@ -399,7 +371,7 @@ class AANotifyProfileFilterTest(AANotifyBase):
expected_return_code = expected[0] expected_return_code = expected[0]
expected_output_has = expected[1] expected_output_has = expected[1]
return_code, output = cmd(aanotify_bin + days_params + params) return_code, output = cmd_pipe_stderr(aanotify_bin + days_params + params)
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
self.assertEqual(expected_return_code, return_code, result + output) self.assertEqual(expected_return_code, return_code, result + output)
result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has) result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has)
@ -430,7 +402,7 @@ class AANotifyProfileFilterTest(AANotifyBase):
expected_return_code = expected[0] expected_return_code = expected[0]
expected_output_has = expected[1] expected_output_has = expected[1]
return_code, output = cmd(aanotify_bin + login_params + params) return_code, output = cmd_pipe_stderr(aanotify_bin + login_params + params)
if 'ERROR: Could not find last login' in output: if 'ERROR: Could not find last login' in output:
self.skipTest('Could not find last login') self.skipTest('Could not find last login')
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
@ -463,7 +435,7 @@ class AANotifyOperationFilterTest(AANotifyBase):
expected_return_code = expected[0] expected_return_code = expected[0]
expected_output_has = expected[1] expected_output_has = expected[1]
return_code, output = cmd(aanotify_bin + days_params + params) return_code, output = cmd_pipe_stderr(aanotify_bin + days_params + params)
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
self.assertEqual(expected_return_code, return_code, result + output) self.assertEqual(expected_return_code, return_code, result + output)
result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has) result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has)
@ -492,7 +464,7 @@ class AANotifyOperationFilterTest(AANotifyBase):
expected_return_code = expected[0] expected_return_code = expected[0]
expected_output_has = expected[1] expected_output_has = expected[1]
return_code, output = cmd(aanotify_bin + login_params + params) return_code, output = cmd_pipe_stderr(aanotify_bin + login_params + params)
if 'ERROR: Could not find last login' in output: if 'ERROR: Could not find last login' in output:
self.skipTest('Could not find last login') self.skipTest('Could not find last login')
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
@ -528,7 +500,7 @@ class AANotifyNameFilterTest(AANotifyBase):
expected_return_code = expected[0] expected_return_code = expected[0]
expected_output_has = expected[1] expected_output_has = expected[1]
return_code, output = cmd(aanotify_bin + days_params + params) return_code, output = cmd_pipe_stderr(aanotify_bin + days_params + params)
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
self.assertEqual(expected_return_code, return_code, result + output) self.assertEqual(expected_return_code, return_code, result + output)
result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has) result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has)
@ -560,7 +532,7 @@ class AANotifyNameFilterTest(AANotifyBase):
expected_return_code = expected[0] expected_return_code = expected[0]
expected_output_has = expected[1] expected_output_has = expected[1]
return_code, output = cmd(aanotify_bin + login_params + params) return_code, output = cmd_pipe_stderr(aanotify_bin + login_params + params)
if 'ERROR: Could not find last login' in output: if 'ERROR: Could not find last login' in output:
self.skipTest('Could not find last login') self.skipTest('Could not find last login')
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
@ -593,7 +565,7 @@ class AANotifyDeniedFilterTest(AANotifyBase):
expected_return_code = expected[0] expected_return_code = expected[0]
expected_output_has = expected[1] expected_output_has = expected[1]
return_code, output = cmd(aanotify_bin + days_params + params) return_code, output = cmd_pipe_stderr(aanotify_bin + days_params + params)
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
self.assertEqual(expected_return_code, return_code, result + output) self.assertEqual(expected_return_code, return_code, result + output)
result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has) result = 'Got output "{}", expected "{}"\n'.format(output, expected_output_has)
@ -622,7 +594,7 @@ class AANotifyDeniedFilterTest(AANotifyBase):
expected_return_code = expected[0] expected_return_code = expected[0]
expected_output_has = expected[1] expected_output_has = expected[1]
return_code, output = cmd(aanotify_bin + login_params + params) return_code, output = cmd_pipe_stderr(aanotify_bin + login_params + params)
if 'ERROR: Could not find last login' in output: if 'ERROR: Could not find last login' in output:
self.skipTest('Could not find last login') self.skipTest('Could not find last login')
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code) result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)

View File

@ -0,0 +1,118 @@
#! /usr/bin/python3
# ------------------------------------------------------------------
#
# Copyright (C) 2025 Maxime Bélair <maxime.belair@canonical.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License published by the Free Software Foundation.
#
# ------------------------------------------------------------------
import os
import sys
import unittest
import subprocess
import apparmor.aa as aa
from apparmor.common import cmd
from common_test import AATest, setup_aa, setup_all_loops
class AAShowUsageTest(AATest):
def test_help_contents(self):
"""Test output of help text"""
expected_return_code = 0
expected_output_1 = \
'''usage: aa-show-usage [-h] [-s {all,used,unused}] [-j] [-d DIR]
[--show-matching-path] [--filter.flags FLAGS]
[--filter.profile_name PROFILE_NAME]
[--filter.profile_attach PROFILE_ATTACH]
[--filter.profile_path PROFILE_PATH]
Check which profiles are used
''' # noqa: E128
expected_output_2 = \
'''
-h, --help show this help message and exit
-s, --show-type {all,used,unused}
Type of profiles to show
-j, --json Output in JSON
-d, --dir DIR Path to profiles
--show-matching-path Show the path of a file matching the profile
Filtering options:
Filters are used to reduce the output of information to only those entries
that will match the filter. Filters use Python's regular expression syntax.
--filter.flags FLAGS Filter by flags
--filter.profile_name PROFILE_NAME
Filter by profile name
--filter.profile_attach PROFILE_ATTACH
Filter by profile attachment
--filter.profile_path PROFILE_PATH
Filter by profile path
''' # noqa: E128
if sys.version_info[:2] < (3, 13):
# Python 3.13 tweaked argparse output [1]. When running on older
# Python versions, we adapt the expected output to match.
#
# https://github.com/python/cpython/pull/103372
patches = [(
'-s, --show-type {all,used,unused}',
'-s {all,used,unused}, --show-type {all,used,unused}',
), (
'-d, --dir DIR Path to profiles',
'-d DIR, --dir DIR Path to profiles'
)]
for patch in patches:
expected_output_2 = expected_output_2.replace(patch[0], patch[1])
return_code, output = cmd([aashowusage_bin, '--help'])
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
self.assertEqual(expected_return_code, return_code, result + output)
self.assertIn(expected_output_1, output)
self.assertIn(expected_output_2, output)
def test_show_unconfined_profiles(self):
expected_return_code = 0
return_code, output = cmd([aashowusage_bin, '--filter.flags=unconfined', '-d', aa.profile_dir])
result = 'Got return code {}, expected {}\n'.format(return_code, expected_return_code)
self.assertEqual(expected_return_code, return_code, result + output)
nb_profile = 0
for line in output.splitlines():
if line.startswith(' Profile '):
nb_profile += 1
command = ['grep', '-Er', r'flags=.*unconfined.*\{', '--', aa.profile_dir]
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, check=False)
self.assertEqual(
len(result.stdout.splitlines()), nb_profile,
"Error found {} profiles, expected {}\n\n Output was: \n {}. Grepped profiles are: {}".format(
nb_profile, len(result.stdout.splitlines()), output, result.stdout)
)
setup_aa(aa) # Wrapper for aa.init_aa()
setup_all_loops(__name__)
# The location of the aa-show-usage utility can be overridden by setting
# the APPARMOR_SHOW_USAGE or USE_SYSTEM environment variable;
# this is useful for running these tests in an installed environment
aashowusage_bin = "../aa-show-usage"
if __name__ == '__main__':
if 'APPARMOR_SHOW_USAGE' in os.environ:
aashowusage_bin = os.environ['APPARMOR_SHOW_USAGE']
elif 'USE_SYSTEM' in os.environ:
aashowusage_bin = 'aa-show-usage'
unittest.main(verbosity=1)

View File

@ -552,7 +552,7 @@ class AaTest_parse_profile_data(AATest):
self.assertEqual(prof['/foo']['name'], '/foo') self.assertEqual(prof['/foo']['name'], '/foo')
self.assertEqual(prof['/foo']['filename'], 'somefile') self.assertEqual(prof['/foo']['filename'], 'somefile')
self.assertEqual(prof['/foo']['flags'], None) self.assertEqual(prof['/foo']['flags'], None)
self.assertEqual(prof['/foo']['xattrs'], 'user.bar=bar') self.assertEqual(prof['/foo']['xattrs'], {'user.bar': 'bar'})
def test_parse_xattrs_02(self): def test_parse_xattrs_02(self):
prof = parse_profile_data('/foo xattrs=(user.bar=bar user.foo=*) {\n}\n'.split(), 'somefile', False, False) prof = parse_profile_data('/foo xattrs=(user.bar=bar user.foo=*) {\n}\n'.split(), 'somefile', False, False)
@ -561,7 +561,7 @@ class AaTest_parse_profile_data(AATest):
self.assertEqual(prof['/foo']['name'], '/foo') self.assertEqual(prof['/foo']['name'], '/foo')
self.assertEqual(prof['/foo']['filename'], 'somefile') self.assertEqual(prof['/foo']['filename'], 'somefile')
self.assertEqual(prof['/foo']['flags'], None) self.assertEqual(prof['/foo']['flags'], None)
self.assertEqual(prof['/foo']['xattrs'], 'user.bar=bar user.foo=*') self.assertEqual(prof['/foo']['xattrs'], {'user.bar': 'bar', 'user.foo': '*'})
def test_parse_xattrs_03(self): def test_parse_xattrs_03(self):
d = '/foo xattrs=(user.bar=bar) flags=(complain) {\n}\n' d = '/foo xattrs=(user.bar=bar) flags=(complain) {\n}\n'
@ -571,7 +571,7 @@ class AaTest_parse_profile_data(AATest):
self.assertEqual(prof['/foo']['name'], '/foo') self.assertEqual(prof['/foo']['name'], '/foo')
self.assertEqual(prof['/foo']['filename'], 'somefile') self.assertEqual(prof['/foo']['filename'], 'somefile')
self.assertEqual(prof['/foo']['flags'], 'complain') self.assertEqual(prof['/foo']['flags'], 'complain')
self.assertEqual(prof['/foo']['xattrs'], 'user.bar=bar') self.assertEqual(prof['/foo']['xattrs'], {'user.bar': 'bar'})
def test_parse_xattrs_04(self): def test_parse_xattrs_04(self):
with self.assertRaises(AppArmorException): with self.assertRaises(AppArmorException):

View File

@ -41,30 +41,30 @@ class TestUnknownKey(AATest):
class AaTest_get_header(AATest): class AaTest_get_header(AATest):
tests = ( tests = (
# name embedded_hat depth flags attachment xattrs prof.keyw. comment expected # name embedded_hat depth flags attachment xattrs prof.keyw. comment expected
(('/foo', False, 1, 'complain', '', '', False, ''), ' /foo flags=(complain) {'), (('/foo', False, 1, 'complain', '', {}, False, ''), ' /foo flags=(complain) {'),
(('/foo', True, 1, 'complain', '', '', False, ''), ' profile /foo flags=(complain) {'), (('/foo', True, 1, 'complain', '', {}, False, ''), ' profile /foo flags=(complain) {'),
(('/foo sp', False, 2, 'complain', '', '', False, ''), ' "/foo sp" flags=(complain) {'), (('/foo sp', False, 2, 'complain', '', {}, False, ''), ' "/foo sp" flags=(complain) {'),
(('/foo', True, 2, 'complain', '', '', False, ''), ' profile /foo flags=(complain) {'), (('/foo', True, 2, 'complain', '', {}, False, ''), ' profile /foo flags=(complain) {'),
(('/foo', False, 0, None, '', '', False, ''), '/foo {'), (('/foo', False, 0, None, '', {}, False, ''), '/foo {'),
(('/foo', False, 0, None, '', 'user.foo=bar', False, ''), '/foo xattrs=(user.foo=bar) {'), (('/foo', False, 0, None, '', {'user.foo': 'bar'}, False, ''), '/foo xattrs=(user.foo=bar) {'),
(('/foo', True, 0, None, '', '', False, ''), 'profile /foo {'), (('/foo', True, 0, None, '', {}, False, ''), 'profile /foo {'),
(('bar', False, 1, 'complain', '', '', False, ''), ' profile bar flags=(complain) {'), (('bar', False, 1, 'complain', '', {}, False, ''), ' profile bar flags=(complain) {'),
(('bar', False, 1, 'complain', '/foo', '', False, ''), ' profile bar /foo flags=(complain) {'), (('bar', False, 1, 'complain', '/foo', {}, False, ''), ' profile bar /foo flags=(complain) {'),
(('bar', True, 1, 'complain', '/foo', '', False, ''), ' profile bar /foo flags=(complain) {'), (('bar', True, 1, 'complain', '/foo', {}, False, ''), ' profile bar /foo flags=(complain) {'),
(('bar baz', False, 1, None, '/foo', '', False, ''), ' profile "bar baz" /foo {'), (('bar baz', False, 1, None, '/foo', {}, False, ''), ' profile "bar baz" /foo {'),
(('bar', True, 1, None, '/foo', '', False, ''), ' profile bar /foo {'), (('bar', True, 1, None, '/foo', {}, False, ''), ' profile bar /foo {'),
(('bar baz', False, 1, 'complain', '/foo sp', '', False, ''), ' profile "bar baz" "/foo sp" flags=(complain) {'), (('bar baz', False, 1, 'complain', '/foo sp', {}, False, ''), ' profile "bar baz" "/foo sp" flags=(complain) {'),
(('bar baz', False, 1, 'complain', '/foo sp', 'user.foo=bar', False, ''), ' profile "bar baz" "/foo sp" xattrs=(user.foo=bar) flags=(complain) {'), (('bar baz', False, 1, 'complain', '/foo sp', {'user.foo': 'bar'}, False, ''), ' profile "bar baz" "/foo sp" xattrs=(user.foo=bar) flags=(complain) {'),
(('^foo', False, 1, 'complain', '', '', False, ''), ' profile ^foo flags=(complain) {'), (('^foo', False, 1, 'complain', '', {}, False, ''), ' profile ^foo flags=(complain) {'),
(('^foo', True, 1, 'complain', '', '', False, ''), ' ^foo flags=(complain) {'), (('^foo', True, 1, 'complain', '', {}, False, ''), ' ^foo flags=(complain) {'),
(('^foo', True, 1.5, 'complain', '', '', False, ''), ' ^foo flags=(complain) {'), (('^foo', True, 1.5, 'complain', '', {}, False, ''), ' ^foo flags=(complain) {'),
(('^foo', True, 1.3, 'complain', '', '', False, ''), ' ^foo flags=(complain) {'), (('^foo', True, 1.3, 'complain', '', {}, False, ''), ' ^foo flags=(complain) {'),
(('/foo', False, 1, 'complain', '', '', True, ''), ' profile /foo flags=(complain) {'), (('/foo', False, 1, 'complain', '', {}, True, ''), ' profile /foo flags=(complain) {'),
(('/foo', True, 1, 'complain', '', '', True, ''), ' profile /foo flags=(complain) {'), (('/foo', True, 1, 'complain', '', {}, True, ''), ' profile /foo flags=(complain) {'),
(('/foo', False, 1, 'complain', '', '', False, '# x'), ' /foo flags=(complain) { # x'), (('/foo', False, 1, 'complain', '', {}, False, '# x'), ' /foo flags=(complain) { # x'),
(('/foo', True, 1, None, '', '', False, '# x'), ' profile /foo { # x'), (('/foo', True, 1, None, '', {}, False, '# x'), ' profile /foo { # x'),
(('/foo', False, 1, None, '', '', True, '# x'), ' profile /foo { # x'), (('/foo', False, 1, None, '', {}, True, '# x'), ' profile /foo { # x'),
(('/foo', True, 1, 'complain', '', '', True, '# x'), ' profile /foo flags=(complain) { # x'), (('/foo', True, 1, 'complain', '', {}, True, '# x'), ' profile /foo flags=(complain) { # x'),
) )
def _run_test(self, params, expected): def _run_test(self, params, expected):
@ -88,8 +88,10 @@ class AaTest_get_header_01(AATest):
({'name': '/foo', 'depth': 1, 'flags': 'complain'}, ' /foo flags=(complain) {'), ({'name': '/foo', 'depth': 1, 'flags': 'complain'}, ' /foo flags=(complain) {'),
({'name': '/foo', 'depth': 1, 'flags': 'complain', 'profile_keyword': True}, ' profile /foo flags=(complain) {'), ({'name': '/foo', 'depth': 1, 'flags': 'complain', 'profile_keyword': True}, ' profile /foo flags=(complain) {'),
({'name': '/foo', 'flags': 'complain'}, '/foo flags=(complain) {'), ({'name': '/foo', 'flags': 'complain'}, '/foo flags=(complain) {'),
({'name': '/foo', 'xattrs': 'user.foo=bar', 'flags': 'complain'}, '/foo xattrs=(user.foo=bar) flags=(complain) {'), ({'name': '/foo', 'xattrs': {'user.foo': 'bar'}, 'flags': 'complain'}, '/foo xattrs=(user.foo=bar) flags=(complain) {'),
({'name': '/foo', 'xattrs': 'user.foo=bar', 'embedded_hat': True}, 'profile /foo xattrs=(user.foo=bar) {'), ({'name': '/foo', 'xattrs': {'user.foo': 'bar'}, 'embedded_hat': True}, 'profile /foo xattrs=(user.foo=bar) {'),
({'name': '/foo', 'xattrs': {'user.foo': None}, 'embedded_hat': True}, 'profile /foo xattrs=(user.foo) {'),
({'name': '/foo', 'xattrs': {'user.foo': None, 'user.bar': None}, 'embedded_hat': True}, 'profile /foo xattrs=(user.bar user.foo) {'),
) )
def _run_test(self, params, expected): def _run_test(self, params, expected):
@ -178,6 +180,7 @@ class TestSetInvalid(AATest):
(('attachment', None), AppArmorBug), (('attachment', None), AppArmorBug),
(('filename', True), AppArmorBug), # expects string or None (('filename', True), AppArmorBug), # expects string or None
(('allow', None), AppArmorBug), # doesn't allow overwriting at all (('allow', None), AppArmorBug), # doesn't allow overwriting at all
(('xattrs', 0), AppArmorBug), # Invalid type
) )
def _run_test(self, params, expected): def _run_test(self, params, expected):
@ -196,7 +199,7 @@ class AaTest_repr(AATest):
def testRepr(self): def testRepr(self):
prof_storage = ProfileStorage('foo', 'hat', 'TEST') prof_storage = ProfileStorage('foo', 'hat', 'TEST')
prof_storage['name'] = 'foo' prof_storage['name'] = 'foo'
prof_storage['xattrs'] = 'user.bar=bar' prof_storage['xattrs'] = {'user.bar': 'bar'}
prof_storage['capability'].add(CapabilityRule('dac_override')) prof_storage['capability'].add(CapabilityRule('dac_override'))
self.assertEqual(str(prof_storage), '\n<ProfileStorage>\nprofile foo xattrs=(user.bar=bar) {\n capability dac_override,\n\n}\n</ProfileStorage>\n') self.assertEqual(str(prof_storage), '\n<ProfileStorage>\nprofile foo xattrs=(user.bar=bar) {\n capability dac_override,\n\n}\n</ProfileStorage>\n')
@ -205,15 +208,21 @@ class AaTest_repr(AATest):
class AaTest_parse_profile_start(AATest): class AaTest_parse_profile_start(AATest):
tests = ( tests = (
# profile start line profile hat parent name profile hat attachment xattrs flags pps_set_hat_external # 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 {', None, None), ('', '/foo', '/foo', '/foo', '', {}, None, False)),
(('/foo (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', '', 'complain', 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 /foo {', None, None), ('', 'foo', 'foo', 'foo', '/foo', {}, None, False)), # named profile
(('profile /foo {', '/bar', None), ('/bar', '/foo', '/bar', '/foo', '', '', None, False)), # child profile (('profile /foo {', '/bar', None), ('/bar', '/foo', '/bar', '/foo', '', {}, None, False)), # child profile
(('/foo//bar {', None, None), ('/foo', '/foo//bar', '/foo', 'bar', '', '', None, True)), # external hat (('profile /foo xattrs=() {', '/bar', None), ('/bar', '/foo', '/bar', '/foo', '', {}, None, False)),
(('profile "/foo" (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', '', 'complain', False)), (('/foo//bar {', None, None), ('/foo', '/foo//bar', '/foo', 'bar', '', {}, None, True)), # external hat
(('profile "/foo" xattrs=(user.bar=bar) {', None, None), ('', '/foo', '/foo', '/foo', '', 'user.bar=bar', None, False)), (('profile "/foo" (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', {}, 'complain', False)),
(('profile "/foo" xattrs=(user.bar=bar user.foo=*) {', None, None), ('', '/foo', '/foo', '/foo', '', 'user.bar=bar user.foo=*', None, False)), (('profile "/foo" xattrs=() {', None, None), ('', '/foo', '/foo', '/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 "/foo" xattrs=(user.bar) {', None, None), ('', '/foo', '/foo', '/foo', '', {'user.bar': None}, None, False)),
(('profile "/foo" xattrs=(user.foo user.bar) {', None, None), ('', '/foo', '/foo', '/foo', '', {'user.bar': None, 'user.foo': None},
None, False)), # noqa: E127
(('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)), # noqa: E127
(('/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): def _run_test(self, params, expected):

View File

@ -17,7 +17,8 @@ from apparmor.regex import (
RE_PROFILE_CAP, RE_PROFILE_DBUS, RE_PROFILE_MOUNT, RE_PROFILE_PTRACE, RE_PROFILE_SIGNAL, RE_PROFILE_CAP, RE_PROFILE_DBUS, RE_PROFILE_MOUNT, RE_PROFILE_PTRACE, RE_PROFILE_SIGNAL,
RE_PROFILE_START, parse_profile_start_line, re_match_include, RE_PROFILE_UNIX, RE_PROFILE_START, parse_profile_start_line, re_match_include, RE_PROFILE_UNIX,
RE_PROFILE_PIVOT_ROOT, RE_PROFILE_PIVOT_ROOT,
re_match_include_parse, strip_parenthesis, strip_quotes) re_match_include_parse, strip_parenthesis, strip_quotes, resolve_variables, expand_braces,
expand_var, expand_string, re_print_dict, re_parse_dict)
from common_test import AATest, setup_aa, setup_all_loops from common_test import AATest, setup_aa, setup_all_loops
@ -724,6 +725,159 @@ class TestStripQuotes(AATest):
self.assertEqual(strip_quotes(params), expected) self.assertEqual(strip_quotes(params), expected)
class TestExpandBraces(AATest):
tests = (
('foo', ['foo']),
('/{,foo}', ['/', '/foo']),
('/{,foo,bar}', ['/', '/foo', '/bar']),
('/{bin,sbin}/runc', ['/bin/runc', '/sbin/runc']),
('/{,usr/}{,s}bin/runc', ['/bin/runc', '/sbin/runc', '/usr/bin/runc', '/usr/sbin/runc']),
('/{,usr/{,s}}bin/runc', ['/bin/runc', '/usr/bin/runc', '/usr/sbin/runc']),
('{{a,b},{c,{d,e}},f}', ['a', 'b', 'c', 'd', 'e', 'f']),
('{,b}{d,e}', ['d', 'e', 'bd', 'be']),
('{aa,b}{d,e}', ['aad', 'aae', 'bd', 'be']),
('{{foo,bar},{baz,qux}}', ['foo', 'bar', 'baz', 'qux']),
)
def _run_test(self, params, expected):
self.assertEqual(expand_braces(params), expected)
class TestInvalidExpandBraces(AATest):
tests = (
# Malformed expressions
('/{', AppArmorException),
('/{}}{', AppArmorException),
('/}', AppArmorException),
('/{foo,bar}}', AppArmorException),
('/{a,b},{c,d}}', AppArmorException),
# Braces should always provide at least 2 alternatives
('{foo}', AppArmorException),
('/{foo}/', AppArmorException),
('/{,foo}{bar}', AppArmorException),
)
def _run_test(self, params, expected):
with self.assertRaises(expected):
expand_braces(params)
var_dict = {
# Normal variables
'@{a}': ['AAA'],
'@{b}': ['BBB'],
# Multiple values variables
'@{c}': ['CC', 'CCC'],
'@{d}': ['DD', 'DDD', 'DDDD'],
# Variable relying on other variables
'@{e}': ['@{a}'],
'@{f}': ['@{e}@{b}', '@{c}'],
'@{g}': ['/bin/@{f}/{foo,bar}'],
# Invalid variables
'@{h}': ['@{h}'], # Self reference
'@{i}': ['@{j}'], # Circular reference with i and j
'@{j}': ['@{i}'],
}
class TestResolveVariables(AATest):
tests = (
('@{a}', ['AAA']),
('/@{a}/@{b}/', ['/AAA/BBB/']),
('/@{a}/@{c}/', ['/AAA/CC/', '/AAA/CCC/']),
('/@{a}/@{d}/', ['/AAA/DD/', '/AAA/DDD/', '/AAA/DDDD/']),
('/@{c}/@{d}/', ['/CC/DD/', '/CC/DDD/', '/CC/DDDD/', '/CCC/DD/', '/CCC/DDD/', '/CCC/DDDD/']),
('/@{e}/', ['/AAA/']),
('/@{f}/', ['/AAABBB/', '/CC/', '/CCC/']),
('@{g}', ['/bin/AAABBB/{foo,bar}', '/bin/CC/{foo,bar}', '/bin/CCC/{foo,bar}']),
)
def _run_test(self, params, expected):
self.assertEqual(resolve_variables(params, var_dict), expected)
class TestInvalidResolveVariables(AATest):
tests = (
('@{h}', AppArmorException),
('@{i}', AppArmorException),
('@{z}', AppArmorException), # @{z} doesn't exist
)
def _run_test(self, params, expected):
with self.assertRaises(expected):
resolve_variables(params, var_dict)
class TestExpandVar(AATest):
tests = (
(('@{a}', set()), ['AAA']),
(('@{d}', set()), ['DD', 'DDD', 'DDDD']),
(('@{f}', set()), ['AAABBB', 'CC', 'CCC']),
)
def _run_test(self, params, expected):
var, seen_vars = params
self.assertEqual(expand_var(var, var_dict, seen_vars), expected)
class TestInvalidExpandVar(AATest):
tests = (
(('@{h}', set()), AppArmorException), # Circular dependency
(('@{a}', {'@{a}'}), AppArmorException), # Circular dependency
(('@{z}', set()), AppArmorException), # Invalid variable (not in var_dict)
)
def _run_test(self, params, expected):
with self.assertRaises(expected):
var, seen_vars = params
expand_var(var, var_dict, seen_vars)
class TestExpandString(AATest):
tests = (
('@{g}/@{a}', ['/bin/AAABBB/{foo,bar}/AAA', '/bin/CC/{foo,bar}/AAA', '/bin/CCC/{foo,bar}/AAA']),
('@{g}/@{c}', ['/bin/AAABBB/{foo,bar}/CC', '/bin/AAABBB/{foo,bar}/CCC', '/bin/CC/{foo,bar}/CC', '/bin/CC/{foo,bar}/CCC', '/bin/CCC/{foo,bar}/CC', '/bin/CCC/{foo,bar}/CCC']),
)
def _run_test(self, params, expected):
self.assertEqual(expand_string(params, var_dict, set()), expected)
class TestInvalidExpandString(AATest):
tests = (
(('@{h}', set()), AppArmorException), # Circular dependency
(('@{a}', {'@{a}'}), AppArmorException), # Circular dependency
(('@{z}', set()), AppArmorException), # Invalid variable (not in var_dict)
)
def _run_test(self, params, expected):
with self.assertRaises(expected):
var, seen_vars = params
expand_string(var, var_dict, seen_vars)
class TestRePrintDict(AATest):
tests = (
({'a': 'b'}, 'a=b'),
({'a': 'b', 'bb': 'cc'}, 'a=b bb=cc'),
({'z': 'c', 'y': 'b', 'x': 'a'}, 'x=a y=b z=c'),
)
def _run_test(self, params, expected):
self.assertEqual(re_print_dict(params), expected)
class TestReParseDict(AATest):
tests = (
('a=b', {'a': 'b'}),
(' a=bbb bb=cc', {'a': 'bbb', 'bb': 'cc'}),
)
def _run_test(self, params, expected):
self.assertEqual(re_parse_dict(params), expected)
setup_aa(aa) setup_aa(aa)
setup_all_loops(__name__) setup_all_loops(__name__)
if __name__ == '__main__': if __name__ == '__main__':