Add aa-logprof test framework

... and a simple test for a single (fake) event for ping.

Notes:
- to let aa-logprof work in the CI environment, we need to skip checking
  for the AppArmor mountpoint. Introduce --no-check-mountpoint for this.
- PYTHONPATH and LD_LIBRARY_PATH need to be explicitely forwarded when
  starting aa-logprof via subprocess.Popen()
- if the test runs with coverage enabled, it will also start aa-logprof
  with coverage (parameters copied from Makefile).

Speaking about coverage - this test adds 4% overall coverage, and 10%
more coverage for apparmor/aa.py.
This commit is contained in:
Christian Boltz 2023-08-13 16:44:14 +02:00
parent 313366fbbc
commit 46debcc493
Failed to generate hash of commit
7 changed files with 172 additions and 2 deletions

View file

@ -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()

View file

@ -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

View file

@ -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"

View file

@ -0,0 +1,33 @@
abi <abi/4.0>,
include <tunables/global>
# ------------------------------------------------------------------
#
# 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 <abstractions/base>
include <abstractions/consoles>
include <abstractions/nameservice>
include if exists <local/bin.ping>
capability net_raw,
capability setuid,
network inet raw,
network inet6 raw,
/etc/modules.conf r,
/proc/21622/cmdline r,
/{,usr/}bin/{,iputils-}ping mrix,
}

View file

@ -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."}

1
utils/test/severity.db Symbolic link
View file

@ -0,0 +1 @@
../severity.db

119
utils/test/test-logprof.py Normal file
View file

@ -0,0 +1,119 @@
#! /usr/bin/python3
# ------------------------------------------------------------------
#
# Copyright (C) 2023 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
# 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)