diff --git a/utils/aa-logprof b/utils/aa-logprof index 37f3e937a..b3f6b5060 100755 --- a/utils/aa-logprof +++ b/utils/aa-logprof @@ -29,6 +29,7 @@ parser.add_argument('-f', '--file', type=str, help=_('path to logfile')) parser.add_argument('-m', '--mark', type=str, help=_('mark in the log to start processing after')) parser.add_argument('-j', '--json', action='store_true', help=_('Input and Output in JSON')) parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS) +parser.add_argument('--no-check-mountpoint', action='store_true', help=argparse.SUPPRESS) args = parser.parse_args() logmark = args.mark or '' @@ -41,7 +42,7 @@ if args.json: apparmor.set_logfile(args.file) aa_mountpoint = apparmor.check_for_apparmor() -if not aa_mountpoint: +if not aa_mountpoint and not args.no_check_mountpoint: raise AppArmorException(_('It seems AppArmor was not started. Please enable AppArmor and try again.')) apparmor.loadincludes() diff --git a/utils/test/Makefile b/utils/test/Makefile index c86c4256a..bc9f49254 100644 --- a/utils/test/Makefile +++ b/utils/test/Makefile @@ -21,7 +21,7 @@ COMMONDIR=../../common/ include $(COMMONDIR)/Make.rules # files that don't have 100% test coverage -INCOMPLETE_COVERAGE=libraries/libapparmor/swig/python/.*/LibAppArmor/LibAppArmor.py|utils/apparmor/aa.py|utils/apparmor/common.py|utils/apparmor/config.py|utils/apparmor/easyprof.py|utils/apparmor/fail.py|utils/apparmor/logparser.py|utils/apparmor/profile_storage.py|utils/apparmor/rules.py|utils/apparmor/ui.py|minitools_test.py +INCOMPLETE_COVERAGE=libraries/libapparmor/swig/python/.*/LibAppArmor/LibAppArmor.py|utils/aa-logprof|utils/apparmor/aa.py|utils/apparmor/common.py|utils/apparmor/config.py|utils/apparmor/easyprof.py|utils/apparmor/fail.py|utils/apparmor/logparser.py|utils/apparmor/profile_storage.py|utils/apparmor/rules.py|utils/apparmor/ui.py|minitools_test.py ifdef USE_SYSTEM diff --git a/utils/test/logprof/ping.auditlog b/utils/test/logprof/ping.auditlog new file mode 100644 index 000000000..dc0a319d0 --- /dev/null +++ b/utils/test/logprof/ping.auditlog @@ -0,0 +1,3 @@ +type=AVC msg=audit(1691930856.284:29963): apparmor="DENIED" operation="open" class="file" profile="ping" name="/proc/21622/cmdline" pid=9136 comm="cat" requested_mask="r" denied_mask="r" fsuid=1000 ouid=1000 +type=SYSCALL msg=audit(1691930856.284:29963): arch=c000003e syscall=257 success=no exit=-13 a0=ffffff9c a1=7ffc4539abf8 a2=0 a3=0 items=0 ppid=21622 pid=9136 auid=1000 uid=1000 gid=100 euid=1000 suid=1000 fsuid=1000 egid=100 sgid=100 fsgid=100 tty=pts4 ses=2 comm="cat" exe="/usr/bin/cat" subj=ping key=(null) +type=AVC msg=audit(1691930881.661:29975): apparmor="STATUS" operation="profile_replace" profile="apparmor_parser" name="ping" pid=10005 comm="apparmor_parser" diff --git a/utils/test/logprof/ping.bin.ping b/utils/test/logprof/ping.bin.ping new file mode 100644 index 000000000..3d88a9f34 --- /dev/null +++ b/utils/test/logprof/ping.bin.ping @@ -0,0 +1,33 @@ +abi , + +include + +# ------------------------------------------------------------------ +# +# Copyright (C) 2002-2009 Novell/SUSE +# Copyright (C) 2010 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. +# +# ------------------------------------------------------------------ + + +profile ping /{usr/,}bin/{,iputils-}ping { + include + include + include + include if exists + + capability net_raw, + capability setuid, + + network inet raw, + network inet6 raw, + + /etc/modules.conf r, + /proc/21622/cmdline r, + /{,usr/}bin/{,iputils-}ping mrix, + +} diff --git a/utils/test/logprof/ping.jsonlog b/utils/test/logprof/ping.jsonlog new file mode 100644 index 000000000..dfe91cd89 --- /dev/null +++ b/utils/test/logprof/ping.jsonlog @@ -0,0 +1,13 @@ +o {"dialog": "apparmor-json-version","data": "2.12"} +o {"dialog": "info","data": "Updating AppArmor profiles in /etc/apparmor.d."} +o {"dialog": "info","data": "Reading log entries from /var/log/audit/audit.log."} +o {"dialog": "info","data": "Complain-mode changes:"} +o {"dialog": "info","data": "Enforce-mode changes:"} +o {"dialog": "promptuser","title": null,"headers": {"Profile": "ping","Path": "/proc/21622/cmdline","New Mode": "owner r","Severity": 6},"explanation": null,"options": ["owner /proc/*/cmdline r,","owner /proc/21622/cmdline r,"],"menu_items": ["(A)llow","[(D)eny]","(I)gnore","(G)lob","Glob with (E)xtension","(N)ew","Audi(t)","(O)wner permissions off","Abo(r)t","(F)inish"],"default_key": "d"} +i {"dialog":"promptuser","selected":0,"response_key":"o"} +o {"dialog": "promptuser","title": null,"headers": {"Profile": "ping","Path": "/proc/21622/cmdline","New Mode": "r","Severity": 6},"explanation": null,"options": ["/proc/*/cmdline r,","/proc/21622/cmdline r,"],"menu_items": ["(A)llow","[(D)eny]","(I)gnore","(G)lob","Glob with (E)xtension","(N)ew","Audi(t)","(O)wner permissions on","Abo(r)t","(F)inish"],"default_key": "d"} +i {"dialog":"promptuser","selected":1,"response_key":"a"} +o {"dialog": "info","data": "Adding /proc/21622/cmdline r, to profile."} +o {"dialog": "promptuser","title": "Changed Local Profiles","headers": {},"explanation": "The following local profiles were changed. Would you like to save them?","options": ["ping"],"menu_items": ["(S)ave Changes","Save Selec(t)ed Profile","[(V)iew Changes]","View Changes b/w (C)lean profiles","Abo(r)t"],"default_key": "v"} +i {"dialog":"promptuser","selected":0,"response_key":"t"} +o {"dialog": "info","data": "Writing updated profile for ping."} diff --git a/utils/test/severity.db b/utils/test/severity.db new file mode 120000 index 000000000..267eadc36 --- /dev/null +++ b/utils/test/severity.db @@ -0,0 +1 @@ +../severity.db \ No newline at end of file diff --git a/utils/test/test-logprof.py b/utils/test/test-logprof.py new file mode 100644 index 000000000..483fb078c --- /dev/null +++ b/utils/test/test-logprof.py @@ -0,0 +1,119 @@ +#! /usr/bin/python3 +# ------------------------------------------------------------------ +# +# Copyright (C) 2023 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 +# License published by the Free Software Foundation. +# +# ------------------------------------------------------------------ + +import os +import shutil +import subprocess +import sys +import unittest + +# import apparmor.aa as aa # see the setup_aa() call for details +from common_test import AATest, read_file, setup_all_loops # , setup_aa + + +class TestLogprof(AATest): + # This test expects a set of files: + # - logprof/TESTNAME.auditlog - audit.log + # - logprof/TESTNAME.jsonlog - expected aa-logprof --json input and output (gathered with json_log=1 in logprof.conf) + # - logprof/TESTNAME.PROFILE - one or more profiles in the expected state + # where TESTNAME is the name given in the first column of 'tests' + tests = ( + # test name # profiles to verify + ('ping', ['bin.ping']), + ) + + def AASetup(self): + self.createTmpdir() + + # copy the local profiles to the test directory + self.profile_dir = self.tmpdir + '/profiles' + shutil.copytree('../../profiles/apparmor.d/', self.profile_dir, symlinks=True) + + def AATeardown(self): + self._terminate() + + def _startLogprof(self, auditlog): + exe = [sys.executable] + + if 'coverage' in sys.modules: + exe = exe + ['-m', 'coverage', 'run', '--branch', '-p'] + + exe = exe + ['../aa-logprof', '--json', '--configdir', './', '-f', auditlog, '-d', self.profile_dir, '--no-check-mountpoint'] + + process = subprocess.Popen( + exe, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + # stderr=subprocess.STDOUT, + env={'LANG': 'C', + 'PYTHONPATH': os.environ.get('PYTHONPATH', ''), + 'LD_LIBRARY_PATH': os.environ.get('LD_LIBRARY_PATH', ''), + }, + ) + + return process + + def _terminate(self): + self.process.stdin.close() + self.process.stdout.close() + self.process.terminate() + self.process.wait(timeout=0.2) + + def _run_test(self, params, expected): + auditlog = './logprof/%s.auditlog' % params + jsonlog = './logprof/%s.jsonlog' % params + + jlog = read_file(jsonlog) + jlog = jlog.replace('/etc/apparmor.d', self.profile_dir) + jlog = jlog.replace('/var/log/audit/audit.log', auditlog) + jlog = jlog.strip().split('\n') + + self.process = self._startLogprof(auditlog) + + for line in jlog: + if line.startswith('o '): # read from stdout + output = self.process.stdout.readline().decode("utf-8").strip() + self.assertEqual(output, line[2:]) + + elif line.startswith('i '): # send to stdin + # expect an empty prompt line + output = self.process.stdout.readline().decode("utf-8").strip() + self.assertEqual(output, '') + + # "type" answer + self.process.stdin.write(line[2:].encode("utf-8") + b"\n") + self.process.stdin.flush() + + else: + raise Exception('Unknown line in json log %s: %s' % (jsonlog, line)) + + # give logprof some time to write the updated profile and terminate + self.process.wait(timeout=0.2) + self.assertEqual(self.process.returncode, 0) + + for file in expected: + exp = read_file('./logprof/%s.%s' % (params, file)) + actual = read_file(os.path.join(self.profile_dir, file)) + + # remove '# Last Modified:' line from updated profile + actual = actual.split('\n') + if actual[0].startswith('# Last Modified:'): + actual = actual[1:] + actual = '\n'.join(actual) + + self.assertEqual(actual, exp) + + +# if you import apparmor.aa and call init_aa() in your tests, uncomment this +# setup_aa(aa) +setup_all_loops(__name__) +if __name__ == '__main__': + unittest.main(verbosity=1)