diff --git a/.bzrignore b/.bzrignore index 05b4d442a..18bb381e1 100644 --- a/.bzrignore +++ b/.bzrignore @@ -46,7 +46,10 @@ libraries/libapparmor/ylwrap libraries/libapparmor/doc/Makefile libraries/libapparmor/doc/Makefile.in libraries/libapparmor/doc/*.2 +libraries/libapparmor/doc/aa_*.3 +libraries/libapparmor/include/Makefile libraries/libapparmor/include/Makefile.in +libraries/libapparmor/include/sys/Makefile libraries/libapparmor/include/sys/Makefile.in libraries/libapparmor/src/.deps libraries/libapparmor/src/.libs @@ -54,10 +57,16 @@ libraries/libapparmor/src/Makefile libraries/libapparmor/src/Makefile.in libraries/libapparmor/src/af_protos.h libraries/libapparmor/src/change_hat.lo +libraries/libapparmor/src/features.lo libraries/libapparmor/src/grammar.lo +libraries/libapparmor/src/kernel.lo +libraries/libapparmor/src/kernel_interface.lo libraries/libapparmor/src/libaalogparse.lo libraries/libapparmor/src/libimmunix_warning.lo +libraries/libapparmor/src/policy_cache.lo +libraries/libapparmor/src/private.lo libraries/libapparmor/src/scanner.lo +libraries/libapparmor/src/libapparmor.pc libraries/libapparmor/src/libapparmor.la libraries/libapparmor/src/libimmunix.la libraries/libapparmor/src/grammar.c @@ -74,12 +83,18 @@ libraries/libapparmor/swig/perl/Makefile.PL libraries/libapparmor/swig/perl/Makefile.in libraries/libapparmor/swig/perl/Makefile.perl libraries/libapparmor/swig/perl/Makefile.perle +libraries/libapparmor/swig/perl/MYMETA.json +libraries/libapparmor/swig/perl/MYMETA.yml libraries/libapparmor/swig/perl/blib libraries/libapparmor/swig/perl/libapparmor_wrap.c libraries/libapparmor/swig/perl/pm_to_blib +libraries/libapparmor/swig/python/__init__.py +libraries/libapparmor/swig/python/build/ +libraries/libapparmor/swig/python/libapparmor_wrap.c libraries/libapparmor/swig/python/Makefile libraries/libapparmor/swig/python/Makefile.in libraries/libapparmor/swig/python/setup.py +libraries/libapparmor/swig/python/test/Makefile libraries/libapparmor/swig/python/test/Makefile.in libraries/libapparmor/swig/ruby/Makefile libraries/libapparmor/swig/ruby/Makefile.in diff --git a/Makefile b/Makefile index 134db43b3..f7907cd9e 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ include ${COMMONDIR}/Make.rules DIRS=parser \ profiles \ utils \ + binutils \ libraries/libapparmor \ changehat/mod_apparmor \ changehat/pam_apparmor \ diff --git a/README b/README index 4ebd25d9b..279f5966f 100644 --- a/README +++ b/README @@ -27,6 +27,7 @@ Source Layout AppArmor consists of several different parts: +binutils/ source for basic utilities written in compiled languages changehat/ source for using changehat with Apache, PAM and Tomcat common/ common makefile rules desktop/ empty @@ -71,6 +72,13 @@ $ make install generate Ruby bindings to libapparmor.] +Binary Utilities: +$ cd binutils +$ make +$ make check +$ make install + + Utilities: $ cd utils $ make @@ -104,7 +112,7 @@ $ make check # depends on the parser having been built first $ make install -[Note that for the parser and the utils, if you only with to build/use +[Note that for the parser, binutils, and utils, if you only wish to build/use some of the locale languages, you can override the default by passing the LANGS arguments to make; e.g. make all install "LANGS=en_US fr".] diff --git a/binutils/Makefile b/binutils/Makefile new file mode 100644 index 000000000..70aba452f --- /dev/null +++ b/binutils/Makefile @@ -0,0 +1,157 @@ +# ---------------------------------------------------------------------- +# Copyright (c) 2015 +# Canonical Ltd. (All rights reserved) +# +# 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. +# +# 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. +# ---------------------------------------------------------------------- +NAME=aa-binutils +all: +COMMONDIR=../common/ + +include $(COMMONDIR)/Make.rules + +DESTDIR=/ +BINDIR=${DESTDIR}/usr/bin +LOCALEDIR=/usr/share/locale +MANPAGES=aa-enabled.8 aa-exec.8 + +WARNINGS = -Wall +EXTRA_WARNINGS = -Wsign-compare -Wmissing-field-initializers -Wformat-security -Wunused-parameter +CPP_WARNINGS = +ifndef CFLAGS +CFLAGS = -g -O2 -pipe + +ifdef DEBUG +CFLAGS += -pg -D DEBUG +endif +ifdef COVERAGE +CFLAGS = -g -pg -fprofile-arcs -ftest-coverage +endif +endif #CFLAGS + +EXTRA_CFLAGS = ${EXTRA_CXXFLAGS} ${CPP_WARNINGS} + +#INCLUDEDIR = /usr/src/linux/include +INCLUDEDIR = + +ifdef INCLUDEDIR + CFLAGS += -I$(INCLUDEDIR) +endif + +# Internationalization support. Define a package and a LOCALEDIR +EXTRA_CFLAGS+=-DPACKAGE=\"${NAME}\" -DLOCALEDIR=\"${LOCALEDIR}\" + +SRCS = aa_enabled.c +HDRS = +TOOLS = aa-enabled aa-exec + +AALIB = -Wl,-Bstatic -lapparmor -Wl,-Bdynamic -lpthread + +ifdef USE_SYSTEM + # Using the system libapparmor so Makefile dependencies can't be used + LIBAPPARMOR_A = + INCLUDE_APPARMOR = + APPARMOR_H = + LIBAPPARMOR_LDFLAGS = +else + LIBAPPARMOR_SRC = ../libraries/libapparmor/ + LOCAL_LIBAPPARMOR_INCLUDE = $(LIBAPPARMOR_SRC)/include + LOCAL_LIBAPPARMOR_LDPATH = $(LIBAPPARMOR_SRC)/src/.libs + + LIBAPPARMOR_A = $(LOCAL_LIBAPPARMOR_LDPATH)/libapparmor.a + INCLUDE_APPARMOR = -I$(LOCAL_LIBAPPARMOR_INCLUDE) + APPARMOR_H = $(LOCAL_LIBAPPARMOR_INCLUDE)/sys/apparmor.h + LIBAPPARMOR_LDFLAGS = -L$(LOCAL_LIBAPPARMOR_LDPATH) +endif +EXTRA_CFLAGS += $(INCLUDE_APPARMOR) +LDFLAGS += $(LIBAPPARMOR_LDFLAGS) + +ifdef V + VERBOSE = 1 +endif +ifndef VERBOSE + VERBOSE = 0 +endif +ifeq ($(VERBOSE),1) + BUILD_OUTPUT = + Q = +else + BUILD_OUTPUT = > /dev/null 2>&1 + Q = @ +endif +export Q VERBOSE BUILD_OUTPUT + +po/%.pot: %.c + $(MAKE) -C po $(@F) NAME=$* SOURCES=$*.c + +# targets arranged this way so that people who don't want full docs can +# pick specific targets they want. +arch: $(TOOLS) + +manpages: $(MANPAGES) + +docs: manpages + +indep: docs + $(Q)$(MAKE) -C po all + +all: arch indep + +.PHONY: coverage +coverage: + $(MAKE) clean $(TOOLS) COVERAGE=1 + +ifndef USE_SYSTEM +$(LIBAPPARMOR_A): + @if [ ! -f $@ ]; then \ + echo "error: $@ is missing. Pick one of these possible solutions:" 1>&2; \ + echo " 1) Build against the in-tree libapparmor by building it first and then trying again. See the top-level README for help." 1>&2; \ + echo " 2) Build against the system libapparmor by adding USE_SYSTEM=1 to your make command." 1>&2;\ + return 1; \ + fi +endif + +aa-enabled: aa_enabled.c $(LIBAPPARMOR_A) + $(CC) $(LDFLAGS) $(EXTRA_CFLAGS) -o $@ $< $(LIBS) $(AALIB) + +aa-exec: aa_exec.c $(LIBAPPARMOR_A) + $(CC) $(LDFLAGS) $(EXTRA_CFLAGS) -o $@ $< $(LIBS) $(AALIB) + +.SILENT: check +.PHONY: check +check: check_pod_files tests + +.SILENT: tests +tests: $(TOOLS) $(TESTS) + echo "no tests atm" + +.PHONY: install +install: install-indep install-arch + +.PHONY: install-arch +install-arch: arch + install -m 755 -d ${BINDIR} + install -m 755 ${TOOLS} ${BINDIR} + +.PHONY: install-indep +install-indep: + $(MAKE) -C po install NAME=${NAME} DESTDIR=${DESTDIR} + $(MAKE) install_manpages DESTDIR=${DESTDIR} + +ifndef VERBOSE +.SILENT: clean +endif +.PHONY: clean +clean: pod_clean + rm -f core core.* *.o *.s *.a *~ *.gcda *.gcno + rm -f gmon.out + rm -f $(TOOLS) $(TESTS) + $(MAKE) -s -C po clean + diff --git a/binutils/aa-enabled.pod b/binutils/aa-enabled.pod new file mode 100644 index 000000000..bc9603e2b --- /dev/null +++ b/binutils/aa-enabled.pod @@ -0,0 +1,94 @@ +# 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-enabled - test whether AppArmor is enabled + +=head1 SYNOPSIS + +B [options] + +=head1 DESCRIPTION + +B is used to determine if AppArmor is enabled. + +=head1 OPTIONS + +B accepts the following arguments: + +=over 4 + +=item -h, --help + +Display a brief usage guide. + +=item -q, --quiet + +Do not output anything to stdout. This option is intended to be used by +scripts that simply want to use the exit code to determine if AppArmor is +enabled. + +=back + +=head1 EXIT STATUS + +Upon exiting, B will set its exit status to the following values: + +=over 4 + +=item 0: + +if AppArmor is enabled. + +=item 1: + +if AppArmor is not enabled/loaded. + +=item 2: + +intentionally not used as an B exit status. + +=item 3: + +if the AppArmor control files aren't available under /sys/kernel/security/. + +=item 4: + +if B doesn't have enough privileges to read the apparmor control files. + +=item 64: + +if any unexpected error or condition is encountered. + +=back + +=head1 BUGS + +If you find any bugs, please report them at +L. + +=head1 SEE ALSO + +apparmor(7), apparmor.d(5), aa_is_enabled(2), and L. + +=cut diff --git a/utils/aa-exec.pod b/binutils/aa-exec.pod similarity index 96% rename from utils/aa-exec.pod rename to binutils/aa-exec.pod index 58dedb22d..14f04290c 100644 --- a/utils/aa-exec.pod +++ b/binutils/aa-exec.pod @@ -57,10 +57,6 @@ use the current profile name (likely unconfined). use profiles in NAMESPACE. This will result in confinement transitioning to using the new profile namespace. -=item -f FILE, --file=FILE - -a file or directory containing profiles to load before confining the program. - =item -i, --immediate transition to PROFILE before doing executing IcommandE>. This diff --git a/binutils/aa_enabled.c b/binutils/aa_enabled.c new file mode 100644 index 000000000..3ca84b0c2 --- /dev/null +++ b/binutils/aa_enabled.c @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2015 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. + */ + +#include +#include +#include +#include +#include +#include +#define _(s) gettext(s) + +#include + +void print_help(const char *command) +{ + printf(_("%s: [options]\n" + " options:\n" + " -q | --quiet Don't print out any messages\n" + " -h | --help Print help\n"), + command); + exit(1); +} + + +/* Exit statuses and meanings are documented in the aa-enabled.pod file */ +static void exit_with_error(int saved_errno, int quiet) +{ + int err; + + switch(saved_errno) { + case ENOSYS: + if (!quiet) + printf(_("No - not available on this system.\n")); + exit(1); + case ECANCELED: + if (!quiet) + printf(_("No - disabled at boot.\n")); + exit(1); + case ENOENT: + if (!quiet) + printf(_("Maybe - policy interface not available.\n")); + exit(3); + case EPERM: + case EACCES: + if (!quiet) + printf(_("Maybe - insufficient permissions to determine availability.\n")); + exit(4); + } + + if (!quiet) + printf(_("Error - %s\n"), strerror(saved_errno)); + exit(64); +} + +int main(int argc, char **argv) +{ + int enabled; + int quiet = 0; + + setlocale(LC_MESSAGES, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + + if (argc > 2) { + printf(_("unknown or incompatible options\n")); + print_help(argv[0]); + } else if (argc == 2) { + if (strcmp(argv[1], "--quiet") == 0 || + strcmp(argv[1], "-q") == 0) { + quiet = 1; + } else if (strcmp(argv[1], "--help") == 0 || + strcmp(argv[1], "-h") == 0) { + print_help(argv[0]); + } else { + printf(_("unknown option '%s'\n"), argv[1]); + print_help(argv[0]); + } + } + + enabled = aa_is_enabled(); + if (!enabled) + exit_with_error(errno, quiet); + + if (!quiet) + printf(_("Yes\n")); + exit(0); +} diff --git a/binutils/aa_exec.c b/binutils/aa_exec.c new file mode 100644 index 000000000..7e73f45f3 --- /dev/null +++ b/binutils/aa_exec.c @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2015 + * Canonical, Ltd. (All rights reserved) + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. or Canonical + * Ltd. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define _(s) gettext(s) + +static const char *opt_profile = NULL; +static const char *opt_namespace = NULL; +static bool opt_debug = false; +static bool opt_immediate = false; +static bool opt_verbose = false; + +static void usage(const char *name, bool error) +{ + FILE *stream = stdout; + int status = EXIT_SUCCESS; + + if (error) { + stream = stderr; + status = EXIT_FAILURE; + } + + fprintf(stream, + _("USAGE: %s [OPTIONS] \n" + "\n" + "Confine with the specified PROFILE.\n" + "\n" + "OPTIONS:\n" + " -p PROFILE, --profile=PROFILE PROFILE to confine with\n" + " -n NAMESPACE, --namespace=NAMESPACE NAMESPACE to confine in\n" + " -d, --debug show messages with debugging information\n" + " -i, --immediate change profile immediately instead of at exec\n" + " -v, --verbose show messages with stats\n" + " -h, --help display this help\n" + "\n"), name); + exit(status); +} + +#define error(fmt, args...) _error(_("aa-exec: ERROR: " fmt "\n"), ## args) +static void _error(const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + exit(EXIT_FAILURE); +} + +#define debug(fmt, args...) _debug(_("aa-exec: DEBUG: " fmt "\n"), ## args) +static void _debug(const char *fmt, ...) +{ + va_list args; + + if (!opt_debug) + return; + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); +} + +#define verbose(fmt, args...) _verbose(_(fmt "\n"), ## args) +static void _verbose(const char *fmt, ...) +{ + va_list args; + + if (!opt_verbose) + return; + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); +} + +static void verbose_print_argv(char **argv) +{ + if (!opt_verbose) + return; + + fprintf(stderr, _("exec")); + for (; *argv; argv++) + fprintf(stderr, " %s", *argv); + fprintf(stderr, "\n"); +} + +static char **parse_args(int argc, char **argv) +{ + int opt; + struct option long_opts[] = { + {"debug", no_argument, 0, 'd'}, + {"help", no_argument, 0, 'h'}, + {"profile", required_argument, 0, 'p'}, + {"namespace", required_argument, 0, 'n'}, + {"immediate", no_argument, 0, 'i'}, + {"verbose", no_argument, 0, 'v'}, + }; + + while ((opt = getopt_long(argc, argv, "+dhp:n:iv", long_opts, NULL)) != -1) { + switch (opt) { + case 'd': + opt_debug = true; + break; + case 'h': + usage(argv[0], false); + break; + case 'p': + opt_profile = optarg; + break; + case 'n': + opt_namespace = optarg; + break; + case 'i': + opt_immediate = true; + break; + case 'v': + opt_verbose = true; + break; + default: + usage(argv[0], true); + break; + } + } + + if (optind >= argc) + usage(argv[0], true); + + return argv + optind; +} + +static void build_name(char *name, size_t name_len, + const char *namespace, const char *profile) +{ + size_t required_len = 1; /* reserve 1 byte for NUL-terminator */ + + if (namespace) + required_len += 1 + strlen(namespace) + 3; /* ::// */ + + if (profile) + required_len += strlen(profile); + + if (required_len > name_len) + error("name too long (%zu > %zu)", required_len, name_len); + + name[0] = '\0'; + + if (namespace) { + strcat(name, ":"); + strcat(name, namespace); + strcat(name, "://"); + } + + if (profile) + strcat(name, profile); +} + +int main(int argc, char **argv) +{ + char name[PATH_MAX]; + int rc = 0; + + argv = parse_args(argc, argv); + + if (opt_namespace || opt_profile) + build_name(name, sizeof(name), opt_namespace, opt_profile); + else + goto exec; + + if (opt_immediate) { + verbose("aa_change_profile(\"%s\")", name); + rc = aa_change_profile(name); + debug("%d = aa_change_profile(\"%s\")", rc, name); + } else { + verbose("aa_change_onexec(\"%s\")", name); + rc = aa_change_onexec(name); + debug("%d = aa_change_onexec(\"%s\")", rc, name); + } + + if (rc) { + if (errno == ENOENT || errno == EACCES) { + error("%s '%s' does not exist\n", + opt_profile ? "profile" : "namespace", name); + } else if (errno == EINVAL) { + error("AppArmor interface not available"); + } else { + error("%m"); + } + } + +exec: + verbose_print_argv(argv); + execvp(argv[0], argv); + error("Failed to execute \"%s\": %m", argv[0]); +} diff --git a/binutils/po/Makefile b/binutils/po/Makefile new file mode 100644 index 000000000..8216ec53d --- /dev/null +++ b/binutils/po/Makefile @@ -0,0 +1,19 @@ +# ---------------------------------------------------------------------- +# Copyright (C) 2015 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. +# ---------------------------------------------------------------------- +all: + +# As translations get added, they will automatically be included, unless +# the lang is explicitly added to DISABLED_LANGS; e.g. DISABLED_LANGS=en es + +DISABLED_LANGS= + +COMMONDIR=../../common +include $(COMMONDIR)/Make-po.rules + +XGETTEXT_ARGS+=--language=C --keyword=_ $(shell if [ -f ${NAME}.pot ] ; then echo -n -j ; fi) + diff --git a/binutils/po/aa-enabled.pot b/binutils/po/aa-enabled.pot new file mode 100644 index 000000000..bb2b69e78 --- /dev/null +++ b/binutils/po/aa-enabled.pot @@ -0,0 +1,66 @@ +# Copyright (C) 2015 Canonical Ltd +# This file is distributed under the same license as the AppArmor package. +# John Johansen , 2015. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: apparmor@lists.ubuntu.com\n" +"POT-Creation-Date: 2015-11-28 10:23-0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../aa_enabled.c:26 +#, c-format +msgid "" +"%s: [options]\n" +" options:\n" +" -q | --quiet Don't print out any messages\n" +" -h | --help Print help\n" +msgstr "" + +#: ../aa_enabled.c:45 +#, c-format +msgid "unknown or incompatible options\n" +msgstr "" + +#: ../aa_enabled.c:55 +#, c-format +msgid "unknown option '%s'\n" +msgstr "" + +#: ../aa_enabled.c:64 +#, c-format +msgid "Yes\n" +msgstr "" + +#: ../aa_enabled.c:71 +#, c-format +msgid "No - not available on this system.\n" +msgstr "" + +#: ../aa_enabled.c:74 +#, c-format +msgid "No - disabled at boot.\n" +msgstr "" + +#: ../aa_enabled.c:77 +#, c-format +msgid "Maybe - policy interface not available.\n" +msgstr "" + +#: ../aa_enabled.c:81 +#, c-format +msgid "Maybe - insufficient permissions to determine availability.\n" +msgstr "" + +#: ../aa_enabled.c:84 +#, c-format +msgid "Error - '%s'\n" +msgstr "" diff --git a/binutils/po/id.po b/binutils/po/id.po index 1b0d564e1..4b5765efc 100644 --- a/binutils/po/id.po +++ b/binutils/po/id.po @@ -16,6 +16,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2016-01-21 05:11+0000\n" "X-Generator: Launchpad (build 17886)\n" +"Language: id\n" #: ../aa_enabled.c:26 #, c-format diff --git a/common/Make-po.rules b/common/Make-po.rules index 4bfdb4f42..6fa852bfc 100644 --- a/common/Make-po.rules +++ b/common/Make-po.rules @@ -1,7 +1,7 @@ # ------------------------------------------------------------------ # # Copyright (c) 1999-2008 NOVELL (All rights reserved) -# Copyright 2009-2010 Canonical Ltd. +# Copyright 2009-2015 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 @@ -21,7 +21,7 @@ # exist LOCALEDIR=/usr/share/locale -XGETTEXT_ARGS=--copyright-holder="NOVELL, Inc." --msgid-bugs-address=apparmor@lists.ubuntu.com -d ${NAME} +XGETTEXT_ARGS=--copyright-holder="Canonical Ltd" --msgid-bugs-address=apparmor@lists.ubuntu.com -d ${NAME} # When making the .pot file, it's expected that the parent Makefile will # pass in the list of sources in the SOURCES variable diff --git a/common/Make.rules b/common/Make.rules index 3be487ec8..148f80e8c 100644 --- a/common/Make.rules +++ b/common/Make.rules @@ -82,7 +82,7 @@ pod_clean: # ===================== # emits defined capabilities in a simple list, e.g. "CAP_NAME CAP_NAME2" -CAPABILITIES=$(shell echo "\#include " | cpp -dM | LC_ALL=C sed -n -e '/CAP_EMPTY_SET/d' -e 's/^\#define[ \t]\+CAP_\([A-Z0-9_]\+\)[ \t]\+\([0-9xa-f]\+\)\(.*\)$$/CAP_\1/p' | sort) +CAPABILITIES=$(shell echo "\#include " | cpp -dM | LC_ALL=C sed -n -e '/CAP_EMPTY_SET/d' -e 's/^\#define[ \t]\+CAP_\([A-Z0-9_]\+\)[ \t]\+\([0-9xa-f]\+\)\(.*\)$$/CAP_\1/p' | LC_ALL=C sort) .PHONY: list_capabilities list_capabilities: /usr/include/linux/capability.h diff --git a/common/Version b/common/Version index a0700e68b..098bbf1d3 100644 --- a/common/Version +++ b/common/Version @@ -1 +1 @@ -2.9.90 +2.10.90 diff --git a/documentation/Techdoc - eHFA.odt b/documentation/Techdoc - eHFA.odt index 545bc58af..45ec9d6a3 100644 Binary files a/documentation/Techdoc - eHFA.odt and b/documentation/Techdoc - eHFA.odt differ diff --git a/libraries/libapparmor/doc/aa_query_label.pod b/libraries/libapparmor/doc/aa_query_label.pod index 3e943a7ad..7531944e2 100644 --- a/libraries/libapparmor/doc/aa_query_label.pod +++ b/libraries/libapparmor/doc/aa_query_label.pod @@ -24,27 +24,23 @@ aa_query_label - query access permission associated with a label +aa_query_file_path, aa_query_file_path_len - query access permissions of a file path + +aa_query_link_path, aa_query_link_path_len - query access permissions of a link path + =head1 SYNOPSIS B<#include Esys/apparmor.hE> -B +B -B +B -B +B -B +B -B +B Link with B<-lapparmor> when compiling. diff --git a/libraries/libapparmor/src/Makefile.am b/libraries/libapparmor/src/Makefile.am index deca53ed6..3a06c2def 100644 --- a/libraries/libapparmor/src/Makefile.am +++ b/libraries/libapparmor/src/Makefile.am @@ -26,9 +26,9 @@ INCLUDES = $(all_includes) # For more information, see: # http://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html # -AA_LIB_CURRENT = 3 -AA_LIB_REVISION = 1 -AA_LIB_AGE = 2 +AA_LIB_CURRENT = 4 +AA_LIB_REVISION = 0 +AA_LIB_AGE = 3 SUFFIXES = .pc.in .pc diff --git a/libraries/libapparmor/src/grammar.y b/libraries/libapparmor/src/grammar.y index 108e54dac..2a16d7294 100644 --- a/libraries/libapparmor/src/grammar.y +++ b/libraries/libapparmor/src/grammar.y @@ -166,6 +166,11 @@ aa_record_event_type lookup_aa_event(unsigned int type) %token TOK_SYSLOG_KERNEL %token TOK_SYSLOG_USER +%destructor { free($$); } TOK_QUOTED_STRING TOK_ID TOK_MODE TOK_DMESG_STAMP +%destructor { free($$); } TOK_AUDIT_DIGITS TOK_DATE_MONTH TOK_DATE TOK_TIME +%destructor { free($$); } TOK_HEXSTRING TOK_TYPE_OTHER TOK_MSG_REST +%destructor { free($$); } TOK_IP_ADDR + %% log_message: audit_type @@ -201,7 +206,7 @@ other_audit: TOK_TYPE_OTHER audit_msg TOK_MSG_REST ; dmesg_type: TOK_DMESG_STAMP TOK_AUDIT TOK_COLON key_type audit_id key_list - { ret_record->version = AA_RECORD_SYNTAX_V2; } + { ret_record->version = AA_RECORD_SYNTAX_V2; free($1); } ; syslog_type: diff --git a/libraries/libapparmor/src/tst_aalogmisc.c b/libraries/libapparmor/src/tst_aalogmisc.c index b15392fcb..1ca86632b 100644 --- a/libraries/libapparmor/src/tst_aalogmisc.c +++ b/libraries/libapparmor/src/tst_aalogmisc.c @@ -34,19 +34,25 @@ int main(void) retstr = hex_to_string("2F746D702F646F6573206E6F74206578697374"); MY_TEST(retstr, "basic allocation"); MY_TEST(strcmp(retstr, "/tmp/does not exist") == 0, "basic dehex 1"); + free(retstr); retstr = hex_to_string("61"); MY_TEST(strcmp(retstr, "a") == 0, "basic dehex 2"); + free(retstr); retstr = hex_to_string(""); MY_TEST(strcmp(retstr, "") == 0, "empty string"); + free(retstr); /* ipproto_to_string() tests */ retstr = ipproto_to_string((unsigned) 99999); MY_TEST(strcmp(retstr, "unknown(99999)") == 0, "invalid protocol test"); + free(retstr); retstr = ipproto_to_string((unsigned) 6); MY_TEST(strcmp(retstr, "tcp") == 0, "protocol=tcp"); + free(retstr); + return rc; } diff --git a/libraries/libapparmor/testsuite/Makefile.am b/libraries/libapparmor/testsuite/Makefile.am index 3459bc6c6..8f43cecc9 100644 --- a/libraries/libapparmor/testsuite/Makefile.am +++ b/libraries/libapparmor/testsuite/Makefile.am @@ -15,5 +15,10 @@ test_multi_multi_LDADD = -L../src/.libs -lapparmor clean-local: rm -rf tmp.err.* tmp.out.* site.exp site.bak test_multi/out + rm -f libaalogparse.log libaalogparse.sum + +check-local: + if ! test -f libaalogparse.log ; then echo '*** libaalogparse.log not found - is dejagnu installed? ***'; exit 1; fi + if grep ERROR libaalogparse.log ; then exit 1 ; fi EXTRA_DIST = test_multi/*.in test_multi/*.out test_multi/*.err diff --git a/libraries/libapparmor/testsuite/test_multi/testcase_changehat_01.err b/libraries/libapparmor/testsuite/test_multi/testcase_changehat_01.err new file mode 100644 index 000000000..e69de29bb diff --git a/libraries/libapparmor/testsuite/test_multi/testcase_changehat_01.in b/libraries/libapparmor/testsuite/test_multi/testcase_changehat_01.in new file mode 100644 index 000000000..a84b4d72c --- /dev/null +++ b/libraries/libapparmor/testsuite/test_multi/testcase_changehat_01.in @@ -0,0 +1 @@ +type=AVC msg=audit(1449442292.901:961): apparmor="ALLOWED" operation="change_hat" profile="/usr/sbin/httpd{,2}-prefork" pid=8527 comm="httpd-prefork" target="/usr/sbin/httpd{,2}-prefork//HANDLING_UNTRUSTED_INPUT" diff --git a/libraries/libapparmor/testsuite/test_multi/testcase_changehat_01.out b/libraries/libapparmor/testsuite/test_multi/testcase_changehat_01.out new file mode 100644 index 000000000..2f34f4b67 --- /dev/null +++ b/libraries/libapparmor/testsuite/test_multi/testcase_changehat_01.out @@ -0,0 +1,11 @@ +START +File: testcase_changehat_01.in +Event type: AA_RECORD_ALLOWED +Audit ID: 1449442292.901:961 +Operation: change_hat +Profile: /usr/sbin/httpd{,2}-prefork +Command: httpd-prefork +Name2: /usr/sbin/httpd{,2}-prefork//HANDLING_UNTRUSTED_INPUT +PID: 8527 +Epoch: 1449442292 +Audit subid: 961 diff --git a/libraries/libapparmor/testsuite/test_multi/testcase_syslog_read.err b/libraries/libapparmor/testsuite/test_multi/testcase_syslog_read.err new file mode 100644 index 000000000..e69de29bb diff --git a/libraries/libapparmor/testsuite/test_multi/testcase_syslog_read.in b/libraries/libapparmor/testsuite/test_multi/testcase_syslog_read.in new file mode 100644 index 000000000..7cc088fe6 --- /dev/null +++ b/libraries/libapparmor/testsuite/test_multi/testcase_syslog_read.in @@ -0,0 +1 @@ +Jul 25 15:02:00 redacted kernel: [ 296.524447] audit: type=1400 audit(1437850920.403:64): apparmor="ALLOWED" operation="open" profile="/usr/sbin/vsftpd" name="/home/bane/foo" pid=1811 comm="vsftpd" requested_mask="r" denied_mask="r" fsuid=1000 ouid=1000 diff --git a/libraries/libapparmor/testsuite/test_multi/testcase_syslog_read.out b/libraries/libapparmor/testsuite/test_multi/testcase_syslog_read.out new file mode 100644 index 000000000..7b0639e47 --- /dev/null +++ b/libraries/libapparmor/testsuite/test_multi/testcase_syslog_read.out @@ -0,0 +1,15 @@ +START +File: testcase_syslog_read.in +Event type: AA_RECORD_ALLOWED +Audit ID: 1437850920.403:64 +Operation: open +Mask: r +Denied Mask: r +fsuid: 1000 +ouid: 1000 +Profile: /usr/sbin/vsftpd +Name: /home/bane/foo +Command: vsftpd +PID: 1811 +Epoch: 1437850920 +Audit subid: 64 diff --git a/parser/apparmor.d.pod b/parser/apparmor.d.pod index 93ea835ea..8f1059888 100644 --- a/parser/apparmor.d.pod +++ b/parser/apparmor.d.pod @@ -103,7 +103,7 @@ capabilities(7)) B = [ I ] 'network' [ I ] [ I | I ] -B = ( 'inet' | 'ax25' | 'ipx' | 'appletalk' | 'netrom' | 'bridge' | 'atmpvc' | 'x25' | 'inet6' | 'rose' | 'netbeui' | 'security' | 'key' | 'packet' | 'ash' | 'econet' | 'atmsvc' | 'sna' | 'irda' | 'pppox' | 'wanpipe' | 'bluetooth' | 'netlink' | 'unix' | 'rds' | 'llc' | 'can' | 'tipc' | 'iucv' | 'rxrpc' | 'isdn' | 'phonet' | 'ieee802154' | 'caif' | 'alg' | 'nfc' | 'vsock' ) ',' +B = ( 'inet' | 'ax25' | 'ipx' | 'appletalk' | 'netrom' | 'bridge' | 'atmpvc' | 'x25' | 'inet6' | 'rose' | 'netbeui' | 'security' | 'key' | 'packet' | 'ash' | 'econet' | 'atmsvc' | 'sna' | 'irda' | 'pppox' | 'wanpipe' | 'bluetooth' | 'netlink' | 'unix' | 'rds' | 'llc' | 'can' | 'tipc' | 'iucv' | 'rxrpc' | 'isdn' | 'phonet' | 'ieee802154' | 'caif' | 'alg' | 'nfc' | 'vsock' | 'mpls' | 'ib' ) ',' B = ( 'stream' | 'dgram' | 'seqpacket' | 'rdm' | 'raw' | 'packet' ) @@ -161,7 +161,7 @@ B = 'set' '=' '(' I ')' B = Comma or space separated list of I -B = ( 'hup' | 'int' | 'quit' | 'ill' | 'trap' | 'abrt' | 'bus' | 'fpe' | 'kill' | 'usr1' | 'segv' | 'usr2' | 'pipe' | 'alrm' | 'term' | 'stkflt' | 'chld' | 'cont' | 'stop' | 'stp' | 'ttin' | 'ttou' | 'urg' | 'xcpu' | 'xfsz' | 'vtalrm' | 'prof' | 'winch' | 'io' | 'pwr' | 'sys' | 'emt' | 'exists' ) +B = ( 'hup' | 'int' | 'quit' | 'ill' | 'trap' | 'abrt' | 'bus' | 'fpe' | 'kill' | 'usr1' | 'segv' | 'usr2' | 'pipe' | 'alrm' | 'term' | 'stkflt' | 'chld' | 'cont' | 'stop' | 'stp' | 'ttin' | 'ttou' | 'urg' | 'xcpu' | 'xfsz' | 'vtalrm' | 'prof' | 'winch' | 'io' | 'pwr' | 'sys' | 'emt' | 'exists' | 'rtmin+0' ... 'rtmin+32' ) B = 'peer' '=' I @@ -231,11 +231,13 @@ B = 'set' 'rlimit' [I 'E=' I ] B = ( 'cpu' | 'fsize' | 'data' | 'stack' | 'core' | 'rss' | 'nofile' | 'ofile' | 'as' | 'nproc' | 'memlock' | 'locks' | 'sigpending' | 'msgqueue' | 'nice' | 'rtprio' | 'rttime' ) -B = ( I | I | I ) +B = ( I | I | I | I ) B = I ( 'K' | 'M' | 'G' ) Only applies to RLIMIT of 'fsize', 'data', 'stack', 'core', 'rss', 'as', 'memlock', 'msgqueue'. -B = number from 0 to max rlimit value. Only applies ot RLIMIT of 'nofile', 'locks', 'sigpending', 'nproc', 'rtprio', 'cpu' +B = number from 0 to max rlimit value. Only applies ot RLIMIT of 'ofile', 'nofile', 'locks', 'sigpending', 'nproc', 'rtprio' + +B = I ( 'us' | 'microsecond' | 'microseconds' | 'ms' | 'millisecond' | 'milliseconds' | 's' | 'sec' | 'second' | 'seconds' | 'min' | 'minute' | 'minutes' | 'h' | 'hour' | 'hours' | 'd' | 'day' | 'days' | 'week' | 'weeks' ) Only applies to RLIMIT of 'cpu', 'rttime'. RLIMIT 'cpu' only allows units >= 'seconds'. B = a number between -20 and 19. Only applies to RLIMIT of 'nice' @@ -962,6 +964,9 @@ Example AppArmor signal rules: # Allow us to signal ourselves using the built-in @{profile_name} variable signal peer=@{profile_name}, + # Allow two real-time signals + signal set=(rtmin+0 rtmin+32), + =head2 DBus rules AppArmor supports DBus mediation. The mediation is performed in conjunction @@ -1227,8 +1232,10 @@ provided AppArmor policy: @{HOMEDIRS} @{multiarch} @{pid} + @{pids} @{PROC} @{securityfs} + @{apparmorfs} @{sys} @{tid} @{XDG_DESKTOP_DIR} diff --git a/parser/apparmor_parser.pod b/parser/apparmor_parser.pod index bab5979ae..4e104693c 100644 --- a/parser/apparmor_parser.pod +++ b/parser/apparmor_parser.pod @@ -278,9 +278,32 @@ the matching stats flag. Use --help=dump to see a full list of which dump flags are supported +=item -j n, --jobs=n + +Set the number of jobs used to compile the specified policy. Where n can +be + + # - a specific number of jobs + auto - the # of cpus in the in the system + x# - # * number of cpus + +Eg. + -j8 OR --jobs=8 allows for 8 parallel jobs + -jauto OR --jobs=auto sets the jobs to the # of cpus + -jx4 OR --jobs=x4 sets the jobs to # of cpus * 4 + -jx1 is equivalent to -jauto + +The default value is the number of cpus in the system. + +=item --max-jobs n + +Set a hard cap on the value that can be specified by the --jobs flag. +It takes the same set of options available to the --jobs option, and +defaults to 8*cpus + =item -O n, --optimize=n -Set the optimization flags used by policy compilation. A sinlge optimization +Set the optimization flags used by policy compilation. A single optimization flag can be toggled per -O option, but the optimize flag can be passed multiple times. Turning off some phases of the optimization can make it so that policy can't complete compilation due to size constraints diff --git a/parser/libapparmor_re/expr-tree.h b/parser/libapparmor_re/expr-tree.h index afd426f51..cb12ea387 100644 --- a/parser/libapparmor_re/expr-tree.h +++ b/parser/libapparmor_re/expr-tree.h @@ -96,13 +96,13 @@ ostream &operator<<(ostream &os, Node &node); /* An abstract node in the syntax tree. */ class Node { public: - Node(): nullable(false) { child[0] = child[1] = 0; } - Node(Node *left): nullable(false) + Node(): nullable(false), label(0) { child[0] = child[1] = 0; } + Node(Node *left): nullable(false), label(0) { child[0] = left; child[1] = 0; } - Node(Node *left, Node *right): nullable(false) + Node(Node *left, Node *right): nullable(false), label(0) { child[0] = left; child[1] = right; diff --git a/parser/mount.h b/parser/mount.h index b43526094..c179fa26b 100644 --- a/parser/mount.h +++ b/parser/mount.h @@ -103,7 +103,7 @@ #define MS_CMDS (MS_MOVE | MS_REMOUNT | MS_BIND | MS_RBIND | \ MS_UNBINDABLE | MS_RUNBINDABLE | MS_PRIVATE | MS_RPRIVATE | \ MS_SLAVE | MS_RSLAVE | MS_SHARED | MS_RSHARED) -#define MS_REMOUNT_FLAGS (MS_ALL_FLAGS & ~(MS_CMDS & ~MS_REMOUNT)) +#define MS_REMOUNT_FLAGS (MS_ALL_FLAGS & ~(MS_CMDS & ~MS_REMOUNT & ~MS_BIND & ~MS_RBIND)) #define MNT_SRC_OPT 1 #define MNT_DST_OPT 2 diff --git a/parser/parser.h b/parser/parser.h index dfd195d87..58bd00a68 100644 --- a/parser/parser.h +++ b/parser/parser.h @@ -402,6 +402,9 @@ extern void free_cod_entries(struct cod_entry *list); extern void __debug_capabilities(uint64_t capset, const char *name); void debug_cod_entries(struct cod_entry *list); +#define SECONDS_P_MS (1000LL * 1000LL) +long long convert_time_units(long long value, long long base, const char *units); + /* parser_symtab.c */ struct set_value { diff --git a/parser/parser_common.c b/parser/parser_common.c index af77cb8e5..ab36a5315 100644 --- a/parser/parser_common.c +++ b/parser/parser_common.c @@ -57,7 +57,7 @@ * numbers where supported. */ uint32_t policy_version = 2; -uint32_t parser_abi_version = 1; +uint32_t parser_abi_version = 2; uint32_t kernel_abi_version = 5; int force_complain = 0; diff --git a/parser/parser_lex.l b/parser/parser_lex.l index 2832a1cc3..af5dec4dd 100644 --- a/parser/parser_lex.l +++ b/parser/parser_lex.l @@ -447,7 +447,7 @@ LT_EQUAL <= } { - -?{NUMBER}[[:alpha:]]* { + -?{NUMBER} { yylval.var_val = strdup(yytext); RETURN_TOKEN(TOK_VALUE); } @@ -519,7 +519,14 @@ LT_EQUAL <= #include/.*\r?\n { /* Don't use PUSH() macro here as we don't want #include echoed out. * It needs to be handled specially - */ + */ + yy_push_state(INCLUDE); +} + +include/{WS} { + /* Don't use PUSH() macro here as we don't want #include echoed out. + * It needs to be handled specially + */ yy_push_state(INCLUDE); } diff --git a/parser/parser_main.c b/parser/parser_main.c index 616c1ceda..dbf353d28 100644 --- a/parser/parser_main.c +++ b/parser/parser_main.c @@ -36,8 +36,11 @@ #include #include #include +#include + #include + #include "lib.h" #include "features.h" #include "parser.h" @@ -76,6 +79,18 @@ int abort_on_error = 0; /* stop processing profiles if error */ int skip_bad_cache_rebuild = 0; int mru_skip_cache = 1; int debug_cache = 0; + +/* for jobs_max and jobs + * LONG_MAX : no limit + * 0 : auto = detect system processing cores + * n : use that number of processes/threads to compile policy + */ +#define JOBS_AUTO 0 +long jobs_max = -8; /* 8 * cpus */ +long jobs = JOBS_AUTO; /* default: number of processor cores */ +long njobs = 0; +bool debug_jobs = false; + struct timespec cache_tstamp, mru_policy_tstamp; static char *apparmorfs = NULL; @@ -84,7 +99,7 @@ static char *cacheloc = NULL; static aa_features *features = NULL; /* Make sure to update BOTH the short and long_options */ -static const char *short_options = "adf:h::rRVvI:b:BCD:NSm:M:qQn:XKTWkL:O:po:"; +static const char *short_options = "ad::f:h::rRVvI:b:BCD:NSm:M:qQn:XKTWkL:O:po:j:"; struct option long_options[] = { {"add", 0, 0, 'a'}, {"binary", 0, 0, 'B'}, @@ -116,7 +131,7 @@ struct option long_options[] = { {"purge-cache", 0, 0, 130}, /* no short option */ {"create-cache-dir", 0, 0, 131}, /* no short option */ {"cache-loc", 1, 0, 'L'}, - {"debug", 0, 0, 'd'}, + {"debug", 2, 0, 'd'}, {"dump", 1, 0, 'D'}, {"Dump", 1, 0, 'D'}, {"optimize", 1, 0, 'O'}, @@ -126,6 +141,8 @@ struct option long_options[] = { {"skip-bad-cache-rebuild", 0, 0, 133}, /* no short option */ {"warn", 1, 0, 134}, /* no short option */ {"debug-cache", 0, 0, 135}, /* no short option */ + {"jobs", 1, 0, 'j'}, + {"max-jobs", 1, 0, 136}, /* no short option */ {NULL, 0, 0, 0}, }; @@ -170,11 +187,13 @@ static void display_usage(const char *command) "-v, --verbose Show profile names as they load\n" "-Q, --skip-kernel-load Do everything except loading into kernel\n" "-V, --version Display version info and exit\n" - "-d, --debug Debug apparmor definitions\n" + "-d [n], --debug Debug apparmor definitions OR [n]\n" "-p, --preprocess Dump preprocessed profile\n" "-D [n], --dump Dump internal info for debugging\n" "-O [n], --Optimize Control dfa optimizations\n" "-h [cmd], --help[=cmd] Display this text or info about cmd\n" + "-j n, --jobs n Set the number of compile threads\n" + "--max-jobs n Hard cap on --jobs. Default 8*cpus\n" "--abort-on-error Abort processing of profiles on first error\n" "--skip-bad-cache-rebuild Do not try rebuilding the cache if it is rejected by the kernel\n" "--warn n Enable warnings (see --help=warn)\n" @@ -268,6 +287,32 @@ static int getopt_long_file(FILE *f, const struct option *longopts, return 0; } +static long process_jobs_arg(const char *arg, const char *val) { + char *end; + long n; + + if (!val || strcmp(val, "auto") == 0) + n = JOBS_AUTO; + else if (strcmp(val, "max") == 0) + n = LONG_MAX; + else { + bool multiple = false; + if (*val == 'x') { + multiple = true; + val++; + } + n = strtol(val, &end, 0); + if (!(*val && val != end && *end == '\0')) { + PERROR("%s: Invalid option %s=%s%s\n", progname, arg, multiple ? "x" : "", val); + exit(1); + } + if (multiple) + n = -n; + } + + return n; +} + /* process a single argment from getopt_long * Returns: 1 if an action arg, else 0 */ @@ -286,8 +331,17 @@ static int process_arg(int c, char *optarg) option = OPTION_ADD; break; case 'd': - debug++; - skip_read_cache = 1; + if (!optarg) { + debug++; + skip_read_cache = 1; + } else if (strcmp(optarg, "jobs") == 0 || + strcmp(optarg, "j") == 0) { + debug_jobs = true; + } else { + PERROR("%s: Invalid --debug option '%s'\n", + progname, optarg); + exit(1); + } break; case 'h': if (!optarg) { @@ -470,6 +524,12 @@ static int process_arg(int c, char *optarg) case 135: debug_cache = 1; break; + case 'j': + jobs = process_jobs_arg("-j", optarg); + break; + case 136: + jobs_max = process_jobs_arg("max-jobs", optarg); + break; default: display_usage(progname); exit(1); @@ -803,6 +863,118 @@ out: return retval; } +/* Do not call directly, this is a helper for work_sync, which can handle + * single worker cases and cases were the work queue is optimized away + * + * call only if there are work children to wait on + */ +#define work_sync_one(RESULT) \ +do { \ + int status; \ + wait(&status); \ + if (WIFEXITED(status)) \ + RESULT(WEXITSTATUS(status)); \ + else \ + RESULT(ECHILD); \ + /* TODO: do we need to handle traced */ \ + njobs--; \ + if (debug_jobs) \ + fprintf(stderr, " JOBS SYNC ONE: result %d, jobs left %ld\n", status, njobs); \ +} while (0) + +#define work_sync(RESULT) \ +do { \ + if (debug_jobs) \ + fprintf(stderr, "JOBS SYNC: jobs left %ld\n", njobs); \ + while (njobs) \ + work_sync_one(RESULT); \ +} while (0) + +#define work_spawn(WORK, RESULT) \ +do { \ + /* what to do to avoid fork() overhead when single threaded \ + if (jobs == 1) { \ + // no parallel work so avoid fork() overhead \ + RESULT(WORK); \ + break; \ + }*/ \ + if (njobs == jobs) { \ + /* wait for a child */ \ + if (debug_jobs) \ + fprintf(stderr, " JOBS SPAWN: waiting (jobs %ld == max %ld) ...\n", njobs, jobs); \ + work_sync_one(RESULT); \ + } \ + \ + pid_t child = fork(); \ + if (child == 0) { \ + /* child - exit work unit with returned value */ \ + exit(WORK); \ + } else if (child > 0) { \ + /* parent */ \ + njobs++; \ + if (debug_jobs) \ + fprintf(stderr, " JOBS SPAWN: created %ld ...\n", njobs); \ + } else { \ + /* error */ \ + if (debug_jobs) \ + fprintf(stderr, " JOBS SPAWN: failed error: %d) ...\n", errno); \ + RESULT(errno); \ + } \ +} while (0) + + +/* sadly C forces us to do this with exit, long_jump or returning error + * from work_spawn and work_sync. We could throw a C++ exception, is it + * worth doing it to avoid the exit here. + * + * atm not all resources maybe cleanedup at exit + */ +int last_error = 0; +void handle_work_result(int retval) +{ + if (retval) { + last_error = retval; + if (abort_on_error) { + /* already in abort mode we don't need subsequent + * syncs to do this too + */ + abort_on_error = 0; + work_sync(handle_work_result); + exit(last_error); + + } + } +} + +static long compute_jobs(long n, long j) +{ + if (j == JOBS_AUTO) + j = n; + else if (j < 0) + j = n * j * -1; + return j; +} + +static void setup_parallel_compile(void) +{ + /* jobs and paralell_max set by default, config or args */ + long n = sysconf(_SC_NPROCESSORS_ONLN); + if (n == -1) + /* unable to determine number of processors, default to 1 */ + n = 1; + jobs = compute_jobs(n, jobs); + jobs_max = compute_jobs(n, jobs_max); + + if (jobs > jobs_max) { + pwarn("%s: Warning capping number of jobs to %ld * # of cpus == '%ld'", + progname, jobs_max, jobs); + jobs = jobs_max; + } + njobs = 0; + if (debug_jobs) + fprintf(stderr, "jobs: %ld\n", jobs); +} + struct dir_cb_data { aa_kernel_interface *kernel_interface; const char *dirname; /* name of the parent dir */ @@ -820,8 +992,9 @@ static int profile_dir_cb(int dirfd unused, const char *name, struct stat *st, autofree char *path = NULL; if (asprintf(&path, "%s/%s", cb_data->dirname, name) < 0) PERROR(_("Out of memory")); - rc = process_profile(option, cb_data->kernel_interface, path, - cb_data->cachedir); + work_spawn(process_profile(option, cb_data->kernel_interface, + path, cb_data->cachedir), + handle_work_result); } return rc; } @@ -837,7 +1010,9 @@ static int binary_dir_cb(int dirfd unused, const char *name, struct stat *st, autofree char *path = NULL; if (asprintf(&path, "%s/%s", cb_data->dirname, name) < 0) PERROR(_("Out of memory")); - rc = process_binary(option, cb_data->kernel_interface, path); + work_spawn(process_binary(option, cb_data->kernel_interface, + path), + handle_work_result); } return rc; } @@ -860,8 +1035,8 @@ static void setup_flags(void) int main(int argc, char *argv[]) { aa_kernel_interface *kernel_interface = NULL; - aa_policy_cache *policy_cache; - int retval, last_error; + aa_policy_cache *policy_cache = NULL; + int retval; int i; int optind; @@ -873,6 +1048,8 @@ int main(int argc, char *argv[]) process_config_file("/etc/apparmor/parser.conf"); optind = process_args(argc, argv); + setup_parallel_compile(); + setlocale(LC_MESSAGES, ""); bindtextdomain(PACKAGE, LOCALEDIR); textdomain(PACKAGE); @@ -964,8 +1141,10 @@ int main(int argc, char *argv[]) void *data); struct dir_cb_data cb_data; + memset(&cb_data, 0, sizeof(struct dir_cb_data)); cb_data.dirname = profilename; cb_data.cachedir = cacheloc; + cb_data.kernel_interface = kernel_interface; cb = binary_input ? binary_dir_cb : profile_dir_cb; if ((retval = dirat_for_each(AT_FDCWD, profilename, &cb_data, cb))) { @@ -973,25 +1152,23 @@ int main(int argc, char *argv[]) profilename); } } else if (binary_input) { - retval = process_binary(option, kernel_interface, - profilename); + work_spawn(process_binary(option, kernel_interface, + profilename), + handle_work_result); } else { - retval = process_profile(option, kernel_interface, - profilename, cacheloc); + work_spawn(process_profile(option, kernel_interface, + profilename, cacheloc), + handle_work_result); } if (profilename) free(profilename); profilename = NULL; - - if (retval) { - last_error = retval; - if (abort_on_error) - break; - } } + work_sync(handle_work_result); if (ofile) fclose(ofile); + aa_policy_cache_unref(policy_cache); return last_error; } diff --git a/parser/parser_misc.c b/parser/parser_misc.c index e362f245d..85a45d514 100644 --- a/parser/parser_misc.c +++ b/parser/parser_misc.c @@ -724,7 +724,7 @@ static const char *capnames[] = { "audit_write", "audit_control", "setfcap", - "mac_override" + "mac_override", "syslog", }; @@ -867,6 +867,54 @@ void print_cond_entry(struct cond_entry *ent) } } + +struct time_units { + const char *str; + long long value; +}; + +static struct time_units time_units[] = { + { "us", 1LL }, + { "microsecond", 1LL }, + { "microseconds", 1LL }, + { "ms", 1000LL }, + { "millisecond", 1000LL }, + { "milliseconds", 1000LL }, + { "s", 1000LL * 1000LL }, + { "sec", SECONDS_P_MS }, + { "second", SECONDS_P_MS }, + { "seconds", SECONDS_P_MS }, + { "min" , 60LL * SECONDS_P_MS }, + { "minute", 60LL * SECONDS_P_MS }, + { "minutes", 60LL * SECONDS_P_MS }, + { "h", 60LL * 60LL * SECONDS_P_MS }, + { "hour", 60LL * 60LL * SECONDS_P_MS }, + { "hours", 60LL * 60LL * SECONDS_P_MS }, + { "d", 24LL * 60LL * 60LL * SECONDS_P_MS }, + { "day", 24LL * 60LL * 60LL * SECONDS_P_MS }, + { "days", 24LL * 60LL * 60LL * SECONDS_P_MS }, + { "week", 7LL * 24LL * 60LL * 60LL * SECONDS_P_MS }, + { "weeks", 7LL * 24LL * 60LL * 60LL * SECONDS_P_MS }, + { NULL, 0 } +}; + +long long convert_time_units(long long value, long long base, const char *units) +{ + struct time_units *ent; + if (!units) + /* default to base if no units */ + return value; + + for (ent = time_units; ent->str; ent++) { + if (strcmp(ent->str, units) == 0) { + if (value * ent->value < base) + return -1LL; + return value * ent->value / base; + } + } + return -2LL; +} + #ifdef UNIT_TEST #include "unit_test.h" @@ -1085,6 +1133,50 @@ int test_processquoted(void) return rc; } +#define TIME_TEST(V, B, U, R) \ +MY_TEST(convert_time_units((V), (B), U) == (R), \ + "convert " #V " with base of " #B ", " #U " units") + +int test_convert_time_units() +{ + int rc = 0; + + TIME_TEST(1LL, 1LL, NULL, 1LL); + TIME_TEST(12345LL, 1LL, NULL, 12345LL); + TIME_TEST(10LL, 10LL, NULL, 10LL); + TIME_TEST(123450LL, 10LL, NULL, 123450LL); + + TIME_TEST(12345LL, 1LL, "us", 12345LL); + TIME_TEST(12345LL, 1LL, "microsecond", 12345LL); + TIME_TEST(12345LL, 1LL, "microseconds", 12345LL); + + TIME_TEST(12345LL, 1LL, "ms", 12345LL*1000LL); + TIME_TEST(12345LL, 1LL, "millisecond", 12345LL*1000LL); + TIME_TEST(12345LL, 1LL, "milliseconds", 12345LL*1000LL); + + TIME_TEST(12345LL, 1LL, "s", 12345LL*1000LL*1000LL); + TIME_TEST(12345LL, 1LL, "sec", 12345LL*1000LL*1000LL); + TIME_TEST(12345LL, 1LL, "second", 12345LL*1000LL*1000LL); + TIME_TEST(12345LL, 1LL, "seconds", 12345LL*1000LL*1000LL); + + TIME_TEST(12345LL, 1LL, "min", 12345LL*1000LL*1000LL*60LL); + TIME_TEST(12345LL, 1LL, "minute", 12345LL*1000LL*1000LL*60LL); + TIME_TEST(12345LL, 1LL, "minutes", 12345LL*1000LL*1000LL*60LL); + + TIME_TEST(12345LL, 1LL, "h", 12345LL*1000LL*1000LL*60LL*60LL); + TIME_TEST(12345LL, 1LL, "hour", 12345LL*1000LL*1000LL*60LL*60LL); + TIME_TEST(12345LL, 1LL, "hours", 12345LL*1000LL*1000LL*60LL*60LL); + + TIME_TEST(12345LL, 1LL, "d", 12345LL*1000LL*1000LL*60LL*60LL*24LL); + TIME_TEST(12345LL, 1LL, "day", 12345LL*1000LL*1000LL*60LL*60LL*24LL); + TIME_TEST(12345LL, 1LL, "days", 12345LL*1000LL*1000LL*60LL*60LL*24LL); + + TIME_TEST(12345LL, 1LL, "week", 12345LL*1000LL*1000LL*60LL*60LL*24LL*7LL); + TIME_TEST(12345LL, 1LL, "weeks", 12345LL*1000LL*1000LL*60LL*60LL*24LL*7LL); + + return rc; +} + int main(void) { int rc = 0; @@ -1102,6 +1194,10 @@ int main(void) if (retval != 0) rc = retval; + retval = test_convert_time_units(); + if (retval != 0) + rc = retval; + return rc; } #endif /* UNIT_TEST */ diff --git a/parser/parser_variable.c b/parser/parser_variable.c index ac334dcde..15a439119 100644 --- a/parser/parser_variable.c +++ b/parser/parser_variable.c @@ -275,12 +275,51 @@ static int process_variables_in_rules(Profile &prof) return 0; } +static int process_variables_in_name(Profile &prof) +{ + /* this needs to be done before alias expansion, ie. altnames are + * setup + */ + int error = expand_entry_variables(&prof.name); + if (!error && prof.attachment) + error = expand_entry_variables(&prof.attachment); + + return error; +} + +static std::string escape_re(std::string str) +{ + for (size_t i = 0; i < str.length(); i++) { + if (str[i] == '\\') { + /* skip \ and follow char. Skipping \ and first + * char is enough for multichar escape sequence + */ + i++; + continue; + } + if (strchr("{}[]*?", str[i]) != NULL) { + str.insert(i++, "\\"); + } + } + + return str; +} int process_profile_variables(Profile *prof) { int error = 0, rc; - error = new_set_var(PROFILE_NAME_VARIABLE, prof->get_name(true).c_str()); + /* needs to be before PROFILE_NAME_VARIABLE so that variable will + * have the correct name + */ + error = process_variables_in_name(*prof); + + if (!error) { + /* escape profile name elements that could be interpreted + * as regular expressions. + */ + error = new_set_var(PROFILE_NAME_VARIABLE, escape_re(prof->get_name(false)).c_str()); + } if (!error) error = process_variables_in_entries(prof->entries); diff --git a/parser/parser_yacc.y b/parser/parser_yacc.y index b3083d56a..aa510c6e0 100644 --- a/parser/parser_yacc.y +++ b/parser/parser_yacc.y @@ -78,7 +78,6 @@ mnt_rule *do_mnt_rule(struct cond_entry *src_conds, char *src, int mode); mnt_rule *do_pivot_rule(struct cond_entry *old, char *root, char *transition); - void add_local_entry(Profile *prof); %} @@ -252,6 +251,7 @@ void add_local_entry(Profile *prof); %type valuelist %type expr %type id_or_var +%type opt_id_or_var %type opt_subset_flag %type opt_audit_flag %type opt_owner_flag @@ -307,7 +307,10 @@ opt_ns: { /* nothing */ $$ = NULL; } opt_id: { /* nothing */ $$ = NULL; } | TOK_ID { $$ = $1; } -profile_base: TOK_ID opt_id flags TOK_OPEN rules TOK_CLOSE +opt_id_or_var: { /* nothing */ $$ = NULL; } + | id_or_var { $$ = $1; } + +profile_base: TOK_ID opt_id_or_var flags TOK_OPEN rules TOK_CLOSE { Profile *prof = $5; @@ -315,13 +318,17 @@ profile_base: TOK_ID opt_id flags TOK_OPEN rules TOK_CLOSE yyerror(_("Memory allocation error.")); } + /* Honor the --namespace-string command line option */ + if (profile_ns) { + prof->ns = strdup(profile_ns); + if (!prof->ns) + yyerror(_("Memory allocation error.")); + } + prof->name = $1; prof->attachment = $2; - if ($2 && $2[0] != '/') - /* we don't support variables as part of the profile - * name or attachment atm - */ - yyerror(_("Profile attachment must begin with a '/'.")); + if ($2 && !($2[0] == '/' || strncmp($2, "@{", 2) == 0)) + yyerror(_("Profile attachment must begin with a '/' or variable.")); prof->flags = $3; if (force_complain && kernel_abi_version == 5) /* newer abis encode force complain as part of the @@ -351,12 +358,17 @@ profile: opt_profile_flag opt_ns profile_base if ($3->name[0] != '/' && !($1 || $2)) yyerror(_("Profile names must begin with a '/', namespace or keyword 'profile' or 'hat'.")); - if ($2 && profile_ns) { - pwarn("%s: -n %s overriding policy specified namespace :%s:\n", progname, profile_ns, $2); + if (prof->ns) { + /** + * Print warning if the profile specified a namespace + * different than the one specified with the + * --namespace-string command line option + */ + if ($2 && strcmp(prof->ns, $2)) { + pwarn("%s: -n %s overriding policy specified namespace :%s:\n", + progname, prof->ns, $2); + } free($2); - prof->ns = strdup(profile_ns); - if (!prof->ns) - yyerror(_("Memory allocation error.")); } else prof->ns = $2; if ($1 == 2) @@ -853,7 +865,7 @@ rules: rules cond_rule $$ = merge_policy($1, $2); } -rules: rules TOK_SET TOK_RLIMIT TOK_ID TOK_LE TOK_VALUE TOK_END_OF_RULE +rules: rules TOK_SET TOK_RLIMIT TOK_ID TOK_LE TOK_VALUE opt_id TOK_END_OF_RULE { rlim_t value = RLIM_INFINITY; long long tmp; @@ -866,11 +878,6 @@ rules: rules TOK_SET TOK_RLIMIT TOK_ID TOK_LE TOK_VALUE TOK_END_OF_RULE if (strcmp($6, "infinity") == 0) { value = RLIM_INFINITY; } else { - const char *seconds = "seconds"; - const char *milliseconds = "ms"; - const char *minutes = "minutes"; - const char *hours = "hours"; - const char *days = "days"; const char *kb = "KB"; const char *mb = "MB"; const char *gb = "GB"; @@ -880,34 +887,25 @@ rules: rules TOK_SET TOK_RLIMIT TOK_ID TOK_LE TOK_VALUE TOK_END_OF_RULE case RLIMIT_CPU: if (!end || $6 == end || tmp < 0) yyerror("RLIMIT '%s' invalid value %s\n", $4, $6); - if (*end == '\0' || - strstr(seconds, end) == seconds) { - value = tmp; - } else if (strstr(minutes, end) == minutes) { - value = tmp * 60; - } else if (strstr(hours, end) == hours) { - value = tmp * 60 * 60; - } else if (strstr(days, end) == days) { - value = tmp * 60 * 60 * 24; - } else { + tmp = convert_time_units(tmp, SECONDS_P_MS, $7); + if (tmp == -1LL) + yyerror("RLIMIT '%s %s' < minimum value of 1s\n", $4, $6); + else if (tmp < 0LL) yyerror("RLIMIT '%s' invalid value %s\n", $4, $6); - } + if (!$7) + pwarn(_("RLIMIT 'cpu' no units specified using default units of seconds\n")); + value = tmp; break; case RLIMIT_RTTIME: /* RTTIME is measured in microseconds */ if (!end || $6 == end || tmp < 0) - yyerror("RLIMIT '%s' invalid value %s\n", $4, $6); - if (*end == '\0') { - value = tmp; - } else if (strstr(milliseconds, end) == milliseconds) { - value = tmp * 1000; - } else if (strstr(seconds, end) == seconds) { - value = tmp * 1000 * 1000; - } else if (strstr(minutes, end) == minutes) { - value = tmp * 1000 * 1000 * 60; - } else { - yyerror("RLIMIT '%s' invalid value %s\n", $4, $6); - } + yyerror("RLIMIT '%s' invalid value %s %s\n", $4, $6, $7 ? $7 : ""); + tmp = convert_time_units(tmp, 1LL, $7); + if (tmp < 0LL) + yyerror("RLIMIT '%s' invalid value %s %s\n", $4, $6, $7 ? $7 : ""); + if (!$7) + pwarn(_("RLIMIT 'rttime' no units specified using default units of microseconds\n")); + value = tmp; break; case RLIMIT_NOFILE: case RLIMIT_NPROC: @@ -915,15 +913,15 @@ rules: rules TOK_SET TOK_RLIMIT TOK_ID TOK_LE TOK_VALUE TOK_END_OF_RULE case RLIMIT_SIGPENDING: #ifdef RLIMIT_RTPRIO case RLIMIT_RTPRIO: - if (!end || $6 == end || *end != '\0' || tmp < 0) - yyerror("RLIMIT '%s' invalid value %s\n", $4, $6); + if (!end || $6 == end || $7 || tmp < 0) + yyerror("RLIMIT '%s' invalid value %s %s\n", $4, $6, $7 ? $7 : ""); value = tmp; break; #endif #ifdef RLIMIT_NICE case RLIMIT_NICE: - if (!end || $6 == end || *end != '\0') - yyerror("RLIMIT '%s' invalid value %s\n", $4, $6); + if (!end || $6 == end || $7) + yyerror("RLIMIT '%s' invalid value %s %s\n", $4, $6, $7 ? $7 : ""); if (tmp < -20 || tmp > 19) yyerror("RLIMIT '%s' out of range (-20 .. 19) %d\n", $4, tmp); value = tmp + 20; @@ -938,15 +936,17 @@ rules: rules TOK_SET TOK_RLIMIT TOK_ID TOK_LE TOK_VALUE TOK_END_OF_RULE case RLIMIT_MEMLOCK: case RLIMIT_MSGQUEUE: if ($6 == end || tmp < 0) - yyerror("RLIMIT '%s' invalid value %s\n", $4, $6); - if (strstr(kb, end) == kb) { + yyerror("RLIMIT '%s' invalid value %s %s\n", $4, $6, $7 ? $7 : ""); + if (!$7) { + ; /* use default of bytes */ + } else if (strstr(kb, $7) == kb) { tmp *= 1024; - } else if (strstr(mb, end) == mb) { + } else if (strstr(mb, $7) == mb) { tmp *= 1024*1024; - } else if (strstr(gb, end) == gb) { + } else if (strstr(gb, $7) == gb) { tmp *= 1024*1024*1024; - } else if (*end != '\0') { - yyerror("RLIMIT '%s' invalid value %s\n", $4, $6); + } else { + yyerror("RLIMIT '%s' invalid value %s %s\n", $4, $6, $7); } value = tmp; break; diff --git a/parser/policy_cache.c b/parser/policy_cache.c index 4ba732cb4..9dd4b11cd 100644 --- a/parser/policy_cache.c +++ b/parser/policy_cache.c @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -25,8 +26,6 @@ #include #include #include -#include -#include #include "lib.h" #include "parser.h" @@ -166,12 +165,21 @@ void install_cache(const char *cachetmpname, const char *cachename) /* Only install the generate cache file if it parsed correctly and did not have write/close errors */ if (cachetmpname) { - struct timeval t; + struct timespec times[2]; + /* set the mtime of the cache file to the most newest mtime * of policy files used to generate it */ - TIMESPEC_TO_TIMEVAL(&t, &mru_policy_tstamp); - utimes(cachetmpname, &t); + times[0].tv_sec = 0; + times[0].tv_nsec = UTIME_OMIT; + times[1] = mru_policy_tstamp; + if (utimensat(AT_FDCWD, cachetmpname, times, 0) < 0) { + PERROR("%s: Failed to set the mtime of cache file '%s': %m\n", + progname, cachename); + unlink(cachetmpname); + return; + } + if (rename(cachetmpname, cachename) < 0) { pwarn("Warning failed to write cache: %s\n", cachename); unlink(cachetmpname); diff --git a/parser/rc.apparmor.suse b/parser/rc.apparmor.suse index bd2931a32..0a594ecea 100644 --- a/parser/rc.apparmor.suse +++ b/parser/rc.apparmor.suse @@ -94,12 +94,13 @@ aa_log_skipped_msg() { echo -e "$rc_skipped" } +_set_status() { + return $1 +} + aa_log_end_msg() { - v="-v" - if [ "$1" != '0' ]; then - rc="-v$1" - fi - rc_status $v + _set_status $1 + rc_status -v } usage() { diff --git a/parser/tst/caching.py b/parser/tst/caching.py index 7e82cb629..5a351d3ef 100755 --- a/parser/tst/caching.py +++ b/parser/tst/caching.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # ------------------------------------------------------------------ # -# Copyright (C) 2013 Canonical Ltd. -# Author: Steve Beattie +# Copyright (C) 2013-2015 Canonical Ltd. +# Authors: Steve Beattie +# Tyler Hicks # # 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,11 +13,11 @@ # TODO # - check cache not used if parser in $PATH is newer -# - check cache used/not used if includes are newer/older # - check cache used for force-complain, disable symlink, etc. from argparse import ArgumentParser import os +import platform import shutil import time import tempfile @@ -24,19 +25,24 @@ import unittest import testlib +ABSTRACTION_CONTENTS = ''' + # Simple example abstraction + capability setuid, +''' +ABSTRACTION = 'suid-abstraction' PROFILE_CONTENTS = ''' # Simple example profile for caching tests /bin/pingy { + #include <%s> capability net_raw, - capability setuid, network inet raw, /bin/ping mixr, /etc/modules.conf r, } -''' +''' % (ABSTRACTION) PROFILE = 'sbin.pingy' config = None @@ -51,11 +57,6 @@ class AAParserCachingCommon(testlib.AATestTemplate): # REPORT ALL THE OUTPUT self.maxDiff = None - # skip all the things if apparmor securityfs isn't mounted - if not os.path.exists("/sys/kernel/security/apparmor"): - raise unittest.SkipTest("WARNING: /sys/kernel/security/apparmor does not exist. " - "Skipping tests") - self.tmp_dir = tempfile.mkdtemp(prefix='aa-caching-') os.chmod(self.tmp_dir, 0o755) @@ -63,7 +64,11 @@ class AAParserCachingCommon(testlib.AATestTemplate): self.cache_dir = os.path.join(self.tmp_dir, 'cache') os.mkdir(self.cache_dir) - # write our sample profile out + # default path of the output cache file + self.cache_file = os.path.join(self.cache_dir, PROFILE) + + # write our sample abstraction and profile out + self.abstraction = testlib.write_file(self.tmp_dir, ABSTRACTION, ABSTRACTION_CONTENTS) self.profile = testlib.write_file(self.tmp_dir, PROFILE, PROFILE_CONTENTS) if config.debug: @@ -72,6 +77,9 @@ class AAParserCachingCommon(testlib.AATestTemplate): self.cmd_prefix = [config.parser, '--base', self.tmp_dir, '--skip-kernel-load'] + if not self.is_apparmorfs_mounted(): + self.cmd_prefix += ['-M', './features_files/features.all'] + def tearDown(self): '''teardown for each test''' @@ -89,7 +97,17 @@ class AAParserCachingCommon(testlib.AATestTemplate): self.assertFalse(os.path.exists(path), 'test created file %s, when it was not expected to do so' % path) + def is_apparmorfs_mounted(self): + return os.path.exists("/sys/kernel/security/apparmor") + + def require_apparmorfs(self): + # skip the test if apparmor securityfs isn't mounted + if not self.is_apparmorfs_mounted(): + raise unittest.SkipTest("WARNING: /sys/kernel/security/apparmor does not exist. Skipping test.") + def compare_features_file(self, features_path, expected=True): + # tests that need this function should call require_apparmorfs() early + # compare features contents expected_output = testlib.read_features_dir('/sys/kernel/security/apparmor/features') with open(features_path) as f: @@ -143,6 +161,8 @@ class AAParserBasicCachingTests(AAParserCachingCommon): def test_features_match_when_caching(self): '''test features file is written when caching''' + self.require_apparmorfs() + cmd = list(self.cmd_prefix) cmd.extend(['-q', '--write-cache', '-r', self.profile]) self.run_cmd_check(cmd) @@ -203,21 +223,34 @@ class AAParserCachingTests(AAParserCachingCommon): def setUp(self): super(AAParserCachingTests, self).setUp() - # need separation of length timeout between generating profile - # and generating cache entry, as the parser distinguishes - # between ctime, not mtime. - if not 'timeout' in dir(config): - r = testlib.filesystem_time_resolution() - config.timeout = r[1] - - time.sleep(config.timeout) + r = testlib.filesystem_time_resolution() + self.mtime_res = r[1] def _generate_cache_file(self): cmd = list(self.cmd_prefix) cmd.extend(['-q', '--write-cache', '-r', self.profile]) self.run_cmd_check(cmd) - self.assert_path_exists(os.path.join(self.cache_dir, PROFILE)) + self.assert_path_exists(self.cache_file) + + def _assertTimeStampEquals(self, time1, time2): + '''Compare two timestamps to ensure equality''' + + # python 3.2 and earlier don't support writing timestamps with + # nanosecond resolution, only microsecond. When comparing + # timestamps in such an environment, loosen the equality bounds + # to compensate + # Reference: https://bugs.python.org/issue12904 + (major, minor, _) = platform.python_version_tuple() + if (int(major) < 3) or ((int(major) == 3) and (int(minor) <= 2)): + self.assertAlmostEquals(time1, time2, places=5) + else: + self.assertEquals(time1, time2) + + def _set_mtime(self, path, mtime): + atime = os.stat(path).st_atime + os.utime(path, (atime, mtime)) + self._assertTimeStampEquals(os.stat(path).st_mtime, mtime) def test_cache_loaded_when_exists(self): '''test cache is loaded when it exists, is newer than profile, and features match''' @@ -260,6 +293,8 @@ class AAParserCachingTests(AAParserCachingCommon): def test_cache_writing_does_not_overwrite_features_when_features_differ(self): '''test cache writing does not overwrite the features files when it differs and --skip-bad-cache is given''' + self.require_apparmorfs() + features_file = testlib.write_file(self.cache_dir, '.features', 'monkey\n') cmd = list(self.cmd_prefix) @@ -277,11 +312,13 @@ class AAParserCachingTests(AAParserCachingCommon): cmd = list(self.cmd_prefix) cmd.extend(['-v', '--write-cache', '--skip-bad-cache', '-r', self.profile]) self.run_cmd_check(cmd, expected_string='Replacement succeeded for') - self.assert_path_exists(os.path.join(self.cache_dir, PROFILE), expected=False) + self.assert_path_exists(self.cache_file, expected=False) def test_cache_writing_updates_features(self): '''test cache writing updates features''' + self.require_apparmorfs() + features_file = testlib.write_file(self.cache_dir, '.features', 'monkey\n') cmd = list(self.cmd_prefix) @@ -294,18 +331,18 @@ class AAParserCachingTests(AAParserCachingCommon): '''test cache writing updates cache file''' cache_file = testlib.write_file(self.cache_dir, PROFILE, 'monkey\n') - orig_size = os.stat(cache_file).st_size + orig_stat = os.stat(cache_file) cmd = list(self.cmd_prefix) cmd.extend(['-v', '--write-cache', '-r', self.profile]) self.run_cmd_check(cmd, expected_string='Replacement succeeded for') self.assert_path_exists(cache_file) - with open(cache_file, 'rb') as f: - new_size = os.fstat(f.fileno()).st_size + stat = os.stat(cache_file) # We check sizes here rather than whether the string monkey is # in cache_contents because of the difficulty coercing cache # file bytes into strings in python3 - self.assertNotEquals(orig_size, new_size, 'Expected cache file to be updated, size is not changed.') + self.assertNotEquals(orig_stat.st_size, stat.st_size, 'Expected cache file to be updated, size is not changed.') + self.assertEquals(os.stat(self.profile).st_mtime, stat.st_mtime) def test_cache_writing_clears_all_files(self): '''test cache writing clears all cache files''' @@ -317,27 +354,110 @@ class AAParserCachingTests(AAParserCachingCommon): self.run_cmd_check(cmd, expected_string='Replacement succeeded for') self.assert_path_exists(check_file, expected=False) + def test_profile_mtime_preserved(self): + '''test profile mtime is preserved when it is newest''' + expected = 1 + self._set_mtime(self.abstraction, 0) + self._set_mtime(self.profile, expected) + self._generate_cache_file() + self.assertEquals(expected, os.stat(self.cache_file).st_mtime) + + def test_abstraction_mtime_preserved(self): + '''test abstraction mtime is preserved when it is newest''' + expected = 1000 + self._set_mtime(self.profile, 0) + self._set_mtime(self.abstraction, expected) + self._generate_cache_file() + self.assertEquals(expected, os.stat(self.cache_file).st_mtime) + + def test_equal_mtimes_preserved(self): + '''test equal profile and abstraction mtimes are preserved''' + expected = 10000 + self.mtime_res + self._set_mtime(self.profile, expected) + self._set_mtime(self.abstraction, expected) + self._generate_cache_file() + self.assertEquals(expected, os.stat(self.cache_file).st_mtime) + def test_profile_newer_skips_cache(self): '''test cache is skipped if profile is newer''' self._generate_cache_file() - time.sleep(config.timeout) - testlib.touch(self.profile) + profile_mtime = os.stat(self.cache_file).st_mtime + self.mtime_res + self._set_mtime(self.profile, profile_mtime) + + orig_stat = os.stat(self.cache_file) cmd = list(self.cmd_prefix) cmd.extend(['-v', '-r', self.profile]) self.run_cmd_check(cmd, expected_string='Replacement succeeded for') + stat = os.stat(self.cache_file) + self.assertEquals(orig_stat.st_size, stat.st_size) + self.assertEquals(orig_stat.st_ino, stat.st_ino) + self.assertEquals(orig_stat.st_mtime, stat.st_mtime) + + def test_abstraction_newer_skips_cache(self): + '''test cache is skipped if abstraction is newer''' + + self._generate_cache_file() + abstraction_mtime = os.stat(self.cache_file).st_mtime + self.mtime_res + self._set_mtime(self.abstraction, abstraction_mtime) + + orig_stat = os.stat(self.cache_file) + + cmd = list(self.cmd_prefix) + cmd.extend(['-v', '-r', self.profile]) + self.run_cmd_check(cmd, expected_string='Replacement succeeded for') + + stat = os.stat(self.cache_file) + self.assertEquals(orig_stat.st_size, stat.st_size) + self.assertEquals(orig_stat.st_ino, stat.st_ino) + self.assertEquals(orig_stat.st_mtime, stat.st_mtime) + + def test_profile_newer_rewrites_cache(self): + '''test cache is rewritten if profile is newer''' + + self._generate_cache_file() + profile_mtime = os.stat(self.cache_file).st_mtime + self.mtime_res + self._set_mtime(self.profile, profile_mtime) + + orig_stat = os.stat(self.cache_file) + + cmd = list(self.cmd_prefix) + cmd.extend(['-v', '-r', '-W', self.profile]) + self.run_cmd_check(cmd, expected_string='Replacement succeeded for') + + stat = os.stat(self.cache_file) + self.assertNotEquals(orig_stat.st_ino, stat.st_ino) + self._assertTimeStampEquals(profile_mtime, stat.st_mtime) + + def test_abstraction_newer_rewrites_cache(self): + '''test cache is rewritten if abstraction is newer''' + + self._generate_cache_file() + abstraction_mtime = os.stat(self.cache_file).st_mtime + self.mtime_res + self._set_mtime(self.abstraction, abstraction_mtime) + + orig_stat = os.stat(self.cache_file) + + cmd = list(self.cmd_prefix) + cmd.extend(['-v', '-r', '-W', self.profile]) + self.run_cmd_check(cmd, expected_string='Replacement succeeded for') + + stat = os.stat(self.cache_file) + self.assertNotEquals(orig_stat.st_ino, stat.st_ino) + self._assertTimeStampEquals(abstraction_mtime, stat.st_mtime) + def test_parser_newer_uses_cache(self): '''test cache is not skipped if parser is newer''' self._generate_cache_file() - time.sleep(config.timeout) # copy parser os.mkdir(os.path.join(self.tmp_dir, 'parser')) new_parser = os.path.join(self.tmp_dir, 'parser', 'apparmor_parser') shutil.copy(config.parser, new_parser) + self._set_mtime(new_parser, os.stat(self.cache_file).st_mtime + self.mtime_res) cmd = list(self.cmd_prefix) cmd[0] = new_parser @@ -379,6 +499,7 @@ class AAParserAltCacheTests(AAParserCachingTests): self.orig_cache_dir = self.cache_dir self.cache_dir = alt_cache_dir + self.cache_file = os.path.join(self.cache_dir, PROFILE) self.cmd_prefix.extend(['--cache-loc', alt_cache_dir]) def tearDown(self): diff --git a/parser/tst/equality.sh b/parser/tst/equality.sh index 3beed2743..25260ad25 100755 --- a/parser/tst/equality.sh +++ b/parser/tst/equality.sh @@ -464,6 +464,77 @@ verify_binary_equality "change_profile == change_profile -> **" \ "/t { change_profile /**, }" \ "/t { change_profile /** -> **, }" +verify_binary_equality "profile name is hname in rule" \ + ":ns:/hname { signal peer=/hname, }" \ + ":ns:/hname { signal peer=@{profile_name}, }" + +verify_binary_inequality "profile name is NOT fq name in rule" \ + ":ns:/hname { signal peer=:ns:/hname, }" \ + ":ns:/hname { signal peer=@{profile_name}, }" + +verify_binary_equality "profile name is hname in sub pofile rule" \ + ":ns:/hname { profile child { signal peer=/hname//child, } }" \ + ":ns:/hname { profile child { signal peer=@{profile_name}, } }" + +verify_binary_inequality "profile name is NOT fq name in sub profile rule" \ + ":ns:/hname { profile child { signal peer=:ns:/hname//child, } }" \ + ":ns:/hname { profile child { signal peer=@{profile_name}, } }" + +verify_binary_equality "profile name is hname in hat rule" \ + ":ns:/hname { ^child { signal peer=/hname//child, } }" \ + ":ns:/hname { ^child { signal peer=@{profile_name}, } }" + +verify_binary_inequality "profile name is NOT fq name in hat rule" \ + ":ns:/hname { ^child { signal peer=:ns:/hname//child, } }" \ + ":ns:/hname { ^child { signal peer=@{profile_name}, } }" + +verify_binary_equality "@{profile_name} is literal in peer" \ + "/{a,b} { signal peer=/\{a,b\}, }" \ + "/{a,b} { signal peer=@{profile_name}, }" + +verify_binary_equality "@{profile_name} is literal in peer with pattern" \ + "/{a,b} { signal peer={/\{a,b\},c}, }" \ + "/{a,b} { signal peer={@{profile_name},c}, }" + +verify_binary_inequality "@{profile_name} is not pattern in peer" \ + "/{a,b} { signal peer=/{a,b}, }" \ + "/{a,b} { signal peer=@{profile_name}, }" + +verify_binary_equality "@{profile_name} is literal in peer with esc sequence" \ + "/\\\\a { signal peer=/\\\\a, }" \ + "/\\\\a { signal peer=@{profile_name}, }" + +verify_binary_equality "@{profile_name} is literal in peer with esc alt sequence" \ + "/\\{a,b\\},c { signal peer=/\\{a,b\\},c, }" \ + "/\\{a,b\\},c { signal peer=@{profile_name}, }" + + + +# verify rlimit data conversions +verify_binary_equality "set rlimit rttime <= 12 weeks" \ + "/t { set rlimit rttime <= 12 weeks, }" \ + "/t { set rlimit rttime <= $((12 * 7)) days, }" \ + "/t { set rlimit rttime <= $((12 * 7 * 24)) hours, }" \ + "/t { set rlimit rttime <= $((12 * 7 * 24 * 60)) minutes, }" \ + "/t { set rlimit rttime <= $((12 * 7 * 24 * 60 * 60)) seconds, }" \ + "/t { set rlimit rttime <= $((12 * 7 * 24 * 60 * 60 * 1000)) ms, }" \ + "/t { set rlimit rttime <= $((12 * 7 * 24 * 60 * 60 * 1000 * 1000)) us, }" \ + "/t { set rlimit rttime <= $((12 * 7 * 24 * 60 * 60 * 1000 * 1000)), }" + +verify_binary_equality "set rlimit cpu <= 42 weeks" \ + "/t { set rlimit cpu <= 42 weeks, }" \ + "/t { set rlimit cpu <= $((42 * 7)) days, }" \ + "/t { set rlimit cpu <= $((42 * 7 * 24)) hours, }" \ + "/t { set rlimit cpu <= $((42 * 7 * 24 * 60)) minutes, }" \ + "/t { set rlimit cpu <= $((42 * 7 * 24 * 60 * 60)) seconds, }" \ + "/t { set rlimit cpu <= $((42 * 7 * 24 * 60 * 60)), }" + +verify_binary_equality "set rlimit memlock <= 2GB" \ + "/t { set rlimit memlock <= 2GB, }" \ + "/t { set rlimit memlock <= $((2 * 1024)) MB, }" \ + "/t { set rlimit memlock <= $((2 * 1024 * 1024)) KB, }" \ + "/t { set rlimit memlock <= $((2 * 1024 * 1024 * 1024)) , }" \ + if [ $fails -ne 0 -o $errors -ne 0 ] then printf "ERRORS: %d\nFAILS: %d\n" $errors $fails 2>&1 diff --git a/parser/tst/simple_tests/bare_include_tests/bad_1.sd b/parser/tst/simple_tests/bare_include_tests/bad_1.sd new file mode 100644 index 000000000..71cb03857 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/bad_1.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION includes testing - non-existent include should fail +#=EXRESULT FAIL +# +/does/not/exist { + include +} diff --git a/parser/tst/simple_tests/bare_include_tests/bad_2.sd b/parser/tst/simple_tests/bare_include_tests/bad_2.sd new file mode 100644 index 000000000..5f6264693 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/bad_2.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION includes testing - mis-parsing include should fail +#=EXRESULT FAIL +# +/does/not/exist { + include does-not-exist/does-not-exist +} diff --git a/parser/tst/simple_tests/bare_include_tests/bad_3.sd b/parser/tst/simple_tests/bare_include_tests/bad_3.sd new file mode 100644 index 000000000..254bf02e0 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/bad_3.sd @@ -0,0 +1,8 @@ +# +#=DESCRIPTION includes testing - non-existent include should fail +#=EXRESULT FAIL +# +/does/not/exist { + include + include +} diff --git a/parser/tst/simple_tests/bare_include_tests/bad_4.sd b/parser/tst/simple_tests/bare_include_tests/bad_4.sd new file mode 100644 index 000000000..e59276436 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/bad_4.sd @@ -0,0 +1,8 @@ +# +#=DESCRIPTION includes testing - non-existent include should fail +#=EXRESULT FAIL +# +/does/not/exist { + include + include +} diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-bak b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-bak new file mode 100644 index 000000000..26f9e4ca2 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-bak @@ -0,0 +1 @@ +THIS WILL NOT PARSE! diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-dist b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-dist new file mode 100644 index 000000000..26f9e4ca2 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-dist @@ -0,0 +1 @@ +THIS WILL NOT PARSE! diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-new b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-new new file mode 100644 index 000000000..26f9e4ca2 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-new @@ -0,0 +1 @@ +THIS WILL NOT PARSE! diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-old b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-old new file mode 100644 index 000000000..26f9e4ca2 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.dpkg-old @@ -0,0 +1 @@ +THIS WILL NOT PARSE! diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.rpmnew b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.rpmnew new file mode 100644 index 000000000..26f9e4ca2 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.rpmnew @@ -0,0 +1 @@ +THIS WILL NOT PARSE! diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.rpmsave b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.rpmsave new file mode 100644 index 000000000..26f9e4ca2 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include.rpmsave @@ -0,0 +1 @@ +THIS WILL NOT PARSE! diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include~ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include~ new file mode 100644 index 000000000..26f9e4ca2 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/bad-include~ @@ -0,0 +1 @@ +THIS WILL NOT PARSE! diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix/good-include b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/good-include new file mode 100644 index 000000000..79dc425eb --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix/good-include @@ -0,0 +1,6 @@ +#=DESCRIPTION Valid include +# +# if parsed stand-alone, +#=EXRESULT PASS + +@{FOO} = /foo /bar diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix_1.sd b/parser/tst/simple_tests/bare_include_tests/ignored_suffix_1.sd new file mode 100644 index 000000000..c279c2c11 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix_1.sd @@ -0,0 +1,10 @@ +# +#=DESCRIPTION includes testing - verify that ignored suffixes are ignored +#=EXRESULT PASS +# + +include + +/does/not/exist { + @{FOO} r, +} diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix_2.sd b/parser/tst/simple_tests/bare_include_tests/ignored_suffix_2.sd new file mode 100644 index 000000000..7d7db1014 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix_2.sd @@ -0,0 +1,10 @@ +# +#=DESCRIPTION includes testing - verify that only suffixes are ignored +#=EXRESULT PASS +# + +include + +/does/not/exist { + @{FOO} r, +} diff --git a/parser/tst/simple_tests/bare_include_tests/ignored_suffix_2/good.dpkg-new.include b/parser/tst/simple_tests/bare_include_tests/ignored_suffix_2/good.dpkg-new.include new file mode 100644 index 000000000..79dc425eb --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ignored_suffix_2/good.dpkg-new.include @@ -0,0 +1,6 @@ +#=DESCRIPTION Valid include +# +# if parsed stand-alone, +#=EXRESULT PASS + +@{FOO} = /foo /bar diff --git a/parser/tst/simple_tests/bare_include_tests/includes_okay_helper.include b/parser/tst/simple_tests/bare_include_tests/includes_okay_helper.include new file mode 100644 index 000000000..5aee45596 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/includes_okay_helper.include @@ -0,0 +1,9 @@ +# +#=DESCRIPTION A helper for includes_okay.sd +# +# if parsed standalone, +#=EXRESULT FAIL +# + include + + /tmp/** r, diff --git a/parser/tst/simple_tests/bare_include_tests/ok_1.sd b/parser/tst/simple_tests/bare_include_tests/ok_1.sd new file mode 100644 index 000000000..766038b7b --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ok_1.sd @@ -0,0 +1,9 @@ +# +#=DESCRIPTION includes testing - basic include of global and local include +#=EXRESULT PASS +# +/does/not/exist { + #include + #include + #include +} diff --git a/parser/tst/simple_tests/bare_include_tests/ok_2.sd b/parser/tst/simple_tests/bare_include_tests/ok_2.sd new file mode 100644 index 000000000..d382fa0b5 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ok_2.sd @@ -0,0 +1,8 @@ +# +#=DESCRIPTION includes testing - test some "odd" locations of includes +#=EXRESULT PASS +# +/does/not/exist { + /does/not/exist mr, #include /bin/true Px, + include #include +} diff --git a/parser/tst/simple_tests/bare_include_tests/ok_3.sd b/parser/tst/simple_tests/bare_include_tests/ok_3.sd new file mode 100644 index 000000000..854cfd4e7 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/ok_3.sd @@ -0,0 +1,9 @@ +# +#=DESCRIPTION includes testing - basic include of a directory +#=EXRESULT PASS +# +/does/not/exist { + include + include + include +} diff --git a/parser/tst/simple_tests/bare_include_tests/recursive.sd b/parser/tst/simple_tests/bare_include_tests/recursive.sd new file mode 100644 index 000000000..83f033c97 --- /dev/null +++ b/parser/tst/simple_tests/bare_include_tests/recursive.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION includes testing - recursive include should fail +#=EXRESULT FAIL +# +/does/not/exist { + include +} diff --git a/parser/tst/simple_tests/capability/bad_outside1.sd b/parser/tst/simple_tests/capability/bad_outside1.sd new file mode 100644 index 000000000..fdbdea3af --- /dev/null +++ b/parser/tst/simple_tests/capability/bad_outside1.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION capability rule outside of a profile +#=EXRESULT FAIL +# + +capability, + diff --git a/parser/tst/simple_tests/change_profile/bad_outside_1.sd b/parser/tst/simple_tests/change_profile/bad_outside_1.sd new file mode 100644 index 000000000..c16c50ef5 --- /dev/null +++ b/parser/tst/simple_tests/change_profile/bad_outside_1.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION change_profile rule outside of a profile +#=EXRESULT FAIL +# + +change_profile -> /bin/foo, + diff --git a/parser/tst/simple_tests/dbus/bad_outside_1.sd b/parser/tst/simple_tests/dbus/bad_outside_1.sd new file mode 100644 index 000000000..61dea2c9c --- /dev/null +++ b/parser/tst/simple_tests/dbus/bad_outside_1.sd @@ -0,0 +1,5 @@ +# +#=DESCRIPTION dbus rule outside of a profile +#=EXRESULT FAIL + +dbus name=(SomeService), diff --git a/parser/tst/simple_tests/dbus/ok_bind_2.sd b/parser/tst/simple_tests/dbus/ok_bind_2.sd new file mode 100644 index 000000000..2f7daa25c --- /dev/null +++ b/parser/tst/simple_tests/dbus/ok_bind_2.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION simple dbus implicit bind acceptance test with deny keyword +#=EXRESULT PASS + +profile a_profile { + deny dbus name=(SomeService), +} diff --git a/parser/tst/simple_tests/file/bad_bare_file_outside.sd b/parser/tst/simple_tests/file/bad_bare_file_outside.sd new file mode 100644 index 000000000..a5b42e0aa --- /dev/null +++ b/parser/tst/simple_tests/file/bad_bare_file_outside.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION bare file rule outside of a profile +#=EXRESULT FAIL +# + +file, + diff --git a/parser/tst/simple_tests/file/bad_link_outside.sd b/parser/tst/simple_tests/file/bad_link_outside.sd new file mode 100644 index 000000000..7b7f2bb5a --- /dev/null +++ b/parser/tst/simple_tests/file/bad_link_outside.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION link rule outside of a profile +#=EXRESULT FAIL +# + +deny link /alpha/beta -> /tmp/**, + diff --git a/parser/tst/simple_tests/file/ok_bare_1.sd b/parser/tst/simple_tests/file/ok_bare_1.sd new file mode 100644 index 000000000..5097f388e --- /dev/null +++ b/parser/tst/simple_tests/file/ok_bare_1.sd @@ -0,0 +1,7 @@ +# +#=Description bare file rule +#=EXRESULT PASS +# +/usr/bin/foo { + deny file, +} diff --git a/parser/tst/simple_tests/mount/bad_outside_1.sd b/parser/tst/simple_tests/mount/bad_outside_1.sd new file mode 100644 index 000000000..14814452a --- /dev/null +++ b/parser/tst/simple_tests/mount/bad_outside_1.sd @@ -0,0 +1,6 @@ +# +#=Description mount rule outside of a profile +#=EXRESULT FAIL +# + + mount, diff --git a/parser/tst/simple_tests/network/bad_network_outside_1.sd b/parser/tst/simple_tests/network/bad_network_outside_1.sd new file mode 100644 index 000000000..af4142edf --- /dev/null +++ b/parser/tst/simple_tests/network/bad_network_outside_1.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION network rule outside of a profile +#=EXRESULT FAIL +# + +network, + diff --git a/parser/tst/simple_tests/ptrace/bad_outside_01.sd b/parser/tst/simple_tests/ptrace/bad_outside_01.sd new file mode 100644 index 000000000..6b0279851 --- /dev/null +++ b/parser/tst/simple_tests/ptrace/bad_outside_01.sd @@ -0,0 +1,7 @@ +# +#=Description ptrace all rule outside of a profile +#=EXRESULT FAIL +# + + ptrace, + diff --git a/parser/tst/simple_tests/rlimits/bad_rlimit_01.sd b/parser/tst/simple_tests/rlimits/bad_rlimit_01.sd new file mode 100644 index 000000000..317ea6fc3 --- /dev/null +++ b/parser/tst/simple_tests/rlimits/bad_rlimit_01.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION realtime time rlimit test with ambiguous unit 'm' which could mean 'ms' or 'minutes' +#=EXRESULT FAIL + +profile rlimit { + set rlimit rttime <= 60m, +} diff --git a/parser/tst/simple_tests/rlimits/bad_rlimit_outside_01.sd b/parser/tst/simple_tests/rlimits/bad_rlimit_outside_01.sd new file mode 100644 index 000000000..9bebb5446 --- /dev/null +++ b/parser/tst/simple_tests/rlimits/bad_rlimit_outside_01.sd @@ -0,0 +1,5 @@ +# +#=DESCRIPTION simple cpu rlimit rule outside of a profile +#=EXRESULT FAIL + +set rlimit cpu <= 1024, diff --git a/parser/tst/simple_tests/rlimits/ok_rlimit_01.sd b/parser/tst/simple_tests/rlimits/ok_rlimit_01.sd index b13bdd2c2..6757da340 100644 --- a/parser/tst/simple_tests/rlimits/ok_rlimit_01.sd +++ b/parser/tst/simple_tests/rlimits/ok_rlimit_01.sd @@ -1,5 +1,5 @@ # -#=DESCRIPTION simple cpu rlimit test +#=DESCRIPTION simple cpu rlimit test, cpu allows default units #=EXRESULT PASS profile rlimit { diff --git a/parser/tst/simple_tests/rlimits/ok_rlimit_13.sd b/parser/tst/simple_tests/rlimits/ok_rlimit_13.sd index e4cf92982..50b9a6f37 100644 --- a/parser/tst/simple_tests/rlimits/ok_rlimit_13.sd +++ b/parser/tst/simple_tests/rlimits/ok_rlimit_13.sd @@ -1,7 +1,7 @@ # -#=DESCRIPTION simple cpu rlimit test +#=DESCRIPTION simple rttime rlimit allows default units #=EXRESULT PASS profile rlimit { - set rlimit cpu <= 12, + set rlimit rttime <= 12, } diff --git a/parser/tst/simple_tests/signal/bad_outside_01.sd b/parser/tst/simple_tests/signal/bad_outside_01.sd new file mode 100644 index 000000000..cc7fb40c9 --- /dev/null +++ b/parser/tst/simple_tests/signal/bad_outside_01.sd @@ -0,0 +1,7 @@ +# +#=Description signal rule outside of a profile +#=EXRESULT FAIL +# + + signal, + diff --git a/parser/tst/simple_tests/unix/bad_outside_1.sd b/parser/tst/simple_tests/unix/bad_outside_1.sd new file mode 100644 index 000000000..da893d2ff --- /dev/null +++ b/parser/tst/simple_tests/unix/bad_outside_1.sd @@ -0,0 +1,5 @@ +# +#=DESCRIPTION unix accept rule outside of a profile +#=EXRESULT FAIL + + unix accept, diff --git a/parser/tst/simple_tests/vars/vars_profile_name_01.sd b/parser/tst/simple_tests/vars/vars_profile_name_01.sd new file mode 100644 index 000000000..a83c2e786 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_01.sd @@ -0,0 +1,8 @@ +#=DESCRIPTION reference variables in rules that also have alternations +#=EXRESULT PASS + +@{FOO}=bar + +/does/not/exist@{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_02.sd b/parser/tst/simple_tests/vars/vars_profile_name_02.sd new file mode 100644 index 000000000..672af43aa --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_02.sd @@ -0,0 +1,8 @@ +#=DESCRIPTION reference variables in rules that also have alternations +#=EXRESULT PASS + +@{FOO}=bar baz + +/does/not/exist@{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_03.sd b/parser/tst/simple_tests/vars/vars_profile_name_03.sd new file mode 100644 index 000000000..23037c86e --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_03.sd @@ -0,0 +1,8 @@ +#=DESCRIPTION profiles declared with the profile keyword can begin with var +#=EXRESULT PASS + +@{FOO}=bar + +profile @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_04.sd b/parser/tst/simple_tests/vars/vars_profile_name_04.sd new file mode 100644 index 000000000..32247591f --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_04.sd @@ -0,0 +1,8 @@ +#=DESCRIPTION profiles declared with the profile keyword can begin with var +#=EXRESULT PASS + +@{FOO}=bar baz + +profile @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_05.sd b/parser/tst/simple_tests/vars/vars_profile_name_05.sd new file mode 100644 index 000000000..1fc0758b0 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_05.sd @@ -0,0 +1,8 @@ +#=DESCRIPTION reference variables in rules that also have alternations +#=EXRESULT PASS + +@{FOO}=bar + +profile /does/not /exist{@{FOO},} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_06.sd b/parser/tst/simple_tests/vars/vars_profile_name_06.sd new file mode 100644 index 000000000..b051c24bb --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_06.sd @@ -0,0 +1,8 @@ +#=DESCRIPTION reference variables in rules that also have alternations +#=EXRESULT PASS + +@{FOO}=bar baz + +profile /does/not /exist@{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_07.sd b/parser/tst/simple_tests/vars/vars_profile_name_07.sd new file mode 100644 index 000000000..6ec43e58a --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_07.sd @@ -0,0 +1,10 @@ +#=DESCRIPTION profiles declared with the profile keyword can begin with var +#=EXRESULT FAIL +#=TODO +# This test needs check on @{FOO} attachment having leading / post var expansion + +@{FOO}=bar + +profile /does/not/exist @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_08.sd b/parser/tst/simple_tests/vars/vars_profile_name_08.sd new file mode 100644 index 000000000..99dfd5698 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_08.sd @@ -0,0 +1,10 @@ +#=DESCRIPTION profiles declared with the profile keyword can begin with var +#=EXRESULT FAIL +#=TODO +# This test needs check on @{FOO} attachment having leading / post var expansion + +@{FOO}=bar baz + +profile /does/not/exist @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_09.sd b/parser/tst/simple_tests/vars/vars_profile_name_09.sd new file mode 100644 index 000000000..69edff0f8 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_09.sd @@ -0,0 +1,9 @@ +#=DESCRIPTION reference variables in name and attachment +#=EXRESULT PASS + +@{FOO}=bar +@{BAR}=baz + +profile /does/not@{BAR} /exist@{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_10.sd b/parser/tst/simple_tests/vars/vars_profile_name_10.sd new file mode 100644 index 000000000..e6a574fd9 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_10.sd @@ -0,0 +1,9 @@ +#=DESCRIPTION reference variables in rules that also have alternations +#=EXRESULT PASS + +@{FOO}=bar baz +@{BAR}=baz + +profile /does/not@{BAR} /exist@{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_11.sd b/parser/tst/simple_tests/vars/vars_profile_name_11.sd new file mode 100644 index 000000000..ed007f562 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_11.sd @@ -0,0 +1,9 @@ +#=DESCRIPTION profiles declared with the profile keyword have var and var attachment +#=EXRESULT PASS + +@{FOO}=/bar /baz +@{BAR}=baz foo + +profile /does/not/exist@{BAR} @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_12.sd b/parser/tst/simple_tests/vars/vars_profile_name_12.sd new file mode 100644 index 000000000..8e3a40540 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_12.sd @@ -0,0 +1,11 @@ +#=DESCRIPTION profiles declared with the profile keyword can expand var and have var attachment +#=EXRESULT FAIL +#=TODO +# This test needs check on @{FOO} attachment having leading / post var expansion + +@{FOO}=bar baz +@{BAR}=baz foo + +profile /does/not/exist@{BAR} @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_13.sd b/parser/tst/simple_tests/vars/vars_profile_name_13.sd new file mode 100644 index 000000000..29797ec4e --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_13.sd @@ -0,0 +1,11 @@ +#=DESCRIPTION reference variables that are the profile name and attachment +#=EXRESULT FAIL +#=TODO +# This test needs check on @{FOO} attachment having leading / post var expansion + +@{FOO}=bar +@{BAR}=baz + +profile @{BAR} @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_14.sd b/parser/tst/simple_tests/vars/vars_profile_name_14.sd new file mode 100644 index 000000000..feffe8139 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_14.sd @@ -0,0 +1,11 @@ +#=DESCRIPTION reference variables in rules that also have alternations +#=EXRESULT PASS +#=TODO +# This test needs check on @{FOO} attachment having leading / post var expansion + +@{FOO}=/bar /baz +@{BAR}=baz + +profile @{BAR} @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_15.sd b/parser/tst/simple_tests/vars/vars_profile_name_15.sd new file mode 100644 index 000000000..37aa3886b --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_15.sd @@ -0,0 +1,11 @@ +#=DESCRIPTION profiles declared with the profile keyword can begin with var +#=EXRESULT FAIL +#=TODO +# This test needs check on @{FOO} attachment having leading / post var expansion + +@{FOO}=bar baz +@{BAR}=baz foo + +profile @{BAR} @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_16.sd b/parser/tst/simple_tests/vars/vars_profile_name_16.sd new file mode 100644 index 000000000..8d0057d5d --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_16.sd @@ -0,0 +1,9 @@ +#=DESCRIPTION var in sub profile name +#=EXRESULT PASS + +@{FOO}=bar + +profile /does/not/exist { + profile foo@{FOO} { + } +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_17.sd b/parser/tst/simple_tests/vars/vars_profile_name_17.sd new file mode 100644 index 000000000..32515f317 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_17.sd @@ -0,0 +1,9 @@ +#=DESCRIPTION var in sub profile name +#=EXRESULT PASS + +@{FOO}=bar + +profile /does/not/exist { + profile @{FOO} { + } +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_18.sd b/parser/tst/simple_tests/vars/vars_profile_name_18.sd new file mode 100644 index 000000000..2a5b24dff --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_18.sd @@ -0,0 +1,9 @@ +#=DESCRIPTION var in hat name +#=EXRESULT PASS + +@{FOO}=bar + +profile /does/not/exist { + ^foo@{FOO} { + } +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_19.sd b/parser/tst/simple_tests/vars/vars_profile_name_19.sd new file mode 100644 index 000000000..32515f317 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_19.sd @@ -0,0 +1,9 @@ +#=DESCRIPTION var in sub profile name +#=EXRESULT PASS + +@{FOO}=bar + +profile /does/not/exist { + profile @{FOO} { + } +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_20.sd b/parser/tst/simple_tests/vars/vars_profile_name_20.sd new file mode 100644 index 000000000..8d0057d5d --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_20.sd @@ -0,0 +1,9 @@ +#=DESCRIPTION var in sub profile name +#=EXRESULT PASS + +@{FOO}=bar + +profile /does/not/exist { + profile foo@{FOO} { + } +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_21.sd b/parser/tst/simple_tests/vars/vars_profile_name_21.sd new file mode 100644 index 000000000..6e1575a2a --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_21.sd @@ -0,0 +1,9 @@ +#=DESCRIPTION var in hat name +#=EXRESULT PASS + +@{FOO}=bar + +profile /does/not/exist { + ^@{FOO} { + } +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_22.sd b/parser/tst/simple_tests/vars/vars_profile_name_22.sd new file mode 100644 index 000000000..a42afba28 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_22.sd @@ -0,0 +1,10 @@ +#=DESCRIPTION all attachment expansions must start with / +#=EXRESULT FAIL +#=TODO +# This test needs check on @{FOO} attachment having leading / post var expansion + +@{FOO}=/bar baz + +profile /does/not/exist @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_23.sd b/parser/tst/simple_tests/vars/vars_profile_name_23.sd new file mode 100644 index 000000000..5bb212269 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_23.sd @@ -0,0 +1,7 @@ +#=DESCRIPTION reference variables in profile name is undefined +#=EXRESULT FAIL + + +/does/not/exist@{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_24.sd b/parser/tst/simple_tests/vars/vars_profile_name_24.sd new file mode 100644 index 000000000..ebfb40324 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_24.sd @@ -0,0 +1,8 @@ +#=DESCRIPTION reference variables is null +#=EXRESULT FAIL + +@{FOO}= + +/does/not/exist@{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_25.sd b/parser/tst/simple_tests/vars/vars_profile_name_25.sd new file mode 100644 index 000000000..56ce8bae7 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_25.sd @@ -0,0 +1,10 @@ +#=DESCRIPTION reference variables is null +#=EXRESULT FAIL +#=TODO +#needs post var expansion check that variable contained a value + +@{FOO}= + +@{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_26.sd b/parser/tst/simple_tests/vars/vars_profile_name_26.sd new file mode 100644 index 000000000..e81acb944 --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_26.sd @@ -0,0 +1,10 @@ +#=DESCRIPTION reference variables is null +#=EXRESULT FAIL +#=TODO +#needs post var expansion check that variable contained a value + +@{FOO}= + +profile bar @{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_bad_1.sd b/parser/tst/simple_tests/vars/vars_profile_name_bad_1.sd new file mode 100644 index 000000000..0b308c82c --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_bad_1.sd @@ -0,0 +1,8 @@ +#=DESCRIPTION bare profile names must start with / +#=EXRESULT FAIL + +@{FOO}=bar + +@{FOO} { + /does/not/exist r, +} diff --git a/parser/tst/simple_tests/vars/vars_profile_name_bad_2.sd b/parser/tst/simple_tests/vars/vars_profile_name_bad_2.sd new file mode 100644 index 000000000..009d0b86f --- /dev/null +++ b/parser/tst/simple_tests/vars/vars_profile_name_bad_2.sd @@ -0,0 +1,6 @@ +#=DESCRIPTION special @{profile_name} not defined for profile name declaration +#=EXRESULT FAIL + +profile @{profile_name} { + /does/not/exist r, +} diff --git a/parser/tst/testlib.py b/parser/tst/testlib.py index b3474f39d..d15aa5ecc 100644 --- a/parser/tst/testlib.py +++ b/parser/tst/testlib.py @@ -48,6 +48,8 @@ class AANoCleanupMetaClass(type): def new_unittest_func(self): try: return unittest_func(self) + except unittest.SkipTest: + raise except Exception: self.do_cleanup = False raise @@ -135,7 +137,7 @@ class TimeoutFunction: def filesystem_time_resolution(): - '''detect whether the filesystem stores sub 1 second timestamps''' + '''detect whether the filesystem stores subsecond timestamps''' default_diff = 0.1 result = (True, default_diff) @@ -150,7 +152,7 @@ def filesystem_time_resolution(): s = os.fstat(f.fileno()) if (s.st_mtime == last_stamp): - print('\n===> WARNING: TMPDIR lacks nanosecond timestamp resolution, falling back to slower test') + print('\n===> WARNING: TMPDIR lacks subsecond timestamp resolution, falling back to slower test') result = (False, 1.0) break diff --git a/presentations/LSS_apparmor-labeling-2013.odp b/presentations/LSS_apparmor-labeling-2013.odp new file mode 100644 index 000000000..82b801b75 Binary files /dev/null and b/presentations/LSS_apparmor-labeling-2013.odp differ diff --git a/presentations/LSS_apparmor-userspace-2013.odp b/presentations/LSS_apparmor-userspace-2013.odp new file mode 100644 index 000000000..87ae5f889 Binary files /dev/null and b/presentations/LSS_apparmor-userspace-2013.odp differ diff --git a/profiles/Makefile b/profiles/Makefile index 44abdd719..1e5beda1e 100644 --- a/profiles/Makefile +++ b/profiles/Makefile @@ -78,11 +78,8 @@ endif IGNORE_FILES=${EXTRAS_SOURCE}/README CHECK_PROFILES=$(filter-out ${IGNORE_FILES} ${SUBDIRS}, $(wildcard ${PROFILES_SOURCE}/*) $(wildcard ${EXTRAS_SOURCE}/*)) -# check-logprof is disabled here because it has not kept up with -# advances in the apparmor policy language. Re-enable when it is -# updated. .PHONY: check -check: check-parser +check: check-parser check-logprof .PHONY: check-parser check-parser: local diff --git a/profiles/apparmor.d/abstractions/X b/profiles/apparmor.d/abstractions/X index 147dc994a..362586ff2 100644 --- a/profiles/apparmor.d/abstractions/X +++ b/profiles/apparmor.d/abstractions/X @@ -27,6 +27,9 @@ unix (connect, receive, send) type=stream peer=(addr="@/tmp/.X11-unix/X[0-9]*"), + unix (connect, receive, send) + type=stream + peer=(addr="@/tmp/.ICE-unix/[0-9]*"), /usr/include/X11/ r, /usr/include/X11/** r, diff --git a/profiles/apparmor.d/abstractions/base b/profiles/apparmor.d/abstractions/base index 550822f2e..266a9222c 100644 --- a/profiles/apparmor.d/abstractions/base +++ b/profiles/apparmor.d/abstractions/base @@ -26,6 +26,7 @@ /etc/locale/** r, /etc/locale.alias r, /etc/localtime r, + /usr/share/locale-bundle/** r, /usr/share/locale-langpack/** r, /usr/share/locale/** r, /usr/share/**/locale/** r, diff --git a/profiles/apparmor.d/abstractions/dconf b/profiles/apparmor.d/abstractions/dconf index 1bf309232..7ef697832 100644 --- a/profiles/apparmor.d/abstractions/dconf +++ b/profiles/apparmor.d/abstractions/dconf @@ -3,5 +3,6 @@ # permissions for querying dconf settings; granting write access should # be specified in a specific application's profile. + /etc/dconf/** r, owner /{,var/}run/user/*/dconf/user r, owner @{HOME}/.config/dconf/user r, diff --git a/profiles/apparmor.d/abstractions/nameservice b/profiles/apparmor.d/abstractions/nameservice index 6d695d5d9..27dc2ac9a 100644 --- a/profiles/apparmor.d/abstractions/nameservice +++ b/profiles/apparmor.d/abstractions/nameservice @@ -38,6 +38,9 @@ # /etc/resolvconf/run/resolv.conf /{,var/}run/resolvconf/resolv.conf r, /etc/resolvconf/run/resolv.conf r, + # on systems using systemd's networkd, /etc/resolv.conf is a symlink to + # /run/systemd/resolve/resolv.conf + /{,var/}run/systemd/resolve/resolv.conf r, /etc/samba/lmhosts r, /etc/services r, diff --git a/profiles/apparmor.d/abstractions/python b/profiles/apparmor.d/abstractions/python index b528f989f..ceb0d6e9e 100644 --- a/profiles/apparmor.d/abstractions/python +++ b/profiles/apparmor.d/abstractions/python @@ -10,18 +10,18 @@ # # ------------------------------------------------------------------ - /usr/lib{,32,64}/python{2.[4-7],3.[0-4]}/**.{pyc,so} mr, - /usr/lib{,32,64}/python{2.[4-7],3.[0-4]}/**.{egg,py,pth} r, - /usr/lib{,32,64}/python{2.[4-7],3.[0-4]}/{site,dist}-packages/ r, - /usr/lib{,32,64}/python3.[0-4]/lib-dynload/*.so mr, + /usr/lib{,32,64}/python{2.[4-7],3.[0-5]}/**.{pyc,so} mr, + /usr/lib{,32,64}/python{2.[4-7],3.[0-5]}/**.{egg,py,pth} r, + /usr/lib{,32,64}/python{2.[4-7],3.[0-5]}/{site,dist}-packages/ r, + /usr/lib{,32,64}/python3.[0-5]/lib-dynload/*.so mr, - /usr/local/lib{,32,64}/python{2.[4-7],3.[0-4]}/**.{pyc,so} mr, - /usr/local/lib{,32,64}/python{2.[4-7],3.[0-4]}/**.{egg,py,pth} r, - /usr/local/lib{,32,64}/python{2.[4-7],3.[0-4]}/{site,dist}-packages/ r, - /usr/local/lib{,32,64}/python3.[0-4]/lib-dynload/*.so mr, + /usr/local/lib{,32,64}/python{2.[4-7],3.[0-5]}/**.{pyc,so} mr, + /usr/local/lib{,32,64}/python{2.[4-7],3.[0-5]}/**.{egg,py,pth} r, + /usr/local/lib{,32,64}/python{2.[4-7],3.[0-5]}/{site,dist}-packages/ r, + /usr/local/lib{,32,64}/python3.[0-5]/lib-dynload/*.so mr, # Site-wide configuration - /etc/python{2.[4-7],3.[0-4]}/** r, + /etc/python{2.[4-7],3.[0-5]}/** r, # shared python paths /usr/share/{pyshared,pycentral,python-support}/** r, @@ -34,4 +34,4 @@ /usr/lib/wx/python/*.pth r, # python build configuration and headers - /usr/include/python{2.[4-7],3.[0-4]}*/pyconfig.h r, + /usr/include/python{2.[4-7],3.[0-5]}*/pyconfig.h r, diff --git a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/plugins-common b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/plugins-common index 2a95b1996..c928f92cc 100644 --- a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/plugins-common +++ b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/plugins-common @@ -5,10 +5,10 @@ # @{PROC}/@{pid}/fd/ r, /usr/lib/** rm, - /bin/bash ixr, - /bin/dash ixr, - /bin/grep ixr, - /bin/sed ixr, + /{,usr/}bin/bash ixr, + /{,usr/}bin/dash ixr, + /{,usr/}bin/grep ixr, + /{,usr/}bin/sed ixr, /usr/bin/m4 ixr, # Since all the ubuntu-browsers.d abstractions need this, just include it diff --git a/profiles/apparmor.d/apache2.d/phpsysinfo b/profiles/apparmor.d/apache2.d/phpsysinfo index 349013445..669f7a491 100644 --- a/profiles/apparmor.d/apache2.d/phpsysinfo +++ b/profiles/apparmor.d/apache2.d/phpsysinfo @@ -8,10 +8,10 @@ #include #include - /bin/dash ixr, - /bin/df ixr, - /bin/mount ixr, - /bin/uname ixr, + /{,usr/}bin/dash ixr, + /{,usr/}bin/df ixr, + /{,usr/}bin/mount ixr, + /{,usr/}bin/uname ixr, /dev/bus/usb/ r, /dev/bus/usb/** r, /etc/debian_version r, diff --git a/profiles/apparmor.d/bin.ping b/profiles/apparmor.d/bin.ping index 4752b4784..5eaf6941d 100644 --- a/profiles/apparmor.d/bin.ping +++ b/profiles/apparmor.d/bin.ping @@ -19,7 +19,7 @@ capability setuid, network inet raw, - /bin/ping mixr, + /{,usr/}bin/ping mixr, /etc/modules.conf r, # Site-specific additions and overrides. See local/README for details. diff --git a/profiles/apparmor.d/sbin.syslog-ng b/profiles/apparmor.d/sbin.syslog-ng index ce4bd42b3..a4004c18f 100644 --- a/profiles/apparmor.d/sbin.syslog-ng +++ b/profiles/apparmor.d/sbin.syslog-ng @@ -20,6 +20,7 @@ profile syslog-ng /{usr/,}sbin/syslog-ng { #include #include #include + #include capability chown, capability dac_override, @@ -37,7 +38,10 @@ profile syslog-ng /{usr/,}sbin/syslog-ng { /dev/syslog w, /dev/tty10 rw, /dev/xconsole rw, + /etc/machine-id r, /etc/syslog-ng/* r, + /etc/syslog-ng/conf.d/ r, + /etc/syslog-ng/conf.d/* r, @{PROC}/kmsg r, /etc/hosts.deny r, /etc/hosts.allow r, @@ -50,6 +54,10 @@ profile syslog-ng /{usr/,}sbin/syslog-ng { @{CHROOT_BASE}/var/log/** w, @{CHROOT_BASE}/{,var/}run/syslog-ng.pid krw, @{CHROOT_BASE}/{,var/}run/syslog-ng.ctl rw, + /{var,var/run,run}/log/journal/ r, + /{var,var/run,run}/log/journal/*/ r, + /{var,var/run,run}/log/journal/*/*.journal r, + /{var/,}run/syslog-ng.ctl a, /{var/,}run/syslog-ng/additional-log-sockets.conf r, # Site-specific additions and overrides. See local/README for details. diff --git a/profiles/apparmor.d/usr.lib.dovecot.imap b/profiles/apparmor.d/usr.lib.dovecot.imap index dd103e702..51b2acee5 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.imap +++ b/profiles/apparmor.d/usr.lib.dovecot.imap @@ -27,6 +27,7 @@ @{HOME} r, # ??? /usr/lib/dovecot/imap mr, /{,var/}run/dovecot/auth-master rw, + /{,var/}run/dovecot/mounts r, # Site-specific additions and overrides. See local/README for details. #include diff --git a/profiles/apparmor.d/usr.sbin.avahi-daemon b/profiles/apparmor.d/usr.sbin.avahi-daemon index 641701045..fa0fb3c94 100644 --- a/profiles/apparmor.d/usr.sbin.avahi-daemon +++ b/profiles/apparmor.d/usr.sbin.avahi-daemon @@ -26,6 +26,7 @@ /{,var/}run/avahi-daemon/ w, /{,var/}run/avahi-daemon/pid krw, /{,var/}run/avahi-daemon/socket w, + /{,var/}run/systemd/notify w, # Site-specific additions and overrides. See local/README for details. #include diff --git a/profiles/apparmor.d/usr.sbin.dnsmasq b/profiles/apparmor.d/usr.sbin.dnsmasq index 6dd71d445..4ee0913e4 100644 --- a/profiles/apparmor.d/usr.sbin.dnsmasq +++ b/profiles/apparmor.d/usr.sbin.dnsmasq @@ -29,9 +29,13 @@ signal (receive) peer=/usr/sbin/libvirtd, ptrace (readby) peer=/usr/sbin/libvirtd, + owner /dev/tty rw, + /etc/dnsmasq.conf r, /etc/dnsmasq.d/ r, /etc/dnsmasq.d/* r, + /etc/dnsmasq.d-available/ r, + /etc/dnsmasq.d-available/* r, /etc/ethers r, /etc/NetworkManager/dnsmasq.d/ r, /etc/NetworkManager/dnsmasq.d/* r, @@ -45,7 +49,7 @@ /var/lib/misc/dnsmasq.leases rw, # Required only for DHCP server usage - /bin/bash ix, # Required to execute --dhcp-script argument + /{,usr/}bin/{ba,da,}sh ix, # Required to execute --dhcp-script argument # access to iface mtu needed for Router Advertisement messages in IPv6 # Neighbor Discovery protocol (RFC 2461) @@ -55,19 +59,16 @@ @{TFTP_DIR}/ r, @{TFTP_DIR}/** r, - # libvirt config, lease and hosts files for dnsmasq + # libvirt config and hosts file for dnsmasq /var/lib/libvirt/dnsmasq/ r, /var/lib/libvirt/dnsmasq/* r, - /var/lib/libvirt/dnsmasq/*.leases rw, - /var/lib/libvirt/dnsmasq/*.status* rw, # libvirt pid files for dnsmasq /{,var/}run/libvirt/network/ r, /{,var/}run/libvirt/network/*.pid rw, # libvirt lease helper - /usr/lib{,64}/libvirt/libvirt_leaseshelper ix, - /{,var/}run/leaseshelper.pid rwk, + /usr/lib{,64}/libvirt/libvirt_leaseshelper Cx -> libvirt_leaseshelper, # lxc-net pid and lease files /{,var/}run/lxc/dnsmasq.pid rw, @@ -79,6 +80,25 @@ /{,var/}run/NetworkManager/dnsmasq.conf r, /{,var/}run/NetworkManager/dnsmasq.pid w, + profile libvirt_leaseshelper { + #include + + /etc/libnl-3/classid r, + + owner @{PROC}/@{pid}/net/psched r, + owner @{PROC}/@{pid}/status r, + + /sys/devices/system/cpu/ r, + /sys/devices/system/node/ r, + /sys/devices/system/node/*/meminfo r, + + # libvirt lease and status files for dnsmasq + /var/lib/libvirt/dnsmasq/*.leases rw, + /var/lib/libvirt/dnsmasq/*.status* rw, + + /{,var/}run/leaseshelper.pid rwk, + } + # Site-specific additions and overrides. See local/README for details. #include } diff --git a/profiles/apparmor.d/usr.sbin.ntpd b/profiles/apparmor.d/usr.sbin.ntpd index d1c3f6f98..dab6fa8f5 100644 --- a/profiles/apparmor.d/usr.sbin.ntpd +++ b/profiles/apparmor.d/usr.sbin.ntpd @@ -37,6 +37,7 @@ /etc/ntpd.conf.tmp r, /tmp/ntp* rwl, + /{usr/,usr/local/,}{s,}bin/ r, /usr/sbin/ntpd rmix, /var/lib/ntp/drift rwl, /var/lib/ntp/drift.TEMP rwl, diff --git a/profiles/apparmor.d/usr.sbin.smbldap-useradd b/profiles/apparmor.d/usr.sbin.smbldap-useradd index 4376e3d78..a2eb1c17f 100644 --- a/profiles/apparmor.d/usr.sbin.smbldap-useradd +++ b/profiles/apparmor.d/usr.sbin.smbldap-useradd @@ -8,7 +8,7 @@ #include /dev/tty rw, - /bin/bash ix, + /{,usr/}bin/bash ix, /etc/init.d/nscd Cx, /etc/shadow r, /etc/smbldap-tools/smbldap.conf r, @@ -26,9 +26,9 @@ capability sys_ptrace, - /bin/bash r, - /bin/mountpoint rix, - /bin/systemctl rix, + /{,usr/}bin/bash r, + /{,usr/}bin/mountpoint rix, + /{,usr/}bin/systemctl rix, /dev/tty rw, /etc/init.d/nscd r, /etc/rc.status r, diff --git a/profiles/apparmor.d/usr.sbin.winbindd b/profiles/apparmor.d/usr.sbin.winbindd index 497d1a5ab..981aa41b6 100644 --- a/profiles/apparmor.d/usr.sbin.winbindd +++ b/profiles/apparmor.d/usr.sbin.winbindd @@ -15,7 +15,7 @@ /etc/samba/secrets.tdb rwk, /etc/samba/smbd.tmp/ rw, /etc/samba/smbd.tmp/msg/ rw, - /etc/samba/smbd.tmp/msg/* rw, + /etc/samba/smbd.tmp/msg/* rwk, @{PROC}/sys/kernel/core_pattern r, /tmp/.winbindd/ w, /tmp/krb5cc_* rwk, diff --git a/profiles/apparmor/profiles/extras/sbin.dhclient b/profiles/apparmor/profiles/extras/sbin.dhclient index b077d7ae9..0e870a0fd 100644 --- a/profiles/apparmor/profiles/extras/sbin.dhclient +++ b/profiles/apparmor/profiles/extras/sbin.dhclient @@ -1,6 +1,7 @@ # ------------------------------------------------------------------ # # Copyright (C) 2002-2005 Novell/SUSE +# Copyright (C) 2015 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 @@ -25,6 +26,8 @@ #include #include + capability net_raw, + network packet packet, network packet raw, @@ -47,13 +50,17 @@ /usr/bin/uptime mrix, /usr/bin/vmstat mrix, /usr/bin/w mrix, + /usr/lib/nm-dhcp-helper rix, /var/lib/dhcp/dhclient.leases rw, /var/lib/dhcp/dhclient-*.leases rw, + /var/lib/dhcp6/dhclient.leases rw, + /var/lib/NetworkManager/dhclient-*.conf r, + /var/lib/NetworkManager/dhclient-*.lease rw, /var/log/lastlog r, /var/log/messages r, /var/log/wtmp r, - /{,var/}run/dhclient.pid rw, - /{,var/}run/dhclient-*.pid rw, + /{,var/}run/dhclient.pid rw, + /{,var/}run/dhclient-*.pid rw, /var/spool r, /var/spool/mail r, diff --git a/profiles/apparmor/profiles/extras/usr.bin.skype b/profiles/apparmor/profiles/extras/usr.bin.skype index dd8a4d2a1..b92492bb7 100644 --- a/profiles/apparmor/profiles/extras/usr.bin.skype +++ b/profiles/apparmor/profiles/extras/usr.bin.skype @@ -20,6 +20,7 @@ @{PROC}/sys/kernel/{ostype,osrelease} r, @{PROC}/@{pid}/net/arp r, + @{PROC}/@{pid}/net/dev r, owner @{PROC}/@{pid}/auxv r, owner @{PROC}/@{pid}/cmdline r, owner @{PROC}/@{pid}/fd/ r, diff --git a/tests/regression/apparmor/Makefile b/tests/regression/apparmor/Makefile index c0aad62af..892f1c5c8 100644 --- a/tests/regression/apparmor/Makefile +++ b/tests/regression/apparmor/Makefile @@ -18,7 +18,7 @@ ifdef USE_SYSTEM echo -lapparmor ; \ fi ) ifeq ($(strip $(LIBAPPARMOR)),) - ERROR_MESSAGE = $(error ${nl}\ + LIBAPPARMOR_ERROR_MESSAGE = $(error ${nl}\ ************************************************************************${nl}\ Unable to find libapparmor installed on this system; either${nl}\ install libapparmor devel packages, set the LIBAPPARMOR variable${nl}\ @@ -27,13 +27,23 @@ manually, or build against in-tree libapparmor.${nl}\ endif # LIBAPPARMOR not set LDLIBS += $(LIBAPPARMOR) + AA_EXEC = $(shell which aa-exec) + ifeq ($(AA_EXEC),) + AA_EXEC_ERROR_MESSAGE = $(error ${nl}\ +************************************************************************${nl}\ +Unable to find aa-exec installed on this system; either install the${nl}\ +apparmor package, set the AA_EXEC variable manually, or use the in-tree${nl}\ +aa-exec.${nl}\ +************************************************************************${nl}) + endif # AA_EXEC not set + else # !USE_SYSTEM # use in-tree versions LIBAPPARMOR_SRC := ../../../libraries/libapparmor/ LIBAPPARMOR_INCLUDE = $(LIBAPPARMOR_SRC)/include LIBAPPARMOR_PATH := $(LIBAPPARMOR_SRC)/src/.libs/ ifeq ($(realpath $(LIBAPPARMOR_PATH)/libapparmor.a),) - ERROR_MESSAGE = $(error ${nl}\ + LIBAPPARMOR_ERROR_MESSAGE = $(error ${nl}\ ************************************************************************${nl}\ $(LIBAPPARMOR_PATH)/libapparmor.a is missing; either build against${nl}\ the in-tree libapparmor by building it first and then trying again${nl}\ @@ -42,6 +52,17 @@ libapparmor by adding USE_SYSTEM=1 to your make command.${nl}\ ************************************************************************${nl}) endif + BINUTILS_SRC := ../../../binutils + AA_EXEC = $(BINUTILS_SRC)/aa-exec + ifeq ($(realpath $(AA_EXEC)),) + AA_EXEC_ERROR_MESSAGE = $(error ${nl}\ +************************************************************************${nl}\ +$(AA_EXEC) is missing; either build the $(BINUTILS_SRC) directory${nl}\ +and then try again (see the top-level README for help) or use the${nl}\ +system aa-exec by adding USE_SYSTEM=1 to your make command.${nl}\ +************************************************************************${nl}) + endif + CFLAGS += -L$(LIBAPPARMOR_PATH) -I$(LIBAPPARMOR_INCLUDE) LDLIBS += -Wl,-Bstatic -lapparmor -Wl,-Bdynamic -lpthread endif # USE_SYSTEM @@ -152,7 +173,8 @@ endif EXEC=$(SRC:%.c=%) -TESTS=access \ +TESTS=aa_exec \ + access \ introspect \ capabilities \ changeprofile \ @@ -217,9 +239,11 @@ RISKY_TESTS= .PHONY: libapparmor_check .SILENT: libapparmor_check -libapparmor_check: ; $(ERROR_MESSAGE) +libapparmor_check: ; $(LIBAPPARMOR_ERROR_MESSAGE) -all: libapparmor_check $(EXEC) changehat.h uservars.inc +aa_exec_check: ; $(AA_EXEC_ERROR_MESSAGE) + +all: libapparmor_check aa_exec_check $(EXEC) changehat.h uservars.inc uservars.inc: uservars.inc.source uservars.inc.system ifdef USE_SYSTEM diff --git a/tests/regression/apparmor/aa_exec.sh b/tests/regression/apparmor/aa_exec.sh new file mode 100755 index 000000000..daaefee9b --- /dev/null +++ b/tests/regression/apparmor/aa_exec.sh @@ -0,0 +1,81 @@ +#! /bin/bash +# Copyright (C) 2015 Canonical, Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, version 2 of the +# License. + +#=NAME aa_exec +#=DESCRIPTION +# This test verifies that the aa_exec command is indeed transitioning +# profiles as intended. +#=END + +#set -x + +pwd=`dirname $0` +pwd=`cd $pwd ; /bin/pwd` + +bin=$pwd + +. $bin/prologue.inc + +ns=aa_exec_ns + +genprofile_aa_exec() +{ + mode="" + if [ $# -eq 2 ]; then + if [ $2 -ne 0 ]; then + mode="(complain) " + fi + fi + genprofile --stdin <&1) +rc=$? + +if [ $rc -eq 0 ] && [ "$out" == "$2" ]; then + echo PASS + exit 0 +elif [ $rc -ne 0 ]; then + echo "FAIL: aa-exec exited with status ${rc}:\n${out}\n" + exit 1 +else + echo "FAIL: bad confinement context: \"$out\" != \"$2 $3\"" + exit 1 +fi diff --git a/tests/regression/apparmor/link_subset.c b/tests/regression/apparmor/link_subset.c index 4af832c4a..e908487cb 100644 --- a/tests/regression/apparmor/link_subset.c +++ b/tests/regression/apparmor/link_subset.c @@ -15,37 +15,38 @@ #include "changehat.h" -/* actual mapping does not match kernel, just needed for bit manging */ -#define AA_MAY_EXEC 0x001 -#define AA_MAY_WRITE 0x002 -#define AA_MAY_READ 0x004 -#define AA_MAY_APPEND 0x008 +/* Create all possible link + other permission combinations + * actual mapping does NOT match kernel, just needed for bit mangling */ +#define TEST_MAY_EXEC 0x001 +#define TEST_MAY_WRITE 0x002 +#define TEST_MAY_READ 0x004 +#define TEST_MAY_APPEND 0x008 -#define AA_MAY_LINK 0x0010 -#define AA_MAY_LOCK 0x0020 -#define AA_MAY_MOUNT 0x0040 -#define AA_EXEC_MMAP 0x0080 +#define TEST_MAY_LINK 0x0010 +#define TEST_MAY_LOCK 0x0020 +#define TEST_MAY_MOUNT 0x0040 +#define TEST_EXEC_MMAP 0x0080 -#define AA_EXEC_UNSAFE 0x0100 -#define AA_EXEC_INHERIT 0x0200 +#define TEST_EXEC_UNSAFE 0x0100 +#define TEST_EXEC_INHERIT 0x0200 -#define AA_EXEC_MOD_0 0x0400 -#define AA_EXEC_MOD_1 0x0800 -#define AA_EXEC_MOD_2 0x1000 -#define AA_EXEC_MOD_3 0x2000 +#define TEST_EXEC_MOD_0 0x0400 +#define TEST_EXEC_MOD_1 0x0800 +#define TEST_EXEC_MOD_2 0x1000 +#define TEST_EXEC_MOD_3 0x2000 -#define AA_EXEC_MODIFIERS (AA_EXEC_MOD_0 | AA_EXEC_MOD_1 | \ - AA_EXEC_MOD_2 | AA_EXEC_MOD_3) +#define TEST_EXEC_MODIFIERS (TEST_EXEC_MOD_0 | TEST_EXEC_MOD_1 | \ + TEST_EXEC_MOD_2 | TEST_EXEC_MOD_3) -#define AA_EXEC_TYPE (AA_MAY_EXEC | AA_EXEC_UNSAFE | AA_EXEC_INHERIT | \ - AA_EXEC_MODIFIERS) +#define TEST_EXEC_TYPE (TEST_MAY_EXEC | TEST_EXEC_UNSAFE | TEST_EXEC_INHERIT | \ + TEST_EXEC_MODIFIERS) -#define AA_EXEC_UNCONFINED AA_EXEC_MOD_0 -#define AA_EXEC_PROFILE AA_EXEC_MOD_1 -#define AA_EXEC_LOCAL (AA_EXEC_MOD_0 | AA_EXEC_MOD_1) +#define TEST_EXEC_UNCONFINED TEST_EXEC_MOD_0 +#define TEST_EXEC_PROFILE TEST_EXEC_MOD_1 +#define TEST_EXEC_LOCAL (TEST_EXEC_MOD_0 | TEST_EXEC_MOD_1) -#define MAX_PERM (AA_EXEC_MOD_2) +#define MAX_PERM (TEST_EXEC_MOD_2) #define MAX_PERM_LEN 10 @@ -57,38 +58,38 @@ int valid_link_perm_subset(int tperm, int lperm) { /* link must always have link bit set */ - if (!(lperm & AA_MAY_LINK)) + if (!(lperm & TEST_MAY_LINK)) return 0; - lperm = lperm & ~AA_MAY_LINK; + lperm = lperm & ~TEST_MAY_LINK; /* an empty permission set is always a subset of target */ if (!lperm) return 1; /* ix implies mix */ - if (lperm & AA_EXEC_INHERIT) - lperm |= AA_EXEC_MMAP; - if (tperm & AA_EXEC_INHERIT) - tperm |= AA_EXEC_MMAP; + if (lperm & TEST_EXEC_INHERIT) + lperm |= TEST_EXEC_MMAP; + if (tperm & TEST_EXEC_INHERIT) + tperm |= TEST_EXEC_MMAP; /* w implies a */ - if (lperm & AA_MAY_WRITE) - lperm |= AA_MAY_APPEND; - if (tperm & AA_MAY_WRITE) - tperm |= AA_MAY_APPEND; + if (lperm & TEST_MAY_WRITE) + lperm |= TEST_MAY_APPEND; + if (tperm & TEST_MAY_WRITE) + tperm |= TEST_MAY_APPEND; /* currently no such thing as a safe ix - probably should be * depending on how the rule is written */ -// if ((tperm & AA_EXEC_MODIFIERS) == AA_EXEC_INHERIT && !(tperm & AA_EXEC_UNSAFE)) -// tperm |= AA_EXEC_UNSAFE; +// if ((tperm & TEST_EXEC_MODIFIERS) == TEST_EXEC_INHERIT && !(tperm & TEST_EXEC_UNSAFE)) +// tperm |= TEST_EXEC_UNSAFE; /* treat safe exec as subset of unsafe exec */ - if (!(lperm & AA_EXEC_UNSAFE)) - lperm |= AA_EXEC_UNSAFE & tperm; + if (!(lperm & TEST_EXEC_UNSAFE)) + lperm |= TEST_EXEC_UNSAFE & tperm; /* check that exec mode, if present, matches */ - if ((lperm & AA_MAY_EXEC) && ((lperm & AA_EXEC_TYPE) != (tperm & AA_EXEC_TYPE))) + if ((lperm & TEST_MAY_EXEC) && ((lperm & TEST_EXEC_TYPE) != (tperm & TEST_EXEC_TYPE))) return 0; return !(lperm & ~tperm); @@ -98,51 +99,51 @@ void permstring(char *buffer, int mask) { char *b = buffer; - if (mask & AA_EXEC_MMAP) + if (mask & TEST_EXEC_MMAP) *b++ = 'm'; - if (mask & AA_MAY_READ) + if (mask & TEST_MAY_READ) *b++ = 'r'; - if (mask & AA_MAY_WRITE) + if (mask & TEST_MAY_WRITE) *b++ = 'w'; - else if (mask & AA_MAY_APPEND) + else if (mask & TEST_MAY_APPEND) *b++ = 'a'; - if (mask & AA_MAY_EXEC) { - if (mask & AA_EXEC_UNSAFE) { - switch(mask & AA_EXEC_MODIFIERS) { - case AA_EXEC_UNCONFINED: + if (mask & TEST_MAY_EXEC) { + if (mask & TEST_EXEC_UNSAFE) { + switch(mask & TEST_EXEC_MODIFIERS) { + case TEST_EXEC_UNCONFINED: *b++ = 'u'; break; - case AA_EXEC_PROFILE: + case TEST_EXEC_PROFILE: *b++ = 'p'; break; - case AA_EXEC_LOCAL: + case TEST_EXEC_LOCAL: *b++ = 'c'; break; default: *b++ = 'y'; } } else { - switch(mask & AA_EXEC_MODIFIERS) { - case AA_EXEC_UNCONFINED: + switch(mask & TEST_EXEC_MODIFIERS) { + case TEST_EXEC_UNCONFINED: *b++ = 'U'; break; - case AA_EXEC_PROFILE: + case TEST_EXEC_PROFILE: *b++ = 'P'; break; - case AA_EXEC_LOCAL: + case TEST_EXEC_LOCAL: *b++ = 'C'; break; default: *b++ = 'Y'; } } - if (mask & AA_EXEC_INHERIT) + if (mask & TEST_EXEC_INHERIT) *b++ = 'i'; *b++ = 'x'; } - if (mask & AA_MAY_LINK) + if (mask & TEST_MAY_LINK) *b++ = 'l'; - if (mask & AA_MAY_LOCK) + if (mask & TEST_MAY_LOCK) *b++ = 'k'; *b++ = '\0'; } @@ -156,30 +157,30 @@ void build_filename(const char *name, int perm, char *buffer) } int is_valid_perm_set(int perm) { - if (AA_EXEC_TYPE & perm) { + if (TEST_EXEC_TYPE & perm) { /* exec mods need the perm bit set */ - if (!(perm & AA_MAY_EXEC)) + if (!(perm & TEST_MAY_EXEC)) return 0; /* unconfined can't inherit */ - if (((perm & AA_EXEC_MODIFIERS) == AA_EXEC_UNCONFINED) && - (perm & AA_EXEC_INHERIT)) + if (((perm & TEST_EXEC_MODIFIERS) == TEST_EXEC_UNCONFINED) && + (perm & TEST_EXEC_INHERIT)) return 0; /* no such thing as an unsafe ix */ - if ((perm & AA_EXEC_MODIFIERS) == 0 && (perm & AA_EXEC_INHERIT) && (perm & AA_EXEC_UNSAFE)) + if ((perm & TEST_EXEC_MODIFIERS) == 0 && (perm & TEST_EXEC_INHERIT) && (perm & TEST_EXEC_UNSAFE)) return 0; /* check exec_modifiers in range */ - if (!((perm & AA_EXEC_MODIFIERS) > 0 && (perm & AA_EXEC_MODIFIERS) < AA_EXEC_MOD_2)) + if (!((perm & TEST_EXEC_MODIFIERS) > 0 && (perm & TEST_EXEC_MODIFIERS) < TEST_EXEC_MOD_2)) return 0; } /* only 1 of append or write should be set */ - if ((perm & AA_MAY_WRITE) && (perm & AA_MAY_APPEND)) + if ((perm & TEST_MAY_WRITE) && (perm & TEST_MAY_APPEND)) return 0; /* not using mount yet, how should mount perms affect link? */ - if (perm & AA_MAY_MOUNT) + if (perm & TEST_MAY_MOUNT) return 0; return 1; diff --git a/tests/regression/apparmor/ptrace.c b/tests/regression/apparmor/ptrace.c index 797ff2f2f..b219270de 100644 --- a/tests/regression/apparmor/ptrace.c +++ b/tests/regression/apparmor/ptrace.c @@ -40,9 +40,17 @@ int interp_status(int status) # if defined(__x86_64__) || defined(__i386__) # define ARCH_REGS_STRUCT struct user_regs_struct # elif defined(__aarch64__) -# define ARCH_REGS_STRUCT struct user_pt_regs +# if (__GLIBC__ > 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ >= 20)) +# define ARCH_REGS_STRUCT struct user_regs_struct +# else +# define ARCH_REGS_STRUCT struct user_pt_regs +# endif # elif defined(__arm__) || defined(__powerpc__) || defined(__powerpc64__) # define ARCH_REGS_STRUCT struct pt_regs +# elif defined(__s390__) || defined(__s390x__) +# define ARCH_REGS_STRUCT struct _user_regs_struct +# else +# error "Need to define ARCH_REGS_STRUCT for this architecture" # endif int read_ptrace_registers(pid_t pid) diff --git a/tests/regression/apparmor/query_label.c b/tests/regression/apparmor/query_label.c index bf8dfe936..e84d7f2a0 100644 --- a/tests/regression/apparmor/query_label.c +++ b/tests/regression/apparmor/query_label.c @@ -35,28 +35,68 @@ #define AA_MAY_APPEND (1 << 3) #endif -#ifndef AA_MAY_LINK -#define AA_MAY_LINK (1 << 4) +#ifndef AA_MAY_CREATE +#define AA_MAY_CREATE (1 << 4) +#endif + +#ifndef AA_MAY_DELETE +#define AA_MAY_DELETE (1 << 5) +#endif + +#ifndef AA_MAY_OPEN +#define AA_MAY_OPEN (1 << 6) +#endif + +#ifndef AA_MAY_RENAME +#define AA_MAY_RENAME (1 << 7) +#endif + +#ifndef AA_MAY_SETATTR +#define AA_MAY_SETATTR (1 << 8) +#endif + +#ifndef AA_MAY_GETATTR +#define AA_MAY_GETATTR (1 << 9) +#endif + +#ifndef AA_MAY_SETCRED +#define AA_MAY_SETCRED (1 << 10) +#endif + +#ifndef AA_MAY_GETCRED +#define AA_MAY_GETCRED (1 << 11) +#endif + +#ifndef AA_MAY_CHMOD +#define AA_MAY_CHMOD (1 << 12) +#endif + +#ifndef AA_MAY_CHOWN +#define AA_MAY_CHOWN (1 << 13) #endif #ifndef AA_MAY_LOCK -#define AA_MAY_LOCK (1 << 5) +#define AA_MAY_LOCK 0x8000 #endif #ifndef AA_EXEC_MMAP -#define AA_EXEC_MMAP (1 << 6) +#define AA_EXEC_MMAP 0x10000 #endif -#ifndef AA_EXEC_PUX -#define AA_EXEC_PUX (1 << 7) +#ifndef AA_MAY_LINK +#define AA_MAY_LINK 0x40000 #endif -#ifndef AA_EXEC_UNSAFE -#define AA_EXEC_UNSAFE (1 << 8) +#ifndef AA_LINK_SUBSET /* overlayed perm in pair */ +#define AA_LINK_SUBSET AA_MAY_LOCK #endif -#ifndef AA_EXEC_INHERIT -#define AA_EXEC_INHERIT (1 << 9) +#ifndef AA_MAY_ONEXEC +#define AA_MAY_ONEXEC 0x20000000 +#endif + +#ifndef AA_MAY_CHANGE_PROFILE +#define AA_MAY_CHANGE_PROFILE 0x40000000 #endif static char *progname = NULL; @@ -148,18 +188,26 @@ static int parse_file_perms(uint32_t *mask, char *perms) *mask |= AA_MAY_READ; else if (!strcmp(perm, "append")) *mask |= AA_MAY_APPEND; + else if (!strcmp(perm, "create")) + *mask |= AA_MAY_CREATE; + else if (!strcmp(perm, "delete")) + *mask |= AA_MAY_DELETE; + else if (!strcmp(perm, "setattr")) + *mask |= AA_MAY_SETATTR; + else if (!strcmp(perm, "getattr")) + *mask |= AA_MAY_GETATTR; + else if (!strcmp(perm, "chmod")) + *mask |= AA_MAY_CHMOD; + else if (!strcmp(perm, "chown")) + *mask |= AA_MAY_CHOWN; else if (!strcmp(perm, "link")) *mask |= AA_MAY_LINK; else if (!strcmp(perm, "lock")) *mask |= AA_MAY_LOCK; + else if (!strcmp(perm, "linksubset")) + *mask |= AA_LINK_SUBSET; else if (!strcmp(perm, "exec_mmap")) *mask |= AA_EXEC_MMAP; - else if (!strcmp(perm, "exec_pux")) - *mask |= AA_EXEC_PUX; - else if (!strcmp(perm, "exec_unsafe")) - *mask |= AA_EXEC_UNSAFE; - else if (!strcmp(perm, "exec_inherit")) - *mask |= AA_EXEC_INHERIT; else { fprintf(stderr, "FAIL: unknown perm: %s\n", perm); return 1; @@ -264,8 +312,8 @@ int main(int argc, char **argv) (allowed == should_allow && audited == should_audit)) { printf("PASS\n"); } else { - fprintf(stderr, "FAIL: the access should %sbe allowed and should %sbe audited\n", - allowed ? "" : "not ", audited ? "" : "not "); + fprintf(stderr, "FAIL: the access should %sbe allowed and should %sbe audited. mask 0x%x\n", + allowed ? "" : "not ", audited ? "" : "not ", mask); exit(1); } diff --git a/tests/regression/apparmor/query_label.sh b/tests/regression/apparmor/query_label.sh index 01ec6d139..e9028f15f 100755 --- a/tests/regression/apparmor/query_label.sh +++ b/tests/regression/apparmor/query_label.sh @@ -212,9 +212,9 @@ querytest "QUERY dbus (svc receive)" fail $dbus_svc_query genqueryprofile "file," expect allow -perms file exec,write,read,append,link,lock -querytest "QUERY file (all base perms #1)" pass /anything -querytest "QUERY file (all base perms #2)" pass /everything +perms file exec,write,read,append,create,delete,setattr,getattr,chmod,chown,link,linksubset,lock,exec_mmap +querytest "QUERY file (all base perms #1)" xpass /anything +querytest "QUERY file (all base perms #2)" xpass /everything genqueryprofile "/etc/passwd r," expect allow diff --git a/tests/regression/apparmor/syscall_sysctl.c b/tests/regression/apparmor/syscall_sysctl.c index d3506be3d..9298e4f37 100644 --- a/tests/regression/apparmor/syscall_sysctl.c +++ b/tests/regression/apparmor/syscall_sysctl.c @@ -16,21 +16,44 @@ #include #define BUFSIZE 4096 -int main(int argc, char *argv[]) -{ - int save_max_threads, new_max_threads, read_new_max_threads; - size_t save_sz = sizeof(save_max_threads); - int name[] = {CTL_KERN, KERN_MAX_THREADS}; - int readonly = 0; - - if ((argc > 1) && strcmp(argv[1],"ro") == 0) - readonly = 1; - if (sysctl(name, sizeof(name), &save_max_threads, &save_sz, NULL, 0) == -1){ +static int name[] = {CTL_KERN, KERN_MAX_THREADS}; + +int read_max_threads(int *max_threads) +{ + size_t save_sz = sizeof(*max_threads); + + if (sysctl(name, sizeof(name), max_threads, &save_sz, NULL, 0) == -1){ fprintf(stderr, "FAIL: sysctl read failed - %s\n", strerror(errno)); return 1; } + return 0; +} + +int write_max_threads(int new_max_threads) +{ + size_t save_sz = sizeof(new_max_threads); + + if (sysctl(name, sizeof(name), NULL, 0, &new_max_threads, save_sz) == -1){ + fprintf(stderr, "FAIL: sysctl write failed - %s\n", + strerror(errno)); + return 1; + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + int save_max_threads, new_max_threads, read_new_max_threads; + int readonly = 0; + + if ((argc > 1) && strcmp(argv[1],"ro") == 0) + readonly = 1; + + if (read_max_threads(&save_max_threads) != 0) + return 1; /* printf("Kernel max threads (saved) is %d\n", save_max_threads); */ @@ -41,36 +64,39 @@ int main(int argc, char *argv[]) new_max_threads = save_max_threads + 1024; - if (sysctl(name, sizeof(name), NULL, 0, &new_max_threads, save_sz) == -1){ - fprintf(stderr, "FAIL: sysctl write failed - %s\n", - strerror(errno)); + if (write_max_threads(new_max_threads) != 0) return 1; - } - if (sysctl(name, sizeof(name), &read_new_max_threads, &save_sz, NULL, 0) == -1){ - fprintf(stderr, "FAIL: sysctl read failed - %s\n", - strerror(errno)); + if (read_max_threads(&read_new_max_threads) != 0) return 1; - } /* printf("Kernel max threads (new) is %d\n", read_new_max_threads); */ if (read_new_max_threads != new_max_threads) { - fprintf(stderr, "FAIL: read value does not match written values\n"); - return 1; + /* the kernel possibly rejected our updated max threads + * as being too large; try decreasing max threads. */ + + new_max_threads = save_max_threads - 1024; + + if (write_max_threads(new_max_threads) != 0) + return 1; + + if (read_max_threads(&read_new_max_threads) != 0) + return 1; + + /* printf("Kernel max threads (new, 2nd attempt) is %d\n", read_new_max_threads); */ + + if (read_new_max_threads != new_max_threads) { + fprintf(stderr, "FAIL: read value does not match written values\n"); + return 1; + } } - if (sysctl(name, sizeof(name), NULL, 0, &save_max_threads, save_sz) == -1){ - fprintf(stderr, "FAIL: sysctl write failed - %s\n", - strerror(errno)); + if (write_max_threads(save_max_threads) != 0) return 1; - } - if (sysctl(name, sizeof(name), &read_new_max_threads, &save_sz, NULL, 0) == -1){ - fprintf(stderr, "FAIL: sysctl read failed - %s\n", - strerror(errno)); + if (read_max_threads(&read_new_max_threads) != 0) return 1; - } /* printf("Kernel max threads (saved) is %d\n", read_new_max_threads);*/ diff --git a/tests/regression/apparmor/uservars.inc.source b/tests/regression/apparmor/uservars.inc.source index 7fbfdec12..198df439f 100644 --- a/tests/regression/apparmor/uservars.inc.source +++ b/tests/regression/apparmor/uservars.inc.source @@ -12,3 +12,6 @@ tmpdir=/tmp/sdtest.$$-$RANDOM # 4. Location of load system profiles for verification sys_profiles=/sys/kernel/security/apparmor/profiles + +# 5. Location of aa-exec +aa_exec=${PWD}/../../../binutils/aa-exec diff --git a/tests/regression/apparmor/uservars.inc.system b/tests/regression/apparmor/uservars.inc.system index d304ea709..c448a6b7f 100644 --- a/tests/regression/apparmor/uservars.inc.system +++ b/tests/regression/apparmor/uservars.inc.system @@ -12,3 +12,6 @@ tmpdir=/tmp/sdtest.$$-$RANDOM # 4. Location of load system profiles for verification sys_profiles=/sys/kernel/security/apparmor/profiles + +# 5. Location of aa-exec +aa_exec=$(which aa-exec) diff --git a/utils/Makefile b/utils/Makefile index 5ba5f8f44..acfddbaec 100644 --- a/utils/Makefile +++ b/utils/Makefile @@ -20,13 +20,13 @@ COMMONDIR=../common/ include $(COMMONDIR)/Make.rules -PERLTOOLS = aa-exec aa-notify +PERLTOOLS = aa-notify PYTOOLS = aa-easyprof aa-genprof aa-logprof aa-cleanprof aa-mergeprof \ aa-autodep aa-audit aa-complain aa-enforce aa-disable \ aa-status aa-unconfined TOOLS = ${PERLTOOLS} ${PYTOOLS} aa-decode PYSETUP = python-tools-setup.py -PYMODULES = $(wildcard apparmor/*.py) +PYMODULES = $(wildcard apparmor/*.py apparmor/rule/*.py) MANPAGES = ${TOOLS:=.8} logprof.conf.5 diff --git a/utils/aa-autodep b/utils/aa-autodep index deb5be01c..331dc5348 100755 --- a/utils/aa-autodep +++ b/utils/aa-autodep @@ -28,6 +28,7 @@ parser = argparse.ArgumentParser(description=_('Generate a basic AppArmor profil parser.add_argument('--force', action='store_true', default=False, help=_('overwrite existing profile')) parser.add_argument('-d', '--dir', type=str, help=_('path to profiles')) parser.add_argument('program', type=str, nargs='+', help=_('name of program')) +parser.add_argument('--no-reload', dest='do_reload', action='store_false', default=True, help=_('Do not reload the profile after modifying it')) args = parser.parse_args() tool = apparmor.tools.aa_tools('autodep', args) diff --git a/utils/aa-easyprof b/utils/aa-easyprof index 93861aefb..88d1f92f9 100755 --- a/utils/aa-easyprof +++ b/utils/aa-easyprof @@ -10,7 +10,7 @@ # ------------------------------------------------------------------ import apparmor.easyprof -from apparmor.easyprof import AppArmorException, error +from apparmor.easyprof import error import os import sys @@ -61,12 +61,7 @@ if __name__ == "__main__": for (binary, options) in profiles: if len(profiles) > 1: count += 1 - try: - easyp = apparmor.easyprof.AppArmorEasyProfile(binary, options) - except AppArmorException as e: - error(e.value) - except Exception: - raise + easyp = apparmor.easyprof.AppArmorEasyProfile(binary, options) if options.list_templates: apparmor.easyprof.print_basefilenames(easyp.get_templates()) @@ -118,7 +113,4 @@ if __name__ == "__main__": sys.stdout.write('%s\n' % easyp.gen_manifest(params)) else: params['no_verify'] = options.no_verify - try: - easyp.output_policy(params, count, opt.output_directory) - except AppArmorException as e: - error(e) + easyp.output_policy(params, count, opt.output_directory) diff --git a/utils/aa-exec b/utils/aa-exec deleted file mode 100755 index 23bd3aca3..000000000 --- a/utils/aa-exec +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/perl -# ------------------------------------------------------------------ -# -# Copyright (C) 2011-2013 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. -# -# ------------------------------------------------------------------ - -use strict; -use warnings; -use Errno; - -require LibAppArmor; -require POSIX; - -my $opt_d = ''; -my $opt_h = ''; -my $opt_p = ''; -my $opt_n = ''; -my $opt_i = ''; -my $opt_v = ''; -my $opt_f = ''; - -sub _warn { - my $msg = $_[0]; - print STDERR "aa-exec: WARN: $msg\n"; -} -sub _error { - my $msg = $_[0]; - print STDERR "aa-exec: ERROR: $msg\n"; - exit 1 -} - -sub _debug { - $opt_d or return; - my $msg = $_[0]; - print STDERR "aa-exec: DEBUG: $msg\n"; -} - -sub _verbose { - $opt_v or return; - my $msg = $_[0]; - print STDERR "$msg\n"; -} - -sub usage() { - my $s = <<'EOF'; -USAGE: aa-exec [OPTIONS] - -Confine with the specified PROFILE. - -OPTIONS: - -p PROFILE, --profile=PROFILE PROFILE to confine with - -n NAMESPACE, --namespace=NAMESPACE NAMESPACE to confine in - -f FILE, --file FILE profile file to load - -i, --immediate change profile immediately instead of at exec - -v, --verbose show messages with stats - -h, --help display this help - -EOF - print $s; -} - -use Getopt::Long; - -GetOptions( - 'debug|d' => \$opt_d, - 'help|h' => \$opt_h, - 'profile|p=s' => \$opt_p, - 'namespace|n=s' => \$opt_n, - 'file|f=s' => \$opt_f, - 'immediate|i' => \$opt_i, - 'verbose|v' => \$opt_v, -); - -if ($opt_h) { - usage(); - exit(0); -} - -if ($opt_n || $opt_p) { - my $test; - my $prof; - - if ($opt_n) { - $prof = ":$opt_n:"; - } - - $prof .= $opt_p; - - if ($opt_f) { - system("apparmor_parser", "-r", "$opt_f") == 0 - or _error("\'aborting could not load $opt_f\'"); - } - - if ($opt_i) { - _verbose("aa_change_profile(\"$prof\")"); - $test = LibAppArmor::aa_change_profile($prof); - _debug("$test = aa_change_profile(\"$prof\"); $!"); - } else { - _verbose("aa_change_onexec(\"$prof\")"); - $test = LibAppArmor::aa_change_onexec($prof); - _debug("$test = aa_change_onexec(\"$prof\"); $!"); - } - - if ($test != 0) { - if ($!{ENOENT} || $!{EACCESS}) { - my $pre = ($opt_p) ? "profile" : "namespace"; - _error("$pre \'$prof\' does not exist\n"); - } elsif ($!{EINVAL}) { - _error("AppArmor interface not available\n"); - } else { - _error("$!\n"); - } - } -} - -_verbose("exec @ARGV"); -exec @ARGV; diff --git a/utils/aa-genprof b/utils/aa-genprof index 522908793..5a89b17bc 100755 --- a/utils/aa-genprof +++ b/utils/aa-genprof @@ -136,7 +136,7 @@ while not done_profiling: logmark = subprocess.check_output(['date | md5sum'], shell=True) logmark = logmark.decode('ascii').strip() logmark = re.search('^([0-9a-f]+)', logmark).groups()[0] - t=subprocess.call("%s -p kern.warn 'GenProf: %s'"%(apparmor.logger, logmark), shell=True) + t=subprocess.call("%s -p kern.warn 'GenProf: %s'"%(apparmor.logger_path(), logmark), shell=True) else: logmark = last_audit_entry_time() diff --git a/utils/aa-logprof.pod b/utils/aa-logprof.pod index c7dbb87a8..621bd38fc 100644 --- a/utils/aa-logprof.pod +++ b/utils/aa-logprof.pod @@ -128,7 +128,7 @@ updated profiles to the disk and reload them if AppArmor is running. If there are unhandled x accesses generated by the execve(2) of a new process, aa-logprof will display the parent profile and the target -program that's being executed and prompt the user to select and execute +program that's being executed and prompt the user to select an execute modifier. These modifiers will allow a choice for the target to: have it's own profile (px), inherit the parent's profile (ix), run unconstrained (ux), or deny access for the target. See apparmor.d(5) for details. diff --git a/utils/aa-mergeprof b/utils/aa-mergeprof index a3f5b5dd1..e1be53cb2 100755 --- a/utils/aa-mergeprof +++ b/utils/aa-mergeprof @@ -1,6 +1,7 @@ #! /usr/bin/env python # ---------------------------------------------------------------------- # Copyright (C) 2013 Kshitij Gupta +# Copyright (C) 2014-2015 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 @@ -631,7 +632,7 @@ class Merge(object): elif re.search('\d', ans): default_option = ans - for ruletype in ['capability', 'network', 'change_profile']: + for ruletype in apparmor.aa.ruletypes: if other.aa[profile][hat].get(ruletype, False): # needed until we have proper profile initialization for rule_obj in other.aa[profile][hat][ruletype].rules: diff --git a/utils/apparmor/aa.py b/utils/apparmor/aa.py index 92a717424..9df9ef869 100644 --- a/utils/apparmor/aa.py +++ b/utils/apparmor/aa.py @@ -47,7 +47,7 @@ from apparmor.regex import (RE_PROFILE_START, RE_PROFILE_END, RE_PROFILE_LINK, RE_PROFILE_BARE_FILE_ENTRY, RE_PROFILE_PATH_ENTRY, RE_PROFILE_CHANGE_HAT, RE_PROFILE_HAT_DEF, RE_PROFILE_DBUS, RE_PROFILE_MOUNT, - RE_PROFILE_SIGNAL, RE_PROFILE_PTRACE, RE_PROFILE_PIVOT_ROOT, + RE_PROFILE_PIVOT_ROOT, RE_PROFILE_UNIX, RE_RULE_HAS_COMMA, RE_HAS_COMMENT_SPLIT, strip_quotes, parse_profile_start_line, re_match_include ) @@ -56,9 +56,13 @@ import apparmor.rules as aarules from apparmor.rule.capability import CapabilityRuleset, CapabilityRule from apparmor.rule.change_profile import ChangeProfileRuleset, ChangeProfileRule from apparmor.rule.network import NetworkRuleset, NetworkRule +from apparmor.rule.ptrace import PtraceRuleset, PtraceRule from apparmor.rule.rlimit import RlimitRuleset, RlimitRule +from apparmor.rule.signal import SignalRuleset, SignalRule from apparmor.rule import parse_modifiers, quote_if_needed +ruletypes = ['capability', 'change_profile', 'network', 'ptrace', 'rlimit', 'signal'] + from apparmor.yasti import SendDataToYast, GetDataFromYast, shutdown_yast # setup module translations @@ -82,8 +86,6 @@ cfg = None repo_cfg = None parser = None -ldd = None -logger = None profile_dir = None extra_profile_dir = None ### end our @@ -150,8 +152,8 @@ def fatal_error(message): # Get the traceback to the message tb_stack = traceback.format_list(traceback.extract_stack()) tb_stack = ''.join(tb_stack) - # Append the traceback to message - message = message + '\n' + tb_stack + # Add the traceback to message + message = tb_stack + '\n\n' + message debug_logger.error(message) caller = inspect.stack()[1][3] @@ -257,8 +259,8 @@ def name_to_prof_filename(prof_filename): prof_filename = get_profile_filename(bin_path) if os.path.isfile(prof_filename): return (prof_filename, bin_path) - else: - return None, None + + return None, None def complain(path): """Sets the profile to complain mode if it exists""" @@ -360,6 +362,11 @@ def get_reqs(file): pattern1 = re.compile('^\s*\S+ => (\/\S+)') pattern2 = re.compile('^\s*(\/\S+)') reqs = [] + + ldd = conf.find_first_file(cfg['settings'].get('ldd')) or '/usr/bin/ldd' + if not os.path.isfile(ldd) or not os.access(ldd, os.EX_OK): + raise AppArmorException('Can\'t find ldd') + ret, ldd_out = get_output([ldd, file]) if ret == 0: for line in ldd_out: @@ -397,17 +404,55 @@ def handle_binfmt(profile, path): profile['allow']['path'][library]['mode'] = profile['allow']['path'][library].get('mode', set()) | str_to_mode('mr') profile['allow']['path'][library]['audit'] |= profile['allow']['path'][library].get('audit', set()) +def get_interpreter_and_abstraction(exec_target): + '''Check if exec_target is a script. + If a hashbang is found, check if we have an abstraction for it. + + Returns (interpreter_path, abstraction) + - interpreter_path is none if exec_target is not a script or doesn't have a hashbang line + - abstraction is None if no matching abstraction exists''' + + if not os.path.exists(exec_target): + aaui.UI_Important(_('Execute target %s does not exist!') % exec_target) + return None, None + + if not os.path.isfile(exec_target): + aaui.UI_Important(_('Execute target %s is not a file!') % exec_target) + return None, None + + hashbang = head(exec_target) + if not hashbang.startswith('#!'): + return None, None + + # get the interpreter (without parameters) + interpreter = hashbang[2:].strip().split()[0] + interpreter_path = get_full_path(interpreter) + interpreter = re.sub('^(/usr)?/bin/', '', interpreter_path) + + if interpreter in ['bash', 'dash', 'sh']: + abstraction = 'abstractions/bash' + elif interpreter == 'perl': + abstraction = 'abstractions/perl' + elif re.search('^python([23]|[23]\.[0-9]+)?$', interpreter): + abstraction = 'abstractions/python' + elif interpreter == 'ruby': + abstraction = 'abstractions/ruby' + else: + abstraction = None + + return interpreter_path, abstraction + def get_inactive_profile(local_profile): if extras.get(local_profile, False): return {local_profile: extras[local_profile]} return dict() -def profile_storage(): +def profile_storage(profilename, hat, calledby): # keys used in aa[profile][hat]: # a) rules (as dict): alias, include, lvar # b) rules (as hasher): allow, deny # c) one for each rule class - # d) other: external, flags, name, profile, attachment, initial_comment, + # d) other: external, flags, name, profile, attachment, initial_comment, filename, info, # profile_keyword, header_comment (these two are currently only set by set_profile_flags()) # Note that this function doesn't explicitely init all those keys (yet). @@ -415,32 +460,33 @@ def profile_storage(): profile = hasher() + # profile['info'] isn't used anywhere, but can be helpful in debugging. + profile['info'] = {'profile': profilename, 'hat': hat, 'calledby': calledby} + profile['capability'] = CapabilityRuleset() profile['change_profile'] = ChangeProfileRuleset() profile['network'] = NetworkRuleset() + profile['ptrace'] = PtraceRuleset() profile['rlimit'] = RlimitRuleset() + profile['signal'] = SignalRuleset() profile['allow']['path'] = hasher() profile['allow']['dbus'] = list() profile['allow']['mount'] = list() - profile['allow']['signal'] = list() - profile['allow']['ptrace'] = list() profile['allow']['pivot_root'] = list() return profile def create_new_profile(localfile, is_stub=False): - local_profile = profile_storage() + local_profile = hasher() + local_profile[localfile] = profile_storage('NEW', localfile, 'create_new_profile()') local_profile[localfile]['flags'] = 'complain' local_profile[localfile]['include']['abstractions/base'] = 1 if os.path.exists(localfile) and os.path.isfile(localfile): - hashbang = head(localfile) - if hashbang.startswith('#!'): - interpreter_path = get_full_path(hashbang.lstrip('#!').strip()) - - interpreter = re.sub('^(/usr)?/bin/', '', interpreter_path) + interpreter_path, abstraction = get_interpreter_and_abstraction(localfile) + if interpreter_path: local_profile[localfile]['allow']['path'][localfile]['mode'] = local_profile[localfile]['allow']['path'][localfile].get('mode', str_to_mode('r')) | str_to_mode('r') local_profile[localfile]['allow']['path'][localfile]['audit'] = local_profile[localfile]['allow']['path'][localfile].get('audit', set()) @@ -449,14 +495,9 @@ def create_new_profile(localfile, is_stub=False): local_profile[localfile]['allow']['path'][interpreter_path]['audit'] = local_profile[localfile]['allow']['path'][interpreter_path].get('audit', set()) - if interpreter == 'perl': - local_profile[localfile]['include']['abstractions/perl'] = True - elif re.search('^python([23]|[23]\.[0-9]+)?$', interpreter): - local_profile[localfile]['include']['abstractions/python'] = True - elif interpreter == 'ruby': - local_profile[localfile]['include']['abstractions/ruby'] = True - elif interpreter in ['bash', 'dash', 'sh']: - local_profile[localfile]['include']['abstractions/bash'] = True + if abstraction: + local_profile[localfile]['include'][abstraction] = True + handle_binfmt(local_profile[localfile], interpreter_path) else: @@ -469,6 +510,8 @@ def create_new_profile(localfile, is_stub=False): for hatglob in cfg['required_hats'].keys(): if re.search(hatglob, localfile): for hat in sorted(cfg['required_hats'][hatglob].split()): + if not local_profile.get(hat, False): + local_profile[hat] = profile_storage('NEW', hat, 'create_new_profile() required_hats') local_profile[hat]['flags'] = 'complain' if not is_stub: @@ -1085,6 +1128,26 @@ def handle_children(profile, hat, root): continue prelog[aamode][profile][hat]['capability'][capability] = True + elif typ == 'ptrace': + # If ptrace then we (should) have pid, profile, hat, program, mode, access and peer + pid, p, h, prog, aamode, access, peer = entry + if not regex_nullcomplain.search(p) and not regex_nullcomplain.search(h): + profile = p + hat = h + if not profile or not hat: + continue + prelog[aamode][profile][hat]['ptrace'][peer][access] = True + + elif typ == 'signal': + # If signal then we (should) have pid, profile, hat, program, mode, access, signal and peer + pid, p, h, prog, aamode, access, signal, peer = entry + if not regex_nullcomplain.search(p) and not regex_nullcomplain.search(h): + profile = p + hat = h + if not profile or not hat: + continue + prelog[aamode][profile][hat]['signal'][peer][access][signal] = True + elif typ == 'path' or typ == 'exec': # If path or exec then we (should) have pid, profile, hat, program, mode, details and to_name pid, p, h, prog, aamode, mode, detail, to_name = entry[:8] @@ -1107,6 +1170,7 @@ def handle_children(profile, hat, root): detail = detail.replace('*', '\*') detail = detail.replace('{', '\{') detail = detail.replace('}', '\}') + detail = detail.replace('!', '\!') # Give Execute dialog if x access requested for something that's not a directory # For directories force an 'ix' Path dialog @@ -1120,25 +1184,7 @@ def handle_children(profile, hat, root): else: do_execute = True - if mode & apparmor.aamode.AA_MAY_LINK: - regex_link = re.compile('^from (.+) to (.+)$') - match = regex_link.search(detail) - if match: - path = match.groups()[0] - target = match.groups()[1] - - frommode = str_to_mode('lr') - if prelog[aamode][profile][hat]['path'].get(path, False): - frommode |= prelog[aamode][profile][hat]['path'][path] - prelog[aamode][profile][hat]['path'][path] = frommode - - tomode = str_to_mode('lr') - if prelog[aamode][profile][hat]['path'].get(target, False): - tomode |= prelog[aamode][profile][hat]['path'][target] - prelog[aamode][profile][hat]['path'][target] = tomode - else: - continue - elif mode: + if mode: path = detail if prelog[aamode][profile][hat]['path'].get(path, False): @@ -1403,24 +1449,15 @@ def handle_children(profile, hat, root): changed[profile] = True if exec_mode & str_to_mode('i'): - #if 'perl' in exec_target: - # aa[profile][hat]['include']['abstractions/perl'] = True - #elif '/bin/bash' in exec_target or '/bin/sh' in exec_target: - # aa[profile][hat]['include']['abstractions/bash'] = True - hashbang = head(exec_target) - if hashbang.startswith('#!'): - interpreter = hashbang[2:].strip() - interpreter_path = get_full_path(interpreter) - interpreter = re.sub('^(/usr)?/bin/', '', interpreter_path) + interpreter_path, abstraction = get_interpreter_and_abstraction(exec_target) + if interpreter_path: aa[profile][hat]['allow']['path'][interpreter_path]['mode'] = aa[profile][hat]['allow']['path'][interpreter_path].get('mode', str_to_mode('ix')) | str_to_mode('ix') aa[profile][hat]['allow']['path'][interpreter_path]['audit'] = aa[profile][hat]['allow']['path'][interpreter_path].get('audit', set()) - if interpreter == 'perl': - aa[profile][hat]['include']['abstractions/perl'] = True - elif interpreter in ['bash', 'dash', 'sh']: - aa[profile][hat]['include']['abstractions/bash'] = True + if abstraction: + aa[profile][hat]['include'][abstraction] = True # Update tracking info based on kind of change @@ -1463,7 +1500,8 @@ def handle_children(profile, hat, root): ynans = aaui.UI_YesNo(_('A profile for %s does not exist.\nDo you want to create one?') % exec_target, 'n') if ynans == 'y': hat = exec_target - # XXX do we need to init the profile here? + if not aa[profile].get(hat, False): + aa[profile][hat] = profile_storage(profile, hat, 'handle_children()') aa[profile][hat]['profile'] = True if profile != hat: @@ -1584,7 +1622,7 @@ def ask_the_questions(): hats = [profile] + hats for hat in hats: - log_obj[profile][hat] = profile_storage() + log_obj[profile][hat] = profile_storage(profile, hat, 'ask_the_questions()') for capability in sorted(log_dict[aamode][profile][hat]['capability'].keys()): capability_obj = CapabilityRule(capability, log_event=aamode) @@ -1595,7 +1633,19 @@ def ask_the_questions(): network_obj = NetworkRule(family, sock_type, log_event=aamode) log_obj[profile][hat]['network'].add(network_obj) - for ruletype in ['capability', 'network']: + + for peer in sorted(log_dict[aamode][profile][hat]['ptrace'].keys()): + for access in sorted(log_dict[aamode][profile][hat]['ptrace'][peer].keys()): + ptrace_obj = PtraceRule(access, peer, log_event=aamode) + log_obj[profile][hat]['ptrace'].add(ptrace_obj) + + for peer in sorted(log_dict[aamode][profile][hat]['signal'].keys()): + for access in sorted(log_dict[aamode][profile][hat]['signal'][peer].keys()): + for signal in sorted(log_dict[aamode][profile][hat]['signal'][peer][access].keys()): + signal_obj = SignalRule(access, signal, peer, log_event=aamode) + log_obj[profile][hat]['signal'].add(signal_obj) + + for ruletype in ruletypes: # XXX aa-mergeprof also has this code - if you change it, keep aa-mergeprof in sync! for rule_obj in log_obj[profile][hat][ruletype].rules: @@ -2079,8 +2129,6 @@ def delete_duplicates(profile, incname): # Allow rules covered by denied rules shouldn't be deleted # only a subset allow rules may actually be denied - ruletypes = ['capability', 'change_profile', 'network', 'rlimit'] - if include.get(incname, False): for rule_type in ruletypes: deleted += profile[rule_type].delete_duplicates(include[incname][incname][rule_type]) @@ -2413,16 +2461,30 @@ def collapse_log(): for cap in prelog[aamode][profile][hat]['capability'].keys(): # If capability not already in profile # XXX remove first check when we have proper profile initialisation - if aa[profile][hat].get('capability', False) and not aa[profile][hat]['capability'].is_covered(CapabilityRule(cap)): + if aa[profile][hat].get('capability', False) and not aa[profile][hat]['capability'].is_covered(CapabilityRule(cap, log_event=True)): log_dict[aamode][profile][hat]['capability'][cap] = True nd = prelog[aamode][profile][hat]['netdomain'] for family in nd.keys(): for sock_type in nd[family].keys(): - if not is_known_rule(aa[profile][hat], 'network', NetworkRule(family, sock_type)): + if not is_known_rule(aa[profile][hat], 'network', NetworkRule(family, sock_type, log_event=True)): log_dict[aamode][profile][hat]['netdomain'][family][sock_type] = True -PROFILE_MODE_RE = re.compile('^(r|w|l|m|k|a|ix|ux|px|pux|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix)+$') + ptrace = prelog[aamode][profile][hat]['ptrace'] + for peer in ptrace.keys(): + for access in ptrace[peer].keys(): + if not is_known_rule(aa[profile][hat], 'ptrace', PtraceRule(access, peer, log_event=True)): + log_dict[aamode][profile][hat]['ptrace'][peer][access] = True + + sig = prelog[aamode][profile][hat]['signal'] + for peer in sig.keys(): + for access in sig[peer].keys(): + for signal in sig[peer][access].keys(): + if not is_known_rule(aa[profile][hat], 'signal', SignalRule(access, signal, peer, log_event=True)): + log_dict[aamode][profile][hat]['signal'][peer][access][signal] = True + + +PROFILE_MODE_RE = re.compile('^(r|w|l|m|k|a|ix|ux|px|pux|cx|pix|cix|cux|Ux|Px|PUx|Cx|Pix|Cix|CUx)+$') PROFILE_MODE_DENY_RE = re.compile('^(r|w|l|m|k|a|x)+$') def validate_profile_mode(mode, allow, nt_name=None): @@ -2461,6 +2523,12 @@ def is_skippable_dir(path): return False def read_profiles(): + # we'll read all profiles from disk, so reset the storage first (autodep() might have created/stored + # a profile already, which would cause a 'Conflicting profile' error in attach_profile_data()) + global aa, original_aa + aa = hasher() + original_aa = hasher() + try: os.listdir(profile_dir) except: @@ -2510,6 +2578,12 @@ def attach_profile_data(profiles, profile_data): # Make deep copy of data to avoid changes to # arising due to mutables for p in profile_data.keys(): + if profiles.get(p, False): + for hat in profile_data[p].keys(): + if profiles[p].get(hat, False): + raise AppArmorException(_("Conflicting profiles for %s defined in two files:\n- %s\n- %s") % + (combine_name(p, hat), profiles[p][hat]['filename'], profile_data[p][hat]['filename'])) + profiles[p] = deepcopy(profile_data[p]) @@ -2560,7 +2634,8 @@ def parse_profile_data(data, file, do_include): if do_include: profile = file hat = file - profile_data[profile][hat] = profile_storage() + profile_data[profile][hat] = profile_storage(profile, hat, 'parse_profile_data() do_include') + profile_data[profile][hat]['filename'] = file for lineno, line in enumerate(data): line = line.strip() @@ -2578,7 +2653,7 @@ def parse_profile_data(data, file, do_include): raise AppArmorException('Profile %(profile)s defined twice in %(file)s, last found in line %(line)s' % { 'file': file, 'line': lineno + 1, 'profile': combine_name(profile, hat) }) - profile_data[profile][hat] = profile_storage() + profile_data[profile][hat] = profile_storage(profile, hat, 'parse_profile_data() profile_start') if attachment: profile_data[profile][hat]['attachment'] = attachment @@ -2698,7 +2773,7 @@ def parse_profile_data(data, file, do_include): list_var = strip_quotes(matches[0]) var_operation = matches[1] - value = strip_quotes(matches[2]) + value = matches[2] if profile: if not profile_data[profile][hat].get('lvar', False): @@ -2727,8 +2802,12 @@ def parse_profile_data(data, file, do_include): if not profile: raise AppArmorException(_('Syntax Error: Unexpected bare file rule found in file: %(file)s line: %(line)s') % { 'file': file, 'line': lineno + 1 }) - audit, allow, allow_keyword, comment = parse_modifiers(matches) + audit, deny, allow_keyword, comment = parse_modifiers(matches) # TODO: honor allow_keyword and comment + if deny: + allow = 'deny' + else: + allow = 'allow' mode = apparmor.aamode.AA_BARE_FILE_MODE if not matches.group('owner'): @@ -2873,49 +2952,17 @@ def parse_profile_data(data, file, do_include): mount_rules.append(mount_rule) profile_data[profile][hat][allow]['mount'] = mount_rules - elif RE_PROFILE_SIGNAL.search(line): - matches = RE_PROFILE_SIGNAL.search(line).groups() - + elif SignalRule.match(line): if not profile: raise AppArmorException(_('Syntax Error: Unexpected signal entry found in file: %(file)s line: %(line)s') % { 'file': file, 'line': lineno + 1 }) - audit = False - if matches[0]: - audit = True - allow = 'allow' - if matches[1] and matches[1].strip() == 'deny': - allow = 'deny' - signal = matches[2].strip() - - signal_rule = parse_signal_rule(signal) - signal_rule.audit = audit - signal_rule.deny = (allow == 'deny') - - signal_rules = profile_data[profile][hat][allow].get('signal', list()) - signal_rules.append(signal_rule) - profile_data[profile][hat][allow]['signal'] = signal_rules - - elif RE_PROFILE_PTRACE.search(line): - matches = RE_PROFILE_PTRACE.search(line).groups() + profile_data[profile][hat]['signal'].add(SignalRule.parse(line)) + elif PtraceRule.match(line): if not profile: raise AppArmorException(_('Syntax Error: Unexpected ptrace entry found in file: %(file)s line: %(line)s') % { 'file': file, 'line': lineno + 1 }) - audit = False - if matches[0]: - audit = True - allow = 'allow' - if matches[1] and matches[1].strip() == 'deny': - allow = 'deny' - ptrace = matches[2].strip() - - ptrace_rule = parse_ptrace_rule(ptrace) - ptrace_rule.audit = audit - ptrace_rule.deny = (allow == 'deny') - - ptrace_rules = profile_data[profile][hat][allow].get('ptrace', list()) - ptrace_rules.append(ptrace_rule) - profile_data[profile][hat][allow]['ptrace'] = ptrace_rules + profile_data[profile][hat]['ptrace'].add(PtraceRule.parse(line)) elif RE_PROFILE_PIVOT_ROOT.search(line): matches = RE_PROFILE_PIVOT_ROOT.search(line).groups() @@ -2983,7 +3030,8 @@ def parse_profile_data(data, file, do_include): # if hat is already known, the filelist check some lines below will error out. # nevertheless, just to be sure, don't overwrite existing profile_data. if not profile_data[profile].get(hat, False): - profile_data[profile][hat] = profile_storage() + profile_data[profile][hat] = profile_storage(profile, hat, 'parse_profile_data() hat_def') + profile_data[profile][hat]['filename'] = file flags = matches.group('flags') @@ -2992,7 +3040,7 @@ def parse_profile_data(data, file, do_include): if initial_comment: profile_data[profile][hat]['initial_comment'] = initial_comment initial_comment = '' - if filelist[file]['profiles'][profile].get(hat, False): + if filelist[file]['profiles'][profile].get(hat, False) and not do_include: raise AppArmorException(_('Error: Multiple definitions for hat %(hat)s in profile %(profile)s.') % { 'hat': hat, 'profile': profile }) filelist[file]['profiles'][profile][hat] = True @@ -3032,7 +3080,7 @@ def parse_profile_data(data, file, do_include): if re.search(hatglob, parsed_prof): for hat in cfg['required_hats'][hatglob].split(): if not profile_data[parsed_prof].get(hat, False): - profile_data[parsed_prof][hat] = profile_storage() + profile_data[parsed_prof][hat] = profile_storage(parsed_prof, hat, 'parse_profile_data() required_hats') # End of file reached but we're stuck in a profile if profile and not do_include: @@ -3059,14 +3107,6 @@ def parse_mount_rule(line): # XXX Do real parsing here return aarules.Raw_Mount_Rule(line) -def parse_signal_rule(line): - # XXX Do real parsing here - return aarules.Raw_Signal_Rule(line) - -def parse_ptrace_rule(line): - # XXX Do real parsing here - return aarules.Raw_Ptrace_Rule(line) - def parse_pivot_root_rule(line): # XXX Do real parsing here return aarules.Raw_Pivot_Root_Rule(line) @@ -3078,12 +3118,16 @@ def parse_unix_rule(line): def separate_vars(vs): """Returns a list of all the values for a variable""" data = set() + vs = vs.strip() - RE_VARS = re.compile('\s*((\".+?\")|([^\"]\S+))\s*(.*)$') + RE_VARS = re.compile('^(("[^"]*")|([^"\s]+))\s*(.*)$') while RE_VARS.search(vs): matches = RE_VARS.search(vs).groups() data.add(strip_quotes(matches[0])) - vs = matches[3] + vs = matches[3].strip() + + if vs: + raise AppArmorException('Variable assignments contains invalid parts (unbalanced quotes?): %s' % vs) return data @@ -3211,6 +3255,8 @@ def write_rlimits(prof_data, depth): def var_transform(ref): data = [] for value in ref: + if not value: + value = '""' data.append(quote_if_needed(value)) return ' '.join(data) @@ -3265,40 +3311,16 @@ def write_mount(prof_data, depth): data += write_mount_rules(prof_data, depth, 'allow') return data -def write_signal_rules(prof_data, depth, allow): - pre = ' ' * depth - data = [] - - # no signal rules, so return - if not prof_data[allow].get('signal', False): - return data - - for signal_rule in prof_data[allow]['signal']: - data.append('%s%s' % (pre, signal_rule.serialize())) - data.append('') - return data - def write_signal(prof_data, depth): - data = write_signal_rules(prof_data, depth, 'deny') - data += write_signal_rules(prof_data, depth, 'allow') - return data - -def write_ptrace_rules(prof_data, depth, allow): - pre = ' ' * depth data = [] - - # no ptrace rules, so return - if not prof_data[allow].get('ptrace', False): - return data - - for ptrace_rule in prof_data[allow]['ptrace']: - data.append('%s%s' % (pre, ptrace_rule.serialize())) - data.append('') + if prof_data.get('signal', False): + data = prof_data['signal'].get_clean(depth) return data def write_ptrace(prof_data, depth): - data = write_ptrace_rules(prof_data, depth, 'deny') - data += write_ptrace_rules(prof_data, depth, 'allow') + data = [] + if prof_data.get('ptrace', False): + data = prof_data['ptrace'].get_clean(depth) return data def write_pivot_root_rules(prof_data, depth, allow): @@ -3319,6 +3341,24 @@ def write_pivot_root(prof_data, depth): data += write_pivot_root_rules(prof_data, depth, 'allow') return data +def write_unix_rules(prof_data, depth, allow): + pre = ' ' * depth + data = [] + + # no unix rules, so return + if not prof_data[allow].get('unix', False): + return data + + for unix_rule in prof_data[allow]['unix']: + data.append('%s%s' % (pre, unix_rule.serialize())) + data.append('') + return data + +def write_unix(prof_data, depth): + data = write_unix_rules(prof_data, depth, 'deny') + data += write_unix_rules(prof_data, depth, 'allow') + return data + def write_link_rules(prof_data, depth, allow): pre = ' ' * depth data = [] @@ -3430,6 +3470,7 @@ def write_rules(prof_data, depth): data += write_signal(prof_data, depth) data += write_ptrace(prof_data, depth) data += write_pivot_root(prof_data, depth) + data += write_unix(prof_data, depth) data += write_links(prof_data, depth) data += write_paths(prof_data, depth) data += write_change_profile(prof_data, depth) @@ -3586,6 +3627,7 @@ def serialize_profile_from_old_profile(profile_data, name, options): 'signal': write_signal, 'ptrace': write_ptrace, 'pivot_root': write_pivot_root, + 'unix': write_unix, 'link': write_links, 'path': write_paths, 'change_profile': write_change_profile, @@ -3601,6 +3643,7 @@ def serialize_profile_from_old_profile(profile_data, name, options): 'signal', 'ptrace', 'pivot_root', + 'unix', 'link', 'path', 'change_profile', @@ -3617,6 +3660,7 @@ def serialize_profile_from_old_profile(profile_data, name, options): 'signal': True, # not handled otherwise yet 'ptrace': True, # not handled otherwise yet 'pivot_root': True, # not handled otherwise yet + 'unix': True, # not handled otherwise yet 'link': False, 'path': False, 'change_profile': False, @@ -4071,10 +4115,23 @@ def is_known_rule(profile, rule_type, rule_obj): if profile[rule_type].is_covered(rule_obj, False): return True - for incname in profile['include'].keys(): - if include[incname][incname].get(rule_type, False): - if include[incname][incname][rule_type].is_covered(rule_obj, False): - return True + includelist = list(profile['include'].keys()) + checked = [] + + while includelist: + incname = includelist.pop(0) + checked.append(incname) + + if os.path.isdir(profile_dir + '/' + incname): + includelist += include_dir_filelist(profile_dir, incname) + else: + if include[incname][incname].get(rule_type, False): + if include[incname][incname][rule_type].is_covered(rule_obj, False): + return True + + for childinc in include[incname][incname]['include'].keys(): + if childinc not in checked: + includelist += [childinc] return False @@ -4120,23 +4177,17 @@ def include_dir_filelist(profile_dir, include_name): def load_include(incname): load_includeslist = [incname] - if include.get(incname, {}).get(incname, False): - return 0 while load_includeslist: incfile = load_includeslist.pop(0) - if os.path.isfile(profile_dir + '/' + incfile): + if include.get(incfile, {}).get(incfile, False): + pass # already read, do nothing + elif os.path.isfile(profile_dir + '/' + incfile): data = get_include_data(incfile) incdata = parse_profile_data(data, incfile, True) - #print(incdata) - if not incdata: - # If include is empty, simply push in a placeholder for it - # because other profiles may mention them - incdata = hasher() - incdata[incname] = hasher() attach_profile_data(include, incdata) #If the include is a directory means include all subfiles elif os.path.isdir(profile_dir + '/' + incfile): - load_includeslist += list(map(lambda x: incfile + '/' + x, os.listdir(profile_dir + '/' + incfile))) + load_includeslist += include_dir_filelist(profile_dir, incfile) else: raise AppArmorException("Include file %s not found" % (profile_dir + '/' + incfile) ) @@ -4320,30 +4371,32 @@ def matchregexp(new, old): return None +def logger_path(): + logger = conf.find_first_file(cfg['settings']['logger']) or '/bin/logger' + if not os.path.isfile(logger) or not os.access(logger, os.EX_OK): + raise AppArmorException("Can't find logger!\nPlease make sure %s exists, or update the 'logger' path in logprof.conf." % logger) + return logger + ######Initialisations###### conf = apparmor.config.Config('ini', CONFDIR) cfg = conf.read_config('logprof.conf') -#print(cfg['settings']) -#if 'default_owner_prompt' in cfg['settings']: +# prevent various failures if logprof.conf doesn't exist +if not cfg.sections(): + cfg.add_section('settings') + cfg.add_section('required_hats') + if cfg['settings'].get('default_owner_prompt', False): cfg['settings']['default_owner_prompt'] = '' -profile_dir = conf.find_first_dir(cfg['settings']['profiledir']) or '/etc/apparmor.d' +profile_dir = conf.find_first_dir(cfg['settings'].get('profiledir')) or '/etc/apparmor.d' if not os.path.isdir(profile_dir): raise AppArmorException('Can\'t find AppArmor profiles') -extra_profile_dir = conf.find_first_dir(cfg['settings']['inactive_profiledir']) or '/etc/apparmor/profiles/extras/' +extra_profile_dir = conf.find_first_dir(cfg['settings'].get('inactive_profiledir')) or '/usr/share/apparmor/extra-profiles/' -parser = conf.find_first_file(cfg['settings']['parser']) or '/sbin/apparmor_parser' +parser = conf.find_first_file(cfg['settings'].get('parser')) or '/sbin/apparmor_parser' if not os.path.isfile(parser) or not os.access(parser, os.EX_OK): raise AppArmorException('Can\'t find apparmor_parser') -ldd = conf.find_first_file(cfg['settings']['ldd']) or '/usr/bin/ldd' -if not os.path.isfile(ldd) or not os.access(ldd, os.EX_OK): - raise AppArmorException('Can\'t find ldd') - -logger = conf.find_first_file(cfg['settings']['logger']) or '/bin/logger' -if not os.path.isfile(logger) or not os.access(logger, os.EX_OK): - raise AppArmorException('Can\'t find logger') diff --git a/utils/apparmor/aare.py b/utils/apparmor/aare.py new file mode 100644 index 000000000..3f410b45e --- /dev/null +++ b/utils/apparmor/aare.py @@ -0,0 +1,90 @@ +# ---------------------------------------------------------------------- +# Copyright (C) 2015 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 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 re + +from apparmor.common import convert_regexp, type_is_str, AppArmorBug, AppArmorException + +class AARE(object): + '''AARE (AppArmor Regular Expression) wrapper class''' + + def __init__(self, regex, is_path, log_event=None): + '''create an AARE instance for the given AppArmor regex + If is_path is true, the regex is expected to be a path and therefore must start with / or a variable.''' + # using the specified variables when matching. + + if is_path: + if regex.startswith('/'): + pass + elif regex.startswith('@{'): + pass # XXX ideally check variable content - each part must start with / - or another variable, which must start with / + else: + raise AppArmorException("Path doesn't start with / or variable: %s" % regex) + + if log_event: + self.orig_regex = regex + self.regex = convert_expression_to_aare(regex) + else: + self.orig_regex = None + self.regex = regex + + self._regex_compiled = None # done on first use in match() - that saves us some re.compile() calls + # self.variables = variables # XXX + + def __repr__(self): + '''returns a "printable" representation of AARE''' + return "AARE('%s')" % self.regex + + def __deepcopy__(self, memo): + # thanks to http://bugs.python.org/issue10076, we need to implement this ourself + if self.orig_regex: + return AARE(self.orig_regex, is_path=False, log_event=True) + else: + return AARE(self.regex, is_path=False) + + def match(self, expression): + '''check if the given expression (string or AARE) matches the regex''' + + if type(expression) == AARE: + if expression.orig_regex: + expression = expression.orig_regex + else: + return self.is_equal(expression) # better safe than sorry + elif not type_is_str(expression): + raise AppArmorBug('AARE.match() called with unknown object: %s' % str(expression)) + + if self._regex_compiled is None: + self._regex_compiled = re.compile(convert_regexp(self.regex)) + + return bool(self._regex_compiled.match(expression)) + + def is_equal(self, expression): + '''check if the given expression is equal''' + + if type(expression) == AARE: + return self.regex == expression.regex + elif type_is_str(expression): + return self.regex == expression + else: + raise AppArmorBug('AARE.is_equal() called with unknown object: %s' % str(expression)) + + +def convert_expression_to_aare(expression): + '''convert an expression (taken from audit.log) to an AARE string''' + + aare_escape_chars = ['\\', '?', '*', '[', ']', '{', '}', '"', '!'] + for char in aare_escape_chars: + expression = expression.replace(char, '\\' + char) + + return expression diff --git a/utils/apparmor/cleanprofile.py b/utils/apparmor/cleanprofile.py index 71abc7b6a..126b7f873 100644 --- a/utils/apparmor/cleanprofile.py +++ b/utils/apparmor/cleanprofile.py @@ -1,5 +1,6 @@ # ---------------------------------------------------------------------- # Copyright (C) 2013 Kshitij Gupta +# Copyright (C) 2014-2015 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 @@ -64,12 +65,11 @@ class CleanProf(object): deleted += apparmor.delete_duplicates(self.other.aa[program][hat], inc) #Clean duplicate rules in other profile - if not self.same_file: - deleted += self.other.aa[program][hat]['capability'].delete_duplicates(self.profile.aa[program][hat]['capability']) - deleted += self.other.aa[program][hat]['network'].delete_duplicates(self.profile.aa[program][hat]['network']) - else: - deleted += self.other.aa[program][hat]['capability'].delete_duplicates(None) - deleted += self.other.aa[program][hat]['network'].delete_duplicates(None) + for ruletype in apparmor.ruletypes: + if not self.same_file: + deleted += self.other.aa[program][hat][ruletype].delete_duplicates(self.profile.aa[program][hat][ruletype]) + else: + deleted += self.other.aa[program][hat][ruletype].delete_duplicates(None) #Clean the duplicates of path in other profile deleted += delete_path_duplicates(self.profile.aa[program][hat], self.other.aa[program][hat], 'allow', self.same_file) diff --git a/utils/apparmor/common.py b/utils/apparmor/common.py index de112e56d..fbba89edf 100644 --- a/utils/apparmor/common.py +++ b/utils/apparmor/common.py @@ -245,6 +245,15 @@ def user_perm(prof_dir): return False return True +def type_is_str(var): + ''' returns True if the given variable is a str (or unicode string when using python 2)''' + if type(var) == str: + return True + elif sys.version_info[0] < 3 and type(var) == unicode: # python 2 sometimes uses the 'unicode' type + return True + else: + return False + class DebugLogger(object): def __init__(self, module_name=__name__): self.debugging = False diff --git a/utils/apparmor/config.py b/utils/apparmor/config.py index 5e613bc97..64334c9b7 100644 --- a/utils/apparmor/config.py +++ b/utils/apparmor/config.py @@ -114,10 +114,11 @@ class Config(object): def find_first_file(self, file_list): """Returns name of first matching file None otherwise""" filename = None - for f in file_list.split(): - if os.path.isfile(f): - filename = f - break + if file_list: + for f in file_list.split(): + if os.path.isfile(f): + filename = f + break return filename def find_first_dir(self, dir_list): diff --git a/utils/apparmor/fail.py b/utils/apparmor/fail.py index 19e4491d6..5e5cb50ec 100644 --- a/utils/apparmor/fail.py +++ b/utils/apparmor/fail.py @@ -8,12 +8,16 @@ # # ------------------------------------------------------------------ +from __future__ import print_function # needed in py2 for print('...', file=sys.stderr) + import cgitb import os import sys import tempfile import traceback +from apparmor.common import error + # # Exception handling # @@ -27,8 +31,8 @@ def handle_exception(*exc_info): (ex_cls, ex, tb) = exc_info if ex_cls.__name__ == 'AppArmorException': # I didn't find a way to get this working with isinstance() :-/ - print('') - print(ex.value) + print('', file=sys.stderr) + error(ex.value) else: (fd, path) = tempfile.mkstemp(prefix='apparmor-bugreport-', suffix='.txt') file = os.fdopen(fd, 'w') @@ -40,13 +44,13 @@ def handle_exception(*exc_info): file.write('Please consider reporting a bug at https://bugs.launchpad.net/apparmor/\n') file.write('and attach this file.\n') - print(''.join(traceback.format_exception(*exc_info))) - print('') - print('An unexpected error occoured!') - print('') - print('For details, see %s' % path) - print('Please consider reporting a bug at https://bugs.launchpad.net/apparmor/') - print('and attach this file.') + print(''.join(traceback.format_exception(*exc_info)), file=sys.stderr) + print('', file=sys.stderr) + print('An unexpected error occoured!', file=sys.stderr) + print('', file=sys.stderr) + print('For details, see %s' % path, file=sys.stderr) + print('Please consider reporting a bug at https://bugs.launchpad.net/apparmor/', file=sys.stderr) + print('and attach this file.', file=sys.stderr) def enable_aa_exception_handler(): '''Setup handle_exception() as exception handler''' diff --git a/utils/apparmor/logparser.py b/utils/apparmor/logparser.py index 40d4ad967..3e3f8e1f2 100644 --- a/utils/apparmor/logparser.py +++ b/utils/apparmor/logparser.py @@ -17,7 +17,7 @@ import re import sys import time import LibAppArmor -from apparmor.common import AppArmorException, open_file_read, DebugLogger +from apparmor.common import AppArmorException, AppArmorBug, open_file_read, DebugLogger from apparmor.aamode import validate_log_mode, log_str_to_mode, hide_log_mode, AA_MAY_EXEC @@ -26,8 +26,24 @@ from apparmor.translations import init_translation _ = init_translation() class ReadLog: - RE_LOG_v2_6_syslog = re.compile('kernel:\s+(\[[\d\.\s]+\]\s+)?(audit:\s+)?type=\d+\s+audit\([\d\.\:]+\):\s+apparmor=') - RE_LOG_v2_6_audit = re.compile('type=AVC\s+(msg=)?audit\([\d\.\:]+\):\s+apparmor=') + RE_audit_time_id = '(msg=)?audit\([\d\.\:]+\):\s+' # 'audit(1282626827.320:411): ' + RE_kernel_time = '\[[\d\.\s]+\]' # '[ 1612.746129]' + RE_type_num = '1[45][0-9][0-9]' # 1400..1599 + RE_aa_or_op = '(apparmor=|operation=)' + + RE_log_parts = [ + 'kernel:\s+(' + RE_kernel_time + '\s+)?(audit:\s+)?type=' + RE_type_num + '\s+' + RE_audit_time_id + RE_aa_or_op, # v2_6 syslog + 'kernel:\s+(' + RE_kernel_time + '\s+)?' + RE_audit_time_id + 'type=' + RE_type_num + '\s+' + RE_aa_or_op, + 'type=(AVC|APPARMOR[_A-Z]*|' + RE_type_num + ')\s+' + RE_audit_time_id + '(type=' + RE_type_num + '\s+)?' + RE_aa_or_op, # v2_6 audit and dmesg + 'type=USER_AVC\s+' + RE_audit_time_id + '.*apparmor=', # dbus + 'type=UNKNOWN\[' + RE_type_num + '\]\s+' + RE_audit_time_id + RE_aa_or_op, + 'dbus\[[0-9]+\]:\s+apparmor=', # dbus + ] + + # used to pre-filter log lines so that we hand over only relevant lines to LibAppArmor parsing + RE_LOG_ALL = re.compile('(' + '|'.join(RE_log_parts) + ')') + + # Used by netdomain to identify the operation types # New socket names OPERATION_TYPES = {'create': 'net', @@ -42,6 +58,7 @@ class ReadLog: 'getpeername': 'net', 'getsockopt': 'net', 'setsockopt': 'net', + 'socket_create': 'net', 'sock_shutdown': 'net' } @@ -61,7 +78,7 @@ class ReadLog: if self.next_log_entry: sys.stderr.out('A log entry already present: %s' % self.next_log_entry) self.next_log_entry = self.LOG.readline() - while not self.RE_LOG_v2_6_syslog.search(self.next_log_entry) and not self.RE_LOG_v2_6_audit.search(self.next_log_entry) and not (self.logmark and self.logmark in self.next_log_entry): + while not self.RE_LOG_ALL.search(self.next_log_entry) and not (self.logmark and self.logmark in self.next_log_entry): self.next_log_entry = self.LOG.readline() if not self.next_log_entry: break @@ -120,6 +137,12 @@ class ReadLog: ev['family'] = event.net_family ev['protocol'] = event.net_protocol ev['sock_type'] = event.net_sock_type + elif ev['operation'] and ev['operation'] == 'signal': + ev['signal'] = event.signal + ev['peer'] = event.peer + elif ev['operation'] and ev['operation'] == 'ptrace': + ev['peer'] = event.peer + LibAppArmor.free_record(event) if not ev['time']: @@ -180,23 +203,35 @@ class ReadLog: #print("log",self.log) def add_event_to_tree(self, e): - aamode = e.get('aamode', 'UNKNOWN') - if e.get('type', False): - if re.search('(UNKNOWN\[1501\]|APPARMOR_AUDIT|1501)', e['type']): + e = self.parse_event_for_tree(e) + if e is not None: + (pid, parent, mode, details) = e + self.add_to_tree(pid, parent, mode, details) + + def map_log_type(self, log_type): + if re.search('(UNKNOWN\[1501\]|APPARMOR_AUDIT|1501)', log_type): aamode = 'AUDIT' - elif re.search('(UNKNOWN\[1502\]|APPARMOR_ALLOWED|1502)', e['type']): + elif re.search('(UNKNOWN\[1502\]|APPARMOR_ALLOWED|1502)', log_type): aamode = 'PERMITTING' - elif re.search('(UNKNOWN\[1503\]|APPARMOR_DENIED|1503)', e['type']): + elif re.search('(UNKNOWN\[1503\]|APPARMOR_DENIED|1503)', log_type): aamode = 'REJECTING' - elif re.search('(UNKNOWN\[1504\]|APPARMOR_HINT|1504)', e['type']): + elif re.search('(UNKNOWN\[1504\]|APPARMOR_HINT|1504)', log_type): aamode = 'HINT' - elif re.search('(UNKNOWN\[1505\]|APPARMOR_STATUS|1505)', e['type']): + elif re.search('(UNKNOWN\[1505\]|APPARMOR_STATUS|1505)', log_type): aamode = 'STATUS' - elif re.search('(UNKNOWN\[1506\]|APPARMOR_ERROR|1506)', e['type']): + elif re.search('(UNKNOWN\[1506\]|APPARMOR_ERROR|1506)', log_type): aamode = 'ERROR' else: aamode = 'UNKNOWN' + return aamode + + def parse_event_for_tree(self, e): + aamode = e.get('aamode', 'UNKNOWN') + + if e.get('type', False): + aamode = self.map_log_type(e['type']) + if aamode in ['UNKNOWN', 'AUDIT', 'STATUS', 'ERROR']: return None @@ -221,10 +256,10 @@ class ReadLog: if e['operation'] == 'change_hat': if aamode != 'HINT' and aamode != 'PERMITTING': return None - profile = e['name'] + profile = e['name2'] #hat = None - if '//' in e['name']: - profile, hat = e['name'].split('//')[:2] + if '//' in e['name2']: + profile, hat = e['name2'].split('//')[:2] if not hat: hat = profile @@ -240,28 +275,35 @@ class ReadLog: e['request_mask'], e['name2'] = log_str_to_mode(e['profile'], e['request_mask'], e['name2']) if e.get('info', False) and e['info'] == 'mandatory profile missing': - self.add_to_tree(e['pid'], e['parent'], 'exec', + return(e['pid'], e['parent'], 'exec', [profile, hat, aamode, 'PERMITTING', e['denied_mask'], e['name'], e['name2']]) elif (e.get('name2', False) and '//null-' in e['name2']) or e.get('name', False): - self.add_to_tree(e['pid'], e['parent'], 'exec', + return(e['pid'], e['parent'], 'exec', [profile, hat, prog, aamode, e['denied_mask'], e['name'], '']) else: - self.debug_logger.debug('add_event_to_tree: dropped exec event in %s' % e['profile']) + self.debug_logger.debug('parse_event_for_tree: dropped exec event in %s' % e['profile']) elif ( e['operation'].startswith('file_') or e['operation'].startswith('inode_') or e['operation'] in ['open', 'truncate', 'mkdir', 'mknod', 'chmod', 'rename_src', 'rename_dest', 'unlink', 'rmdir', 'symlink_create', 'link', 'sysctl', 'getattr', 'setattr', 'xattr'] ): - # Map c (create) to a and d (delete) to w (logging is more detailed than the profile language) + # for some reason, we get file_perm and file_inherit log events without request_mask, see + # https://bugs.launchpad.net/apparmor/+bug/1466812/ and https://bugs.launchpad.net/apparmor/+bug/1509030 + # request_mask can also be '', see https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/1525119 + if e['operation'] in ['file_perm', 'file_inherit'] and not e['request_mask']: + self.debug_logger.debug('UNHANDLED (missing request_mask): %s' % e) + return None + + # Map c (create) and d (delete) to w (logging is more detailed than the profile language) rmask = e['request_mask'] - rmask = rmask.replace('c', 'a') + rmask = rmask.replace('c', 'w') rmask = rmask.replace('d', 'w') if not validate_log_mode(hide_log_mode(rmask)): raise AppArmorException(_('Log contains unknown mode %s') % rmask) dmask = e['denied_mask'] - dmask = dmask.replace('c', 'a') + dmask = dmask.replace('c', 'w') dmask = dmask.replace('d', 'w') if not validate_log_mode(hide_log_mode(dmask)): raise AppArmorException(_('Log contains unknown mode %s') % dmask) @@ -281,14 +323,14 @@ class ReadLog: self.throw_away_next_log_entry() if is_domain_change: - self.add_to_tree(e['pid'], e['parent'], 'exec', + return(e['pid'], e['parent'], 'exec', [profile, hat, prog, aamode, e['denied_mask'], e['name'], e['name2']]) else: - self.add_to_tree(e['pid'], e['parent'], 'path', + return(e['pid'], e['parent'], 'path', [profile, hat, prog, aamode, e['denied_mask'], e['name'], '']) elif e['operation'] == 'capable': - self.add_to_tree(e['pid'], e['parent'], 'capability', + return(e['pid'], e['parent'], 'capability', [profile, hat, prog, aamode, e['name'], '']) elif e['operation'] == 'clone': @@ -312,11 +354,17 @@ class ReadLog: # self.pid[child] = arrayref elif self.op_type(e['operation']) == 'net': - self.add_to_tree(e['pid'], e['parent'], 'netdomain', + return(e['pid'], e['parent'], 'netdomain', [profile, hat, prog, aamode, e['family'], e['sock_type'], e['protocol']]) elif e['operation'] == 'change_hat': - self.add_to_tree(e['pid'], e['parent'], 'unknown_hat', + return(e['pid'], e['parent'], 'unknown_hat', [profile, hat, aamode, hat]) + elif e['operation'] == 'ptrace': + return(e['pid'], e['parent'], 'ptrace', + [profile, hat, prog, aamode, e['denied_mask'], e['peer']]) + elif e['operation'] == 'signal': + return(e['pid'], e['parent'], 'signal', + [profile, hat, prog, aamode, e['denied_mask'], e['signal'], e['peer']]) else: self.debug_logger.debug('UNHANDLED: %s' % e) @@ -350,7 +398,14 @@ class ReadLog: event = self.parse_log_record(line) #print(event) if event: - self.add_event_to_tree(event) + try: + self.add_event_to_tree(event) + except AppArmorException as e: + ex_msg = ('%(msg)s\n\nThis error was caused by the log line:\n%(logline)s' % + {'msg': e.value, 'logline': line}) + # when py3 only: Drop the original AppArmorException by passing None as the parent exception + raise AppArmorBug(ex_msg) # py3-only: from None + self.LOG.close() self.logmark = '' return self.log diff --git a/utils/apparmor/regex.py b/utils/apparmor/regex.py index f038a5a0e..01f7cc905 100644 --- a/utils/apparmor/regex.py +++ b/utils/apparmor/regex.py @@ -27,13 +27,15 @@ RE_EOL = '\s*(?P#.*?)?\s*$' # optional whitespace, op RE_COMMA_EOL = '\s*,' + RE_EOL # optional whitespace, comma + RE_EOL RE_PROFILE_NAME = '(?P<%s>(\S+|"[^"]+"))' # string without spaces, or quoted string. %s is the match group name -RE_PROFILE_PATH = '(?P<%s>(/\S+|"/[^"]+"))' # filename (starting with '/') without spaces, or quoted filename. %s is the match group name +RE_PATH = '/\S+|"/[^"]+"' # filename (starting with '/') without spaces, or quoted filename. +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 + '|@{\S+}\S*|"@{\S+}[^"]*"))' # quoted or unquoted filename or variable. %s is the match group name RE_PROFILE_END = re.compile('^\s*\}' + RE_EOL) RE_PROFILE_CAP = re.compile(RE_AUDIT_DENY + 'capability(?P(\s+\S+)+)?' + RE_COMMA_EOL) RE_PROFILE_LINK = re.compile(RE_AUDIT_DENY + 'link\s+(((subset)|(<=))\s+)?([\"\@\/].*?"??)\s+->\s*([\"\@\/].*?"??)' + RE_COMMA_EOL) RE_PROFILE_ALIAS = re.compile('^\s*alias\s+("??.+?"??)\s+->\s*("??.+?"??)' + RE_COMMA_EOL) -RE_PROFILE_RLIMIT = re.compile('^\s*set\s+rlimit\s+(?P[a-z]+)\s*<=\s*(?P[^ ]+)' + RE_COMMA_EOL) +RE_PROFILE_RLIMIT = re.compile('^\s*set\s+rlimit\s+(?P[a-z]+)\s*<=\s*(?P[^ ]+(\s+[a-zA-Z]+)?)' + RE_COMMA_EOL) RE_PROFILE_BOOLEAN = re.compile('^\s*(\$\{?\w*\}?)\s*=\s*(true|false)\s*,?' + RE_EOL, flags=re.IGNORECASE) RE_PROFILE_VARIABLE = re.compile('^\s*(@\{?\w+\}?)\s*(\+?=)\s*(@*.+?)\s*,?' + RE_EOL) RE_PROFILE_CONDITIONAL = re.compile('^\s*if\s+(not\s+)?(\$\{?\w*\}?)\s*\{' + RE_EOL) @@ -46,8 +48,8 @@ RE_PROFILE_CHANGE_HAT = re.compile('^\s*\^(\"??.+?\"??)' + RE_COMMA_EOL) RE_PROFILE_HAT_DEF = re.compile('^(?P\s*)(?P\^|hat\s+)(?P\"??.+?\"??)\s+((flags=)?\((?P.+)\)\s+)*\{' + RE_EOL) RE_PROFILE_DBUS = re.compile(RE_AUDIT_DENY + '(dbus\s*,|dbus\s+[^#]*\s*,)' + RE_EOL) RE_PROFILE_MOUNT = re.compile(RE_AUDIT_DENY + '((mount|remount|umount|unmount)(\s+[^#]*)?\s*,)' + RE_EOL) -RE_PROFILE_SIGNAL = re.compile(RE_AUDIT_DENY + '(signal\s*,|signal\s+[^#]*\s*,)' + RE_EOL) -RE_PROFILE_PTRACE = re.compile(RE_AUDIT_DENY + '(ptrace\s*,|ptrace\s+[^#]*\s*,)' + RE_EOL) +RE_PROFILE_SIGNAL = re.compile(RE_AUDIT_DENY + '(signal\s*,|signal(?P
\s+[^#]*)\s*,)' + RE_EOL) +RE_PROFILE_PTRACE = re.compile(RE_AUDIT_DENY + '(ptrace\s*,|ptrace(?P
\s+[^#]*)\s*,)' + RE_EOL) RE_PROFILE_PIVOT_ROOT = re.compile(RE_AUDIT_DENY + '(pivot_root\s*,|pivot_root\s+[^#]*\s*,)' + RE_EOL) RE_PROFILE_UNIX = re.compile(RE_AUDIT_DENY + '(unix\s*,|unix\s+[^#]*\s*,)' + RE_EOL) @@ -64,9 +66,9 @@ RE_HAS_COMMENT_SPLIT = re.compile('^(?P' + __re_no_or_quoted_hash + RE_PROFILE_START = re.compile( '^(?P\s*)' + '(' + - RE_PROFILE_PATH % 'plainprofile' + # just a path + RE_PROFILE_PATH_OR_VAR % 'plainprofile' + # just a path '|' + # or - '(' + 'profile' + '\s+' + RE_PROFILE_NAME % 'namedprofile' + '(\s+' + RE_PROFILE_PATH % 'attachment' + ')?' + ')' + # 'profile', profile name, optionally attachment + '(' + 'profile' + '\s+' + RE_PROFILE_NAME % 'namedprofile' + '(\s+' + RE_PROFILE_PATH_OR_VAR % 'attachment' + ')?' + ')' + # 'profile', profile name, optionally attachment ')' + '\s+((flags\s*=\s*)?\((?P.+)\)\s*)?\{' + RE_EOL) @@ -75,7 +77,7 @@ RE_PROFILE_START = re.compile( RE_PROFILE_CHANGE_PROFILE = re.compile( RE_AUDIT_DENY + 'change_profile' + - '(\s+' + RE_PROFILE_PATH % 'execcond' + ')?' + # optionally exec condition + '(\s+' + RE_PROFILE_PATH_OR_VAR % 'execcond' + ')?' + # optionally exec condition '(\s+->\s*' + RE_PROFILE_NAME % 'targetprofile' + ')?' + # optionally '->' target profile RE_COMMA_EOL) diff --git a/utils/apparmor/rule/__init__.py b/utils/apparmor/rule/__init__.py index 3150600dd..b3634e581 100644 --- a/utils/apparmor/rule/__init__.py +++ b/utils/apparmor/rule/__init__.py @@ -13,7 +13,8 @@ # # ---------------------------------------------------------------------- -from apparmor.common import AppArmorBug +from apparmor.aare import AARE +from apparmor.common import AppArmorBug, type_is_str # setup module translations from apparmor.translations import init_translation @@ -50,6 +51,38 @@ class BaseRule(object): # Set only in the parse() class method self.raw_rule = None + def _aare_or_all(self, rulepart, partname, is_path, log_event): + '''checks rulepart and returns + - (AARE, False) if rulepart is a (non-empty) string + - (None, True) if rulepart is all_obj (typically *Rule.ALL) + - raises AppArmorBug if rulepart is an empty string or has a wrong type + + Parameters: + - rulepart: the rule part to check (string or *Rule.ALL object) + - partname: the name of the rulepart (for example 'peer', used for exception messages) + - is_path (passed through to AARE) + - log_event (passed through to AARE) + ''' + + if rulepart == self.ALL: + return None, True + elif type_is_str(rulepart): + if len(rulepart.strip()) == 0: + raise AppArmorBug('Passed empty %(partname)s to %(classname)s: %(rulepart)s' % + {'partname': partname, 'classname': self.__class__.__name__, 'rulepart': str(rulepart)}) + return AARE(rulepart, is_path=is_path, log_event=log_event), False + else: + raise AppArmorBug('Passed unknown %(partname)s to %(classname)s: %(rulepart)s' + % {'partname': partname, 'classname': self.__class__.__name__, 'rulepart': str(rulepart)}) + + def __repr__(self): + classname = self.__class__.__name__ + try: + raw_content = self.get_raw() # will fail for BaseRule + return '<%s> %s' % (classname, raw_content) + except NotImplementedError: + return '<%s (NotImplementedError - get_clean() not implemented?)>' % classname + @classmethod def match(cls, raw_rule): '''return True if raw_rule matches the class (main) regex, False otherwise @@ -65,7 +98,7 @@ class BaseRule(object): @classmethod def _match(cls, raw_rule): '''parse raw_rule and return regex match object''' - raise AppArmorBug("'%s' needs to implement _match(), but didn't" % (str(cls))) + raise NotImplementedError("'%s' needs to implement _match(), but didn't" % (str(cls))) @classmethod def parse(cls, raw_rule): @@ -79,7 +112,12 @@ class BaseRule(object): def _parse(cls, raw_rule): '''returns a Rule object created from parsing the raw rule. required to be implemented by subclasses; raise exception if not''' - raise AppArmorBug("'%s' needs to implement _parse(), but didn't" % (str(cls))) + raise NotImplementedError("'%s' needs to implement _parse(), but didn't" % (str(cls))) + + # @abstractmethod FIXME - uncomment when python3 only + def get_clean(self, depth=0): + '''return clean rule (with default formatting, and leading whitespace as specified in the depth parameter)''' + raise NotImplementedError("'%s' needs to implement get_clean(), but didn't" % (str(self.__class__))) def get_raw(self, depth=0): '''return raw rule (with original formatting, and leading whitespace in the depth parameter)''' @@ -112,7 +150,7 @@ class BaseRule(object): # @abstractmethod FIXME - uncomment when python3 only def is_covered_localvars(self, other_rule): '''check if the rule-specific parts of other_rule is covered by this rule object''' - raise AppArmorBug("'%s' needs to implement is_covered_localvars(), but didn't" % (str(self))) + raise NotImplementedError("'%s' needs to implement is_covered_localvars(), but didn't" % (str(self))) def is_equal(self, rule_obj, strict=False): '''compare if rule_obj == self @@ -133,7 +171,7 @@ class BaseRule(object): # @abstractmethod FIXME - uncomment when python3 only def is_equal_localvars(self, other_rule): '''compare if rule-specific variables are equal''' - raise AppArmorBug("'%s' needs to implement is_equal_localvars(), but didn't" % (str(self))) + raise NotImplementedError("'%s' needs to implement is_equal_localvars(), but didn't" % (str(self))) def severity(self, sev_db): '''return severity of this rule, which can be: @@ -169,7 +207,7 @@ class BaseRule(object): def logprof_header_localvars(self): '''return the headers (human-readable version of the rule) to display in aa-logprof for this rule object returns {'label1': 'value1', 'label2': 'value2'} ''' - raise AppArmorBug("'%s' needs to implement logprof_header(), but didn't" % (str(self))) + raise NotImplementedError("'%s' needs to implement logprof_header(), but didn't" % (str(self))) def modifiers_str(self): '''return the allow/deny and audit keyword as string, including whitespace''' @@ -206,6 +244,10 @@ class BaseRuleset(object): '''called by __init__() and delete_all_rules() - override in child class to initialize more variables''' pass + def __repr__(self): + classname = self.__class__.__name__ + return '<%s>\n' % classname + '\n'.join(self.get_raw(1)) + '' % classname + def add(self, rule): '''add a rule object''' self.rules.append(rule) @@ -323,9 +365,32 @@ class BaseRuleset(object): def get_glob_ext(self, path_or_rule): '''returns the next possible glob with extension (for file rules only). For all other rule types, raise an exception''' - raise AppArmorBug("get_glob_ext is not available for this rule type!") + raise NotImplementedError("get_glob_ext is not available for this rule type!") +def check_and_split_list(lst, allowed_keywords, all_obj, classname, keyword_name): + '''check if lst is all_obj or contains only items listed in allowed_keywords''' + + if lst == all_obj: + return None, True, None + elif type_is_str(lst): + result_list = {lst} + elif (type(lst) == list or type(lst) == tuple) and len(lst) > 0: + result_list = set(lst) + else: + raise AppArmorBug('Passed unknown %(type)s object to %(classname)s: %(unknown_object)s' % + {'type': type(lst), 'classname': classname, 'unknown_object': str(lst)}) + + unknown_items = set() + for item in result_list: + if not item.strip(): + raise AppArmorBug('Passed empty %(keyword_name)s to %(classname)s' % + {'keyword_name': keyword_name, 'classname': classname}) + if item not in allowed_keywords: + unknown_items.add(item) + + return result_list, False, unknown_items + def parse_comment(matches): '''returns the comment (with a leading space) from the matches object''' comment = '' diff --git a/utils/apparmor/rule/capability.py b/utils/apparmor/rule/capability.py index 6439ac001..0f9a4fe4a 100644 --- a/utils/apparmor/rule/capability.py +++ b/utils/apparmor/rule/capability.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # ---------------------------------------------------------------------- # Copyright (C) 2013 Kshitij Gupta # Copyright (C) 2014 Christian Boltz @@ -15,7 +14,7 @@ # ---------------------------------------------------------------------- from apparmor.regex import RE_PROFILE_CAP -from apparmor.common import AppArmorBug, AppArmorException +from apparmor.common import AppArmorBug, AppArmorException, type_is_str from apparmor.rule import BaseRule, BaseRuleset, parse_modifiers import re @@ -48,7 +47,7 @@ class CapabilityRule(BaseRule): self.all_caps = True self.capability = set() else: - if type(cap_list) == str: + if type_is_str(cap_list): self.capability = {cap_list} elif type(cap_list) == list and len(cap_list) > 0: self.capability = set(cap_list) diff --git a/utils/apparmor/rule/change_profile.py b/utils/apparmor/rule/change_profile.py index 8fd146b5f..e5173c7d4 100644 --- a/utils/apparmor/rule/change_profile.py +++ b/utils/apparmor/rule/change_profile.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # ---------------------------------------------------------------------- # Copyright (C) 2013 Kshitij Gupta # Copyright (C) 2015 Christian Boltz @@ -15,7 +14,7 @@ # ---------------------------------------------------------------------- from apparmor.regex import RE_PROFILE_CHANGE_PROFILE, strip_quotes -from apparmor.common import AppArmorBug, AppArmorException +from apparmor.common import AppArmorBug, AppArmorException, type_is_str from apparmor.rule import BaseRule, BaseRuleset, parse_modifiers, quote_if_needed # setup module translations @@ -49,7 +48,7 @@ class ChangeProfileRule(BaseRule): self.all_execconds = False if execcond == ChangeProfileRule.ALL: self.all_execconds = True - elif type(execcond) == str: + elif type_is_str(execcond): if not execcond.strip(): raise AppArmorBug('Empty exec condition in change_profile rule') elif execcond.startswith('/') or execcond.startswith('@'): @@ -63,7 +62,7 @@ class ChangeProfileRule(BaseRule): self.all_targetprofiles = False if targetprofile == ChangeProfileRule.ALL: self.all_targetprofiles = True - elif type(targetprofile) == str: + elif type_is_str(targetprofile): if targetprofile.strip(): self.targetprofile = targetprofile else: diff --git a/utils/apparmor/rule/network.py b/utils/apparmor/rule/network.py index 9ce90dce3..7c7053f32 100644 --- a/utils/apparmor/rule/network.py +++ b/utils/apparmor/rule/network.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # ---------------------------------------------------------------------- # Copyright (C) 2013 Kshitij Gupta # Copyright (C) 2015 Christian Boltz @@ -17,7 +16,7 @@ import re from apparmor.regex import RE_PROFILE_NETWORK -from apparmor.common import AppArmorBug, AppArmorException +from apparmor.common import AppArmorBug, AppArmorException, type_is_str from apparmor.rule import BaseRule, BaseRuleset, parse_modifiers # setup module translations @@ -28,7 +27,7 @@ _ = init_translation() network_domain_keywords = [ 'unix', 'inet', 'ax25', 'ipx', 'appletalk', 'netrom', 'bridge', 'atmpvc', 'x25', 'inet6', 'rose', 'netbeui', 'security', 'key', 'netlink', 'packet', 'ash', 'econet', 'atmsvc', 'rds', 'sna', 'irda', 'pppox', 'wanpipe', 'llc', 'can', 'tipc', 'bluetooth', 'iucv', 'rxrpc', 'isdn', 'phonet', - 'ieee802154', 'caif', 'alg', 'nfc', 'vsock' ] + 'ieee802154', 'caif', 'alg', 'nfc', 'vsock', 'mpls', 'ib' ] network_type_keywords = ['stream', 'dgram', 'seqpacket', 'rdm', 'raw', 'packet'] network_protocol_keywords = ['tcp', 'udp', 'icmp'] @@ -67,7 +66,7 @@ class NetworkRule(BaseRule): self.all_domains = False if domain == NetworkRule.ALL: self.all_domains = True - elif type(domain) == str: + elif type_is_str(domain): if domain in network_domain_keywords: self.domain = domain else: @@ -79,7 +78,7 @@ class NetworkRule(BaseRule): self.all_type_or_protocols = False if type_or_protocol == NetworkRule.ALL: self.all_type_or_protocols = True - elif type(type_or_protocol) == str: + elif type_is_str(type_or_protocol): if type_or_protocol in network_protocol_keywords: self.type_or_protocol = type_or_protocol elif type_or_protocol in network_type_keywords: diff --git a/utils/apparmor/rule/ptrace.py b/utils/apparmor/rule/ptrace.py new file mode 100644 index 000000000..f7f7d966d --- /dev/null +++ b/utils/apparmor/rule/ptrace.py @@ -0,0 +1,198 @@ +# ---------------------------------------------------------------------- +# Copyright (C) 2015 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 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 re + +from apparmor.regex import RE_PROFILE_PTRACE, RE_PROFILE_NAME +from apparmor.common import AppArmorBug, AppArmorException +from apparmor.rule import BaseRule, BaseRuleset, check_and_split_list, parse_modifiers, quote_if_needed + +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() + + +access_keywords = ['r', 'w', 'rw', 'wr', 'read', 'write', 'readby', 'trace', 'tracedby'] # XXX 'wr' and 'write' accepted by the parser, but not documented in apparmor.d.pod + +# XXX joint_access_keyword and RE_ACCESS_KEYWORDS exactly as in PtraceRule - move to function! +joint_access_keyword = '\s*(' + '|'.join(access_keywords) + ')\s*' +RE_ACCESS_KEYWORDS = ( joint_access_keyword + # one of the access_keyword or + '|' + # or + '\(' + joint_access_keyword + '(' + '(\s|,)+' + joint_access_keyword + ')*' + '\)' # one or more access_keyword in (...) + ) + + +RE_PTRACE_DETAILS = re.compile( + '^' + + '(\s+(?P' + RE_ACCESS_KEYWORDS + '))?' + # optional access keyword(s) + '(\s+(peer=' + RE_PROFILE_NAME % 'peer' + '))?' + # optional peer + '\s*$') + + +class PtraceRule(BaseRule): + '''Class to handle and store a single ptrace rule''' + + # Nothing external should reference this class, all external users + # should reference the class field PtraceRule.ALL + class __PtraceAll(object): + pass + + ALL = __PtraceAll + + def __init__(self, access, peer, audit=False, deny=False, allow_keyword=False, + comment='', log_event=None): + + super(PtraceRule, self).__init__(audit=audit, deny=deny, + allow_keyword=allow_keyword, + comment=comment, + log_event=log_event) + + self.access, self.all_access, unknown_items = check_and_split_list(access, access_keywords, PtraceRule.ALL, 'PtraceRule', 'access') + if unknown_items: + raise AppArmorException(_('Passed unknown access keyword to PtraceRule: %s') % ' '.join(unknown_items)) + + self.peer, self.all_peers = self._aare_or_all(peer, 'peer', is_path=False, log_event=log_event) + + @classmethod + def _match(cls, raw_rule): + return RE_PROFILE_PTRACE.search(raw_rule) + + @classmethod + def _parse(cls, raw_rule): + '''parse raw_rule and return PtraceRule''' + + matches = cls._match(raw_rule) + if not matches: + raise AppArmorException(_("Invalid ptrace rule '%s'") % raw_rule) + + audit, deny, allow_keyword, comment = parse_modifiers(matches) + + rule_details = '' + if matches.group('details'): + rule_details = matches.group('details') + + if rule_details: + details = RE_PTRACE_DETAILS.search(rule_details) + if not details: + raise AppArmorException(_("Invalid or unknown keywords in 'ptrace %s" % rule_details)) + + if details.group('access'): + # XXX move to function _split_access()? + access = details.group('access') + if access.startswith('(') and access.endswith(')'): + access = access[1:-1] + access = access.replace(',', ' ').split() # split by ',' or whitespace + else: + access = PtraceRule.ALL + + if details.group('peer'): + peer = details.group('peer') + else: + peer = PtraceRule.ALL + else: + access = PtraceRule.ALL + peer = PtraceRule.ALL + + return PtraceRule(access, peer, + audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment) + + def get_clean(self, depth=0): + '''return rule (in clean/default formatting)''' + + space = ' ' * depth + + if self.all_access: + access = '' + elif len(self.access) == 1: + access = ' %s' % ' '.join(self.access) + elif self.access: + access = ' (%s)' % ' '.join(sorted(self.access)) + else: + raise AppArmorBug('Empty access in ptrace rule') + + if self.all_peers: + peer = '' + elif self.peer: + peer = ' peer=%s' % quote_if_needed(self.peer.regex) + else: + raise AppArmorBug('Empty peer in ptrace rule') + + return('%s%sptrace%s%s,%s' % (space, self.modifiers_str(), access, peer, self.comment)) + + def is_covered_localvars(self, other_rule): + '''check if other_rule is covered by this rule object''' + + if not other_rule.access and not other_rule.all_access: + raise AppArmorBug('No access specified in other ptrace rule') + + if not other_rule.peer and not other_rule.all_peers: + raise AppArmorBug('No peer specified in other ptrace rule') + + if not self.all_access: + if other_rule.all_access: + return False + if other_rule.access != self.access: + return False + + if not self.all_peers: + if other_rule.all_peers: + return False + if not self.peer.match(other_rule.peer.regex): + return False + + # still here? -> then it is covered + return True + + def is_equal_localvars(self, rule_obj): + '''compare if rule-specific variables are equal''' + + if not type(rule_obj) == PtraceRule: + raise AppArmorBug('Passed non-ptrace rule: %s' % str(rule_obj)) + + if (self.access != rule_obj.access + or self.all_access != rule_obj.all_access): + return False + + if self.all_peers != rule_obj.all_peers: + return False + + if self.peer and not self.peer.is_equal(rule_obj.peer): + return False + + return True + + def logprof_header_localvars(self): + if self.all_access: + access = _('ALL') + else: + access = ' '.join(sorted(self.access)) + + if self.all_peers: + peer = _('ALL') + else: + peer = self.peer.regex + + return [ + _('Access mode'), access, + _('Peer'), peer + ] + + +class PtraceRuleset(BaseRuleset): + '''Class to handle and store a collection of ptrace rules''' + + def get_glob(self, path_or_rule): + '''Return the next possible glob. For ptrace rules, that means removing access or removing/globbing peer''' + # XXX only remove one part, not all + return 'ptrace,' diff --git a/utils/apparmor/rule/rlimit.py b/utils/apparmor/rule/rlimit.py index f38b0672a..454f44228 100644 --- a/utils/apparmor/rule/rlimit.py +++ b/utils/apparmor/rule/rlimit.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # ---------------------------------------------------------------------- # Copyright (C) 2013 Kshitij Gupta # Copyright (C) 2015 Christian Boltz @@ -17,7 +16,7 @@ import re from apparmor.regex import RE_PROFILE_RLIMIT, strip_quotes -from apparmor.common import AppArmorBug, AppArmorException +from apparmor.common import AppArmorBug, AppArmorException, type_is_str from apparmor.rule import BaseRule, BaseRuleset, parse_comment, quote_if_needed # setup module translations @@ -31,9 +30,9 @@ rlimit_nice = ['nice'] # a number between -20 and 19. rlimit_all = rlimit_size + rlimit_number + rlimit_time + rlimit_nice -RE_NUMBER_UNIT = re.compile('^(?P[0-9]+)(?P[a-zA-Z]*)$') +RE_NUMBER_UNIT = re.compile('^(?P[0-9]+)\s*(?P[a-zA-Z]*)$') RE_NUMBER = re.compile('^[0-9]+$') -RE_UNIT_SIZE = re.compile('^[0-9]+([KMG]B?)?$') +RE_UNIT_SIZE = re.compile('^[0-9]+\s*([KMG]B?)?$') RE_NICE = re.compile('^(-20|-[01]?[0-9]|[01]?[0-9])$') @@ -58,7 +57,7 @@ class RlimitRule(BaseRule): if audit or deny or allow_keyword: raise AppArmorBug('The audit, allow or deny keywords are not allowed in rlimit rules.') - if type(rlimit) == str: + if type_is_str(rlimit): if rlimit in rlimit_all: self.rlimit = rlimit else: @@ -71,7 +70,7 @@ class RlimitRule(BaseRule): self.all_values = False if value == RlimitRule.ALL: self.all_values = True - elif type(value) == str: + elif type_is_str(value): if not value.strip(): raise AppArmorBug('Empty value in rlimit rule') @@ -89,11 +88,6 @@ class RlimitRule(BaseRule): if not RE_NUMBER_UNIT.match(value): raise AppArmorException('Invalid value in rlimit %s %s rule' % (rlimit, value)) number, unit = split_unit(value) - if unit == 'm' and rlimit == 'rttime': - raise AppArmorException('Ambiguous value %s in rlimit %s rule - use "ms" or "minutes"' % (value, rlimit)) - if unit != '' and not ('seconds'.startswith(unit) or 'minutes'.startswith(unit) or 'hours'.startswith(unit) or - (rlimit == 'rttime' and unit in ['ms', 'us']) ): - raise AppArmorException('Invalid unit in rlimit %s %s rule' % (rlimit, value)) if rlimit == 'rttime': self.value_as_int = self.time_to_int(value, 'us') @@ -181,16 +175,24 @@ class RlimitRule(BaseRule): if unit == '': unit = default_unit - if unit == 'us': + if unit in ['us', 'microsecond', 'microseconds']: number = number / 1000000.0 - elif unit == 'ms': + if default_unit == 'seconds': + raise AppArmorException(_('Invalid unit in rlimit cpu %s rule') % value) + elif unit in ['ms', 'millisecond', 'milliseconds']: number = number / 1000.0 - elif 'seconds'.startswith(unit): + if default_unit == 'seconds': + raise AppArmorException(_('Invalid unit in rlimit cpu %s rule') % value) + elif unit in ['s', 'sec', 'second', 'seconds']: # manpage doesn't list sec pass - elif 'minutes'.startswith(unit): + elif unit in ['min', 'minute', 'minutes']: number = number * 60 - elif 'hours'.startswith(unit): + elif unit in ['h', 'hour', 'hours']: number = number * 60 * 60 + elif unit in ['d', 'day', 'days']: # manpage doesn't list 'd' + number = number * 60 * 60 * 24 + elif unit in ['week', 'weeks']: + number = number * 60 * 60 * 24 * 7 else: raise AppArmorException('Unknown unit %s in rlimit %s %s' % (unit, self.rlimit, value)) diff --git a/utils/apparmor/rule/signal.py b/utils/apparmor/rule/signal.py new file mode 100644 index 000000000..e594740e9 --- /dev/null +++ b/utils/apparmor/rule/signal.py @@ -0,0 +1,265 @@ +# ---------------------------------------------------------------------- +# Copyright (C) 2015 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 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 re + +from apparmor.regex import RE_PROFILE_SIGNAL, RE_PROFILE_NAME +from apparmor.common import AppArmorBug, AppArmorException +from apparmor.rule import BaseRule, BaseRuleset, check_and_split_list, parse_modifiers, quote_if_needed + +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() + + +access_keywords_read = ['receive', 'r', 'read'] +access_keywords_write = ['send', 'w', 'write'] +access_keywords_rw = ['rw', 'wr'] +access_keywords = access_keywords_read + access_keywords_write + access_keywords_rw + +signal_keywords = ['hup', 'int', 'quit', 'ill', 'trap', 'abrt', 'bus', 'fpe', 'kill', 'usr1', + 'segv', 'usr2', 'pipe', 'alrm', 'term', 'stkflt', 'chld', 'cont', 'stop', + 'stp', 'ttin', 'ttou', 'urg', 'xcpu', 'xfsz', 'vtalrm', 'prof', 'winch', + 'io', 'pwr', 'sys', 'emt', 'exists'] +RE_SIGNAL_REALTIME = re.compile('^rtmin\+0*([0-9]|[12][0-9]|3[0-2])$') # rtmin+0..rtmin+32, number may have leading zeros + +joint_access_keyword = '\s*(' + '|'.join(access_keywords) + ')\s*' +RE_ACCESS_KEYWORDS = ( joint_access_keyword + # one of the access_keyword or + '|' + # or + '\(' + joint_access_keyword + '(' + '(\s|,)+' + joint_access_keyword + ')*' + '\)' # one or more access_keyword in (...) + ) + +signal_keyword = '\s*([a-z0-9+]+|"[a-z0-9+]+")\s*' # don't check against the signal keyword list in the regex to allow a more helpful error message +RE_SIGNAL_KEYWORDS = ( + 'set\s*=\s*' + signal_keyword + # one of the signal_keyword or + '|' + # or + 'set\s*=\s*\(' + signal_keyword + '(' + '(\s|,)+' + signal_keyword + ')*' + '\)' # one or more signal_keyword in (...) + ) + + +RE_SIGNAL_DETAILS = re.compile( + '^' + + '(\s+(?P' + RE_ACCESS_KEYWORDS + '))?' + # optional access keyword(s) + '(?P' + '(\s+(' + RE_SIGNAL_KEYWORDS + '))+' + ')?' + # optional signal set(s) + '(\s+(peer=' + RE_PROFILE_NAME % 'peer' + '))?' + # optional peer + '\s*$') + + +RE_FILTER_SET_1 = re.compile('set\s*=\s*\(([^)]*)\)') +RE_FILTER_SET_2 = re.compile('set\s*=') +RE_FILTER_PARENTHESIS = re.compile('\((.*)\)') +RE_FILTER_QUOTES = re.compile('"([a-z0-9]+)"') # used to strip quotes around signal keywords - don't use for peer! + +class SignalRule(BaseRule): + '''Class to handle and store a single signal rule''' + + # Nothing external should reference this class, all external users + # should reference the class field SignalRule.ALL + class __SignalAll(object): + pass + + ALL = __SignalAll + + def __init__(self, access, signal, peer, audit=False, deny=False, allow_keyword=False, + comment='', log_event=None): + + super(SignalRule, self).__init__(audit=audit, deny=deny, + allow_keyword=allow_keyword, + comment=comment, + log_event=log_event) + + self.access, self.all_access, unknown_items = check_and_split_list(access, access_keywords, SignalRule.ALL, 'SignalRule', 'access') + if unknown_items: + raise AppArmorException(_('Passed unknown access keyword to SignalRule: %s') % ' '.join(unknown_items)) + + self.signal, self.all_signals, unknown_items = check_and_split_list(signal, signal_keywords, SignalRule.ALL, 'SignalRule', 'signal') + if unknown_items: + for item in unknown_items: + if RE_SIGNAL_REALTIME.match(item): + self.signal.add(item) + else: + raise AppArmorException(_('Passed unknown signal keyword to SignalRule: %s') % item) + + self.peer, self.all_peers = self._aare_or_all(peer, 'peer', is_path=False, log_event=log_event) + + @classmethod + def _match(cls, raw_rule): + return RE_PROFILE_SIGNAL.search(raw_rule) + + @classmethod + def _parse(cls, raw_rule): + '''parse raw_rule and return SignalRule''' + + matches = cls._match(raw_rule) + if not matches: + raise AppArmorException(_("Invalid signal rule '%s'") % raw_rule) + + audit, deny, allow_keyword, comment = parse_modifiers(matches) + + rule_details = '' + if matches.group('details'): + rule_details = matches.group('details') + + if rule_details: + details = RE_SIGNAL_DETAILS.search(rule_details) + if not details: + raise AppArmorException(_("Invalid or unknown keywords in 'signal %s" % rule_details)) + + if details.group('access'): + access = details.group('access') + if access.startswith('(') and access.endswith(')'): + access = access[1:-1] + access = access.replace(',', ' ').split() # split by ',' or whitespace + else: + access = SignalRule.ALL + + if details.group('signal'): + signal = details.group('signal') + signal = RE_FILTER_SET_1.sub(r'\1', signal) # filter out 'set=' + signal = RE_FILTER_SET_2.sub('', signal) # filter out 'set=' + signal = RE_FILTER_QUOTES.sub(r' \1 ', signal) # filter out quote pairs + signal = signal.replace(',', ' ').split() # split at ',' or whitespace + else: + signal = SignalRule.ALL + + if details.group('peer'): + peer = details.group('peer') + else: + peer = SignalRule.ALL + else: + access = SignalRule.ALL + signal = SignalRule.ALL + peer = SignalRule.ALL + + return SignalRule(access, signal, peer, + audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment) + + def get_clean(self, depth=0): + '''return rule (in clean/default formatting)''' + + space = ' ' * depth + + if self.all_access: + access = '' + elif len(self.access) == 1: + access = ' %s' % ' '.join(self.access) + elif self.access: + access = ' (%s)' % ' '.join(sorted(self.access)) + else: + raise AppArmorBug('Empty access in signal rule') + + if self.all_signals: + signal = '' + elif len(self.signal) == 1: + signal = ' set=%s' % ' '.join(self.signal) + elif self.signal: + signal = ' set=(%s)' % ' '.join(sorted(self.signal)) + else: + raise AppArmorBug('Empty signal in signal rule') + + if self.all_peers: + peer = '' + elif self.peer: + peer = ' peer=%s' % quote_if_needed(self.peer.regex) + else: + raise AppArmorBug('Empty peer in signal rule') + + return('%s%ssignal%s%s%s,%s' % (space, self.modifiers_str(), access, signal, peer, self.comment)) + + def is_covered_localvars(self, other_rule): + '''check if other_rule is covered by this rule object''' + + if not other_rule.access and not other_rule.all_access: + raise AppArmorBug('No access specified in other signal rule') + + if not other_rule.signal and not other_rule.all_signals: + raise AppArmorBug('No signal specified in other signal rule') + + if not other_rule.peer and not other_rule.all_peers: + raise AppArmorBug('No peer specified in other signal rule') + + if not self.all_access: + if other_rule.all_access: + return False + if other_rule.access != self.access: + return False + + if not self.all_signals: + if other_rule.all_signals: + return False + if other_rule.signal != self.signal: + return False + + if not self.all_peers: + if other_rule.all_peers: + return False + if not self.peer.match(other_rule.peer.regex): + return False + + # still here? -> then it is covered + return True + + def is_equal_localvars(self, rule_obj): + '''compare if rule-specific variables are equal''' + + if not type(rule_obj) == SignalRule: + raise AppArmorBug('Passed non-signal rule: %s' % str(rule_obj)) + + if (self.access != rule_obj.access + or self.all_access != rule_obj.all_access): + return False + + if (self.signal != rule_obj.signal + or self.all_signals != rule_obj.all_signals): + return False + + if self.all_peers != rule_obj.all_peers: + return False + + if self.peer and not self.peer.is_equal(rule_obj.peer): + return False + + return True + + def logprof_header_localvars(self): + if self.all_access: + access = _('ALL') + else: + access = ' '.join(sorted(self.access)) + + if self.all_signals: + signal = _('ALL') + else: + signal = ' '.join(sorted(self.signal)) + + if self.all_peers: + peer = _('ALL') + else: + peer = self.peer.regex + + return [ + _('Access mode'), access, + _('Signal'), signal, + _('Peer'), peer + ] + + +class SignalRuleset(BaseRuleset): + '''Class to handle and store a collection of signal rules''' + + def get_glob(self, path_or_rule): + '''Return the next possible glob. For signal rules, that means removing access, signal or peer''' + # XXX only remove one part, not all + return 'signal,' + diff --git a/utils/apparmor/rules.py b/utils/apparmor/rules.py index 6c3b53ae9..77a893169 100644 --- a/utils/apparmor/rules.py +++ b/utils/apparmor/rules.py @@ -71,12 +71,6 @@ class Raw_DBUS_Rule(_Raw_Rule): class Raw_Mount_Rule(_Raw_Rule): pass -class Raw_Signal_Rule(_Raw_Rule): - pass - -class Raw_Ptrace_Rule(_Raw_Rule): - pass - class Raw_Pivot_Root_Rule(_Raw_Rule): pass diff --git a/utils/logprof.conf b/utils/logprof.conf index f5e719ada..a3d7a3102 100644 --- a/utils/logprof.conf +++ b/utils/logprof.conf @@ -106,6 +106,13 @@ /usr/bin/killall = icn /usr/bin/nice = icn /usr/bin/perl = icn + /usr/bin/python = icn + /usr/bin/python2 = icn + /usr/bin/python2.7 = icn + /usr/bin/python3 = icn + /usr/bin/python3.3 = icn + /usr/bin/python3.4 = icn + /usr/bin/python3.5 = icn /usr/bin/tr = icn [required_hats] diff --git a/utils/test/Makefile b/utils/test/Makefile index d2e8207b1..45c8e0b9c 100644 --- a/utils/test/Makefile +++ b/utils/test/Makefile @@ -20,6 +20,27 @@ COMMONDIR=../../common/ include $(COMMONDIR)/Make.rules +ifdef USE_SYSTEM + LD_LIBRARY_PATH= + PYTHONPATH= +else + # PYTHON_DIST_BUILD_PATH based on libapparmor/swig/python/test/Makefile.am + PYTHON_DIST_BUILD_PATH = ../../libraries/libapparmor/swig/python/build/$$($(PYTHON) -c "import distutils.util; import platform; print(\"lib.%s-%s\" %(distutils.util.get_platform(), platform.python_version()[:3]))") + LD_LIBRARY_PATH=../../libraries/libapparmor/src/.libs/ + PYTHONPATH=..:$(PYTHON_DIST_BUILD_PATH) +endif + +.PHONY: __libapparmor +__libapparmor: +ifndef USE_SYSTEM + @if [ ! -f $(LD_LIBRARY_PATH)libapparmor.so ]; then \ + echo "error: $(LD_LIBRARY_PATH)libapparmor.so is missing. Pick one of these possible solutions:" 1>&2; \ + echo " 1) Build against the in-tree libapparmor by building it first and then trying again. See the top-level README for help." 1>&2; \ + echo " 2) Build against the system libapparmor by adding USE_SYSTEM=1 to your make command." 1>&2; \ + return 1; \ + fi +endif + COVERAGE_OMIT=test-*.py,common_test.py ifneq ($(COVERAGE_OUT), ) HTML_COVR_ARGS=-d $(COVERAGE_OUT) @@ -40,11 +61,11 @@ endif clean: rm -rf __pycache__/ .coverage htmlcov -check: - export PYTHONPATH=.. ; $(foreach test, $(wildcard test-*.py), $(call pyalldo, $(test))) +check: __libapparmor + export PYTHONPATH=$(PYTHONPATH) ; export LD_LIBRARY_PATH=$(LD_LIBRARY_PATH) ; $(foreach test, $(wildcard test-*.py), $(call pyalldo, $(test))) -.coverage: $(wildcard ../aa-* ../apparmor/*.py test-*.py) - export PYTHONPATH=.. ; $(COVERAGE_IGNORE_FAILURES_CMD) ; $(foreach test, $(wildcard test-*.py), $(PYTHON) -m coverage run --branch -p $(test); ) +.coverage: $(wildcard ../aa-* ../apparmor/*.py test-*.py) __libapparmor + export PYTHONPATH=$(PYTHONPATH) ; export LD_LIBRARY_PATH=$(LD_LIBRARY_PATH); $(COVERAGE_IGNORE_FAILURES_CMD) ; $(foreach test, $(wildcard test-*.py), $(PYTHON) -m coverage run --branch -p $(test); ) $(PYTHON) -m coverage combine coverage: .coverage diff --git a/utils/test/cleanprof_test.in b/utils/test/cleanprof_test.in index 6fd88b6bb..8822b88f1 100644 --- a/utils/test/cleanprof_test.in +++ b/utils/test/cleanprof_test.in @@ -8,6 +8,8 @@ allow /usr/share/X11/locale/** r, allow /home/*/** r, + unix (receive) type=dgram, + ^foo { /etc/fstab r, capability dac_override, diff --git a/utils/test/cleanprof_test.out b/utils/test/cleanprof_test.out index 9238ab171..c20f6ff4d 100644 --- a/utils/test/cleanprof_test.out +++ b/utils/test/cleanprof_test.out @@ -6,6 +6,8 @@ /usr/bin/a/simple/cleanprof/test/profile { #include + unix (receive) type=dgram, + /home/*/** r, /home/foo/** w, diff --git a/utils/test/common_test.py b/utils/test/common_test.py index 1f48082c3..f7c2d25d1 100755 --- a/utils/test/common_test.py +++ b/utils/test/common_test.py @@ -15,25 +15,10 @@ import unittest import inspect import os -import re import shutil import sys import tempfile -import apparmor.common -import apparmor.config - -class Test(unittest.TestCase): - - - def test_RegexParser(self): - tests = apparmor.config.Config('ini') - tests.CONF_DIR = '.' - regex_tests = tests.read_config('regex_tests.ini') - for regex in regex_tests.sections(): - parsed_regex = re.compile(apparmor.common.convert_regexp(regex)) - for regex_testcase in regex_tests.options(regex): - self.assertEqual(bool(parsed_regex.search(regex_testcase)), eval(regex_tests[regex][regex_testcase]), 'Incorrectly Parsed regex: %s' %regex) #def test_readkey(self): # print("Please press the Y button on the keyboard.") diff --git a/utils/test/regex_tests.ini b/utils/test/regex_tests.ini deleted file mode 100644 index fd4d8151e..000000000 --- a/utils/test/regex_tests.ini +++ /dev/null @@ -1,81 +0,0 @@ -# ---------------------------------------------------------------------- -# Copyright (C) 2013 Kshitij Gupta -# -# 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. -# -# ---------------------------------------------------------------------- -[/foo/**/bar/] - /foo/user/tools/bar/ = True - /foo/apparmor/bar/ = True - /foo/apparmor/bar = False - -[/foo/*/bar/] - /foo/apparmor/bar/ = True - /foo/apparmor/tools/bar/ = False - /foo/apparmor/bar = False - -[/foo/{foo,bar,user,other}/bar/] - /foo/user/bar/ = True - /foo/bar/bar/ = True - /foo/wrong/bar/ = False - -[/foo/{foo,bar,user,other}/test,ca}se/{aa,sd,nd}/bar/] - /foo/user/test,ca}se/aa/bar/ = True - /foo/bar/test,ca}se/sd/bar/ = True - /foo/wrong/user/bar/ = False - /foo/user/wrong/bar/ = False - /foo/wrong/aa/bar/ = False - -[/foo/user/ba?/] - /foo/user/bar/ = True - /foo/user/bar/apparmor/ = False - /foo/user/ba/ = False - /foo/user/ba// = False - -[/foo/user/bar/**] - /foo/user/bar/apparmor = True - /foo/user/bar/apparmor/tools = True - /foo/user/bar/ = False - -[/foo/user/bar/*] - /foo/user/bar/apparmor = True - /foo/user/bar/apparmor/tools = False - /foo/user/bar/ = False - /foo/user/bar/apparmor/ = False - -[/foo/**.jpg] - /foo/bar/baz/foobar.jpg = True - /foo/bar/foobar.jpg = True - /foo/bar/*.jpg = True - /foo/bar.jpg = True - /foo/barjpg = False - /foo/.* = False - /foo/**.jpg = True - /foo/*.jpg = True - /bar.jpg = False - /**.jpg = False - /*.jpg = False - /foo/*.bar = False - -[/foo/{**,}] - /foo/ = True - /foo/bar = True - /foo/bar/ = True - /foo/bar/baz = True - /foo/bar/baz/ = True - /bar/ = False - -[/foo/{,**}] - /foo/ = True - /foo/bar = True - /foo/bar/ = True - /foo/bar/baz = True - /foo/bar/baz/ = True - /bar/ = False diff --git a/utils/test/test-aa.py b/utils/test/test-aa.py index 57a23d4b7..b5e662b4c 100644 --- a/utils/test/test-aa.py +++ b/utils/test/test-aa.py @@ -13,8 +13,12 @@ import unittest from common_test import AATest, setup_all_loops from common_test import read_file, write_file -from apparmor.aa import (check_for_apparmor, get_profile_flags, set_profile_flags, is_skippable_file, is_skippable_dir, - parse_profile_start, parse_profile_data, separate_vars, store_list_var, write_header, serialize_parse_profile_start) +import os + +from apparmor.aa import (check_for_apparmor, get_interpreter_and_abstraction, create_new_profile, + get_profile_flags, set_profile_flags, is_skippable_file, is_skippable_dir, + parse_profile_start, parse_profile_data, separate_vars, store_list_var, write_header, + var_transform, serialize_parse_profile_start) from apparmor.common import AppArmorException, AppArmorBug class AaTestWithTempdir(AATest): @@ -68,6 +72,77 @@ class AaTest_check_for_apparmor(AaTestWithTempdir): mounts = write_file(self.tmpdir, 'mounts', self.MOUNTS_WITH_SECURITYFS % self.tmpdir) self.assertEqual('%s/security/apparmor' % self.tmpdir, check_for_apparmor(filesystems, mounts)) +class AaTest_create_new_profile(AATest): + tests = [ + # file content expected interpreter expected abstraction (besides 'base') + ('#!/bin/bash\ntrue', (u'/bin/bash', 'abstractions/bash')), + ('foo bar', (None, None)), + ] + def _run_test(self, params, expected): + exp_interpreter_path, exp_abstraction = expected + + program = self.writeTmpfile('script', params) + profile = create_new_profile(program) + + if exp_interpreter_path: + self.assertEqual(profile[program][program]['allow']['path'][exp_interpreter_path]['mode'], {'x', '::i', '::x', 'i'} ) + self.assertEqual(profile[program][program]['allow']['path'][exp_interpreter_path]['audit'], set() ) + self.assertEqual(profile[program][program]['allow']['path'][program]['mode'], {'r', '::r'} ) + self.assertEqual(profile[program][program]['allow']['path'][program]['audit'], set() ) + self.assertEqual(set(profile[program][program]['allow']['path'].keys()), {program, exp_interpreter_path} ) + else: + self.assertEqual(profile[program][program]['allow']['path'][program]['mode'], {'r', '::r', 'm', '::m'} ) + self.assertEqual(profile[program][program]['allow']['path'][program]['audit'], set() ) + self.assertEqual(set(profile[program][program]['allow']['path'].keys()), {program} ) + + if exp_abstraction: + self.assertEqual(set(profile[program][program]['include'].keys()), {exp_abstraction, 'abstractions/base'}) + else: + self.assertEqual(set(profile[program][program]['include'].keys()), {'abstractions/base'}) + +class AaTest_get_interpreter_and_abstraction(AATest): + tests = [ + ('#!/bin/bash', ('/bin/bash', 'abstractions/bash')), + ('#!/bin/dash', ('/bin/dash', 'abstractions/bash')), + ('#!/bin/sh', ('/bin/sh', 'abstractions/bash')), + ('#! /bin/sh ', ('/bin/sh', 'abstractions/bash')), + ('#! /bin/sh -x ', ('/bin/sh', 'abstractions/bash')), # '-x' is not part of the interpreter path + ('#!/usr/bin/perl', ('/usr/bin/perl', 'abstractions/perl')), + ('#!/usr/bin/perl -w', ('/usr/bin/perl', 'abstractions/perl')), # '-w' is not part of the interpreter path + ('#!/usr/bin/python', ('/usr/bin/python', 'abstractions/python')), + ('#!/usr/bin/python2', ('/usr/bin/python2', 'abstractions/python')), + ('#!/usr/bin/python2.7', ('/usr/bin/python2.7', 'abstractions/python')), + ('#!/usr/bin/python3', ('/usr/bin/python3', 'abstractions/python')), + ('#!/usr/bin/python4', ('/usr/bin/python4', None)), # python abstraction is only applied to py2 and py3 + ('#!/usr/bin/ruby', ('/usr/bin/ruby', 'abstractions/ruby')), + ('#!/usr/bin/foobarbaz', ('/usr/bin/foobarbaz', None)), # we don't have an abstraction for "foobarbaz" + ('foo', (None, None)), # no hashbang - not a script + ] + + def _run_test(self, params, expected): + exp_interpreter_path, exp_abstraction = expected + + program = self.writeTmpfile('program', "%s\nfoo\nbar" % params) + interpreter_path, abstraction = get_interpreter_and_abstraction(program) + + # damn symlinks! + if exp_interpreter_path and os.path.islink(exp_interpreter_path): + dirname = os.path.dirname(exp_interpreter_path) + exp_interpreter_path = os.readlink(exp_interpreter_path) + if not exp_interpreter_path.startswith('/'): + exp_interpreter_path = os.path.join(dirname, exp_interpreter_path) + + self.assertEqual(interpreter_path, exp_interpreter_path) + self.assertEqual(abstraction, exp_abstraction) + + def test_special_file(self): + self.assertEqual((None, None), get_interpreter_and_abstraction('/dev/null')) + + def test_file_not_found(self): + self.createTmpdir() + self.assertEqual((None, None), get_interpreter_and_abstraction('%s/file-not-found' % self.tmpdir)) + + class AaTest_get_profile_flags(AaTestWithTempdir): def _test_get_flags(self, profile_header, expected_flags): file = write_file(self.tmpdir, 'profile', '%s {\n}\n' % profile_header) @@ -410,20 +485,29 @@ class AaTest_separate_vars(AATest): ('' , set() ), (' ' , set() ), (' foo bar' , {'foo', 'bar' }), - ('foo " ' , {'foo' }), # XXX " is ignored - (' " foo ' , {' "', 'foo' }), # XXX really? + ('foo " ' , AppArmorException ), + (' " foo ' , AppArmorException ), # half-quoted (' foo bar ' , {'foo', 'bar' }), - (' foo bar # comment' , {'foo', 'bar', 'comment' }), # XXX should comments be stripped? + (' foo bar # comment' , {'foo', 'bar', '#', 'comment'}), # XXX should comments be stripped? ('foo' , {'foo' }), ('"foo" "bar baz"' , {'foo', 'bar baz' }), ('foo "bar baz" xy' , {'foo', 'bar baz', 'xy' }), - ('foo "bar baz ' , {'foo', 'bar', 'baz' }), # half-quoted + ('foo "bar baz ' , AppArmorException ), # half-quoted (' " foo" bar' , {' foo', 'bar' }), + (' " foo" bar x' , {' foo', 'bar', 'x' }), + ('""' , {'' }), # empty value + ('"" foo' , {'', 'foo' }), # empty value + 'foo' + ('"" foo "bar"' , {'', 'foo', 'bar' }), # empty value + 'foo' + 'bar' (bar has superfluous quotes) + ('"bar"' , {'bar' }), # 'bar' with superfluous quotes ] def _run_test(self, params, expected): - result = separate_vars(params) - self.assertEqual(result, expected) + if expected == AppArmorException: + with self.assertRaises(expected): + separate_vars(params) + else: + result = separate_vars(params) + self.assertEqual(result, expected) class AaTest_store_list_var(AATest): @@ -505,6 +589,17 @@ class AaTest_write_header(AATest): result = write_header(prof_data, depth, name, embedded_hat, write_flags) self.assertEqual(result, [expected]) +class AaTest_var_transform(AATest): + tests = [ + (['foo', ''], 'foo ""' ), + (['foo', 'bar'], 'foo bar' ), + ([''], '""' ), + (['bar baz', 'foo'], '"bar baz" foo' ), + ] + + def _run_test(self, params, expected): + self.assertEqual(var_transform(params), expected) + class AaTest_serialize_parse_profile_start(AATest): def _parse(self, line, profile, hat, prof_data_profile, prof_data_external): # 'correct' is always True in the code that uses serialize_parse_profile_start() (set some lines above the function call) diff --git a/utils/test/test-aare.py b/utils/test/test-aare.py new file mode 100644 index 000000000..25d36cd4a --- /dev/null +++ b/utils/test/test-aare.py @@ -0,0 +1,249 @@ +#! /usr/bin/env python +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 Kshitij Gupta +# Copyright (C) 2015 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 unittest +from common_test import AATest, setup_all_loops + +from copy import deepcopy +import re +from apparmor.common import convert_regexp, AppArmorBug, AppArmorException +from apparmor.aare import AARE, convert_expression_to_aare + +class TestConvert_regexp(AATest): + tests = [ + ('/foo', '^/foo$'), + ('/{foo,bar}', '^/(foo|bar)$'), + # ('/\{foo,bar}', '^/\{foo,bar}$'), # XXX gets converted to ^/\(foo|bar)$ + ('/fo[abc]', '^/fo[abc]$'), + ('/foo bar', '^/foo bar$'), + ('/x\y', '^/x\y$'), + ('/x\[y', '^/x\[y$'), + ('/x\\y', '^/x\\y$'), + ('/fo?', '^/fo[^/\000]$'), + ('/foo/*', '^/foo/(((?<=/)[^/\000]+)|((? /bar,'), 'change_profile -> /bar,') def test_glob_ext(self): - with self.assertRaises(AppArmorBug): + with self.assertRaises(NotImplementedError): # get_glob_ext is not available for change_profile rules self.ruleset.get_glob_ext('change_profile /foo -> /bar,') diff --git a/utils/test/test-common.py b/utils/test/test-common.py new file mode 100644 index 000000000..372cf0259 --- /dev/null +++ b/utils/test/test-common.py @@ -0,0 +1,32 @@ +#! /usr/bin/env python +# ------------------------------------------------------------------ +# +# Copyright (C) 2015 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 unittest +from common_test import AATest, setup_all_loops + +from apparmor.common import type_is_str + +class TestIs_str_type(AATest): + tests = [ + ('foo', True), + (u'foo', True), + (42, False), + (True, False), + ([], False), + ] + + def _run_test(self, params, expected): + self.assertEqual(type_is_str(params), expected) + + +setup_all_loops(__name__) +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/utils/test/test-libapparmor-test_multi.py b/utils/test/test-libapparmor-test_multi.py new file mode 100644 index 000000000..440131c12 --- /dev/null +++ b/utils/test/test-libapparmor-test_multi.py @@ -0,0 +1,167 @@ +#! /usr/bin/env python +# ------------------------------------------------------------------ +# +# Copyright (C) 2015 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 unittest +from common_test import AATest, setup_all_loops + +import os +from apparmor.common import open_file_read + +from apparmor.logparser import ReadLog + +# This testcase will parse all libraries/libapparmor/testsuite/test_multi tests +# and compare the result with the *.out files + + +class TestLibapparmorTestMulti(AATest): + tests = [] # filled by parse_test_profiles() + + def _run_test(self, params, expected): + # tests[][expected] is a dummy, replace it with the real values + expected = self._parse_libapparmor_test_multi(params) + + with open_file_read('%s.in' % params) as f_in: + loglines = f_in.readlines() + + loglines2 = [] + for line in loglines: + if line.strip(): + loglines2 += [line] + + self.assertEqual(len(loglines2), 1, '%s.in should only contain one line!' % params) + + parser = ReadLog('', '', '', '', '') + parsed_event = parser.parse_event(loglines2[0]) + + if parsed_event and expected: + parsed_items = dict(parsed_event.items()) + + # check if the line passes the regex in logparser.py + if not parser.RE_LOG_ALL.search(loglines2[0]): + raise Exception("Log event doesn't match RE_LOG_ALL") + + for label in expected: + if label in [ + 'file', # filename of the *.in file + 'event_type', # mapped to aamode + 'audit_id', 'audit_sub_id', # not set nor relevant + 'comm', # not set, and not too useful + # XXX most of the keywords listed below mean "TODO" + 'fsuid', 'ouid', # file events + 'flags', 'fs_type', # mount + 'namespace', # file_lock only?? (at least the tests don't contain this in other event types with namespace) + 'net_local_addr', 'net_foreign_addr', 'net_local_port', 'net_foreign_port', # detailed network events + 'peer', 'signal', # signal + 'src_name', # pivotroot + 'dbus_bus', 'dbus_interface', 'dbus_member', 'dbus_path', # dbus + 'peer_pid', 'peer_profile', # dbus + ]: + pass + elif parsed_items['operation'] == 'exec' and label in ['sock_type', 'family', 'protocol']: + pass # XXX 'exec' + network? really? + elif not parsed_items.get(label, None): + raise Exception('parsed_items[%s] not set' % label) + elif not expected.get(label, None): + raise Exception('expected[%s] not set' % label) + else: + self.assertEqual(str(parsed_items[label]), expected[label], '%s differs' % label) + elif expected: + self.assertIsNone(parsed_event) # that's why we end up here + self.assertEqual(dict(), expected, 'parsed_event is none') # effectively print the content of expected + elif parsed_event: + self.assertIsNone(expected) # that's why we end up here + self.assertEqual(parsed_event, dict(), 'expected is none') # effectively print the content of parsed_event + else: + self.assertIsNone(expected) # that's why we end up here + self.assertIsNone(parsed_event) # that's why we end up here + self.assertEqual(parsed_event, expected) # both are None + + # list of labels that use a different name in logparser.py than in the test_multi *.out files + # (additionally, .lower() is applied to all labels) + label_map = { + 'Mask': 'request_mask', + 'Command': 'comm', + 'Token': 'magic_token', + 'ErrorCode': 'error_code', + 'Network family': 'family', + 'Socket type': 'sock_type', + 'Local addr': 'net_local_addr', + 'Foreign addr': 'net_foreign_addr', + 'Local port': 'net_local_port', + 'Foreign port': 'net_foreign_port', + 'Audit subid': 'audit_sub_id', + 'Attribute': 'attr', + 'Epoch': 'time', + } + + def _parse_libapparmor_test_multi(self, file_with_path): + '''parse the libapparmor test_multi *.in tests and their expected result in *.out''' + + with open_file_read('%s.out' % file_with_path) as f_in: + expected = f_in.readlines() + + if expected[0].rstrip('\n') != 'START': + raise Exception("%s.out doesn't have 'START' in its first line! (%s)" % ( file_with_path, expected[0])) + + expected.pop(0) + + exresult = dict() + for line in expected: + label, value = line.split(':', 1) + + # test_multi doesn't always use the original labels :-/ + if label in self.label_map.keys(): + label = self.label_map[label] + label = label.replace(' ', '_').lower() + exresult[label] = value.strip() + + if not exresult['event_type'].startswith('AA_RECORD_'): + raise Exception("event_type doesn't start with AA_RECORD_: %s in file %s" % (exresult['event_type'], file_with_path)) + + exresult['aamode'] = exresult['event_type'].replace('AA_RECORD_', '') + if exresult['aamode'] == 'ALLOWED': + exresult['aamode'] = 'PERMITTING' + if exresult['aamode'] == 'DENIED': + exresult['aamode'] = 'REJECTING' + + if exresult['event_type'] == 'AA_RECORD_INVALID': # or exresult.get('error_code', 0) != 0: # XXX should events with errors be ignored? + exresult = None + + return exresult + + +def find_test_multi(log_dir): + '''find all log sniplets in the given log_dir''' + + log_dir = os.path.abspath(log_dir) + + print('Testing libapparmor test_multi tests...') + + tests = [] + for root, dirs, files in os.walk(log_dir): + for file in files: + if file.endswith('.in'): + file_with_path = os.path.join(root, file[:-3]) # filename without '.in' + tests.append([file_with_path, True]) # True is a dummy testresult, parsing of the *.out files is done while running the tests + + elif file.endswith('.out') or file.endswith('.err'): + pass + else: + raise Exception('Found unknown file %s in libapparmor test_multi' % file) + + return tests + + +TestLibapparmorTestMulti.tests = find_test_multi('../../libraries/libapparmor/testsuite/test_multi/') + +setup_all_loops(__name__) +if __name__ == '__main__': + unittest.main(verbosity=1) # reduced verbosity due to the big number of tests diff --git a/utils/test/test-logparser.py b/utils/test/test-logparser.py index 2738d792d..640d77960 100644 --- a/utils/test/test-logparser.py +++ b/utils/test/test-logparser.py @@ -28,8 +28,7 @@ class TestParseEvent(unittest.TestCase): self.assertEqual(parsed_event['aamode'], 'PERMITTING') self.assertEqual(parsed_event['request_mask'], 'wc') - self.assertIsNotNone(ReadLog.RE_LOG_v2_6_audit.search(event)) - self.assertIsNone(ReadLog.RE_LOG_v2_6_syslog.search(event)) + self.assertIsNotNone(ReadLog.RE_LOG_ALL.search(event)) def test_parse_event_audit_2(self): event = 'type=AVC msg=audit(1322614918.292:4376): apparmor="ALLOWED" operation="file_perm" parent=16001 profile=666F6F20626172 name="/home/foo/.bash_history" pid=17011 comm="bash" requested_mask="rw" denied_mask="rw" fsuid=0 ouid=1000' @@ -39,8 +38,7 @@ class TestParseEvent(unittest.TestCase): self.assertEqual(parsed_event['aamode'], 'PERMITTING') self.assertEqual(parsed_event['request_mask'], 'rw') - self.assertIsNotNone(ReadLog.RE_LOG_v2_6_audit.search(event)) - self.assertIsNone(ReadLog.RE_LOG_v2_6_syslog.search(event)) + self.assertIsNotNone(ReadLog.RE_LOG_ALL.search(event)) def test_parse_event_syslog_1(self): # from https://bugs.launchpad.net/apparmor/+bug/1399027 @@ -51,8 +49,7 @@ class TestParseEvent(unittest.TestCase): self.assertEqual(parsed_event['aamode'], 'PERMITTING') self.assertEqual(parsed_event['request_mask'], 'rw') - self.assertIsNone(ReadLog.RE_LOG_v2_6_audit.search(event)) - self.assertIsNotNone(ReadLog.RE_LOG_v2_6_syslog.search(event)) + self.assertIsNotNone(ReadLog.RE_LOG_ALL.search(event)) def test_parse_event_syslog_2(self): # from https://bugs.launchpad.net/apparmor/+bug/1399027 @@ -63,8 +60,7 @@ class TestParseEvent(unittest.TestCase): self.assertEqual(parsed_event['aamode'], 'PERMITTING') self.assertEqual(parsed_event['request_mask'], 'r') - self.assertIsNone(ReadLog.RE_LOG_v2_6_audit.search(event)) - self.assertIsNotNone(ReadLog.RE_LOG_v2_6_syslog.search(event)) + self.assertIsNotNone(ReadLog.RE_LOG_ALL.search(event)) def test_parse_disconnected_path(self): # from https://bugzilla.opensuse.org/show_bug.cgi?id=918787 @@ -91,8 +87,7 @@ class TestParseEvent(unittest.TestCase): 'time': 1424425690 }) - self.assertIsNotNone(ReadLog.RE_LOG_v2_6_audit.search(event)) - self.assertIsNone(ReadLog.RE_LOG_v2_6_syslog.search(event)) + self.assertIsNotNone(ReadLog.RE_LOG_ALL.search(event)) if __name__ == "__main__": diff --git a/utils/test/test-network.py b/utils/test/test-network.py index 0892376fb..2f2b87f02 100644 --- a/utils/test/test-network.py +++ b/utils/test/test-network.py @@ -353,6 +353,15 @@ class NetworkLogprofHeaderTest(AATest): obj = NetworkRule._parse(params) self.assertEqual(obj.logprof_header(), expected) +class NetworkRuleReprTest(AATest): + tests = [ + (NetworkRule('inet', 'stream'), ' network inet stream,'), + (NetworkRule.parse(' allow network inet stream, # foo'), ' allow network inet stream, # foo'), + ] + def _run_test(self, params, expected): + self.assertEqual(str(params), expected) + + ## --- tests for NetworkRuleset --- # class NetworkRulesTest(AATest): @@ -432,13 +441,24 @@ class NetworkGlobTestAATest(AATest): # self.assertEqual(self.ruleset.get_glob('network inet raw,'), 'network inet,') def test_glob_ext(self): - with self.assertRaises(AppArmorBug): + with self.assertRaises(NotImplementedError): # get_glob_ext is not available for network rules self.ruleset.get_glob_ext('network inet raw,') class NetworkDeleteTestAATest(AATest): pass +class NetworkRulesetReprTest(AATest): + def test_network_ruleset_repr(self): + obj = NetworkRuleset() + obj.add(NetworkRule('inet', 'stream')) + obj.add(NetworkRule.parse(' allow network inet stream, # foo')) + + expected = '\n network inet stream,\n allow network inet stream, # foo\n' + self.assertEqual(str(obj), expected) + + + setup_all_loops(__name__) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/utils/test/test-parser-simple-tests.py b/utils/test/test-parser-simple-tests.py new file mode 100644 index 000000000..d527c2d44 --- /dev/null +++ b/utils/test/test-parser-simple-tests.py @@ -0,0 +1,427 @@ +#! /usr/bin/env python +# ------------------------------------------------------------------ +# +# Copyright (C) 2015 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 unittest +from common_test import AATest, setup_all_loops +import apparmor.aa as apparmor + +import os +from apparmor.common import open_file_read, AppArmorException + +# This testcase will parse all parser/tst/simple_tests with parse_profile_data(), +# except the files listed in one of the arrays below. +# +# Files listed in skip_startswith will be completely skipped. +# Files listed in the other arrays will be checked, but with the opposite of the expected result. + + +# XXX tests listed here will be *** SKIPPED *** XXX +skip_startswith = ( + # lots of invalid dbus rules (the tools currently just store them without any parsing) + 'generated_dbus/bad-perms-', + 'generated_dbus/bad-formatting-', + 'generated_dbus/duplicated-conditionals-', + 'generated_dbus/eavesdrop-incompat-', + 'generated_dbus/message-incompat-', + 'generated_dbus/pairing-unsupported-', + 'generated_dbus/service-incompat-', + + # the tools don't check for conflicting x permissions (yet?) + 'generated_x/conflict-', + 'generated_x/ambiguous-', + 'generated_x/dominate-', + + # permissions before path + 'generated_perms_leading/', + + # 'safe' and 'unsafe' keywords + 'generated_perms_safe/', + + # Pux and Cux (which actually mean PUx and CUx) get rejected by the tools + 'generated_x/exact-', +) + +# testcases that should raise an exception, but don't +exception_not_raised = [ + 'capability/bad_1.sd', + 'capability/bad_2.sd', + 'capability/bad_3.sd', + 'capability/bad_4.sd', + 'change_hat/bad_parsing.sd', + 'dbus/bad_bind_1.sd', + 'dbus/bad_bind_2.sd', + 'dbus/bad_eavesdrop_1.sd', + 'dbus/bad_modifier_1.sd', + 'dbus/bad_modifier_2.sd', + 'dbus/bad_modifier_3.sd', + 'dbus/bad_modifier_4.sd', + 'dbus/bad_peer_1.sd', + 'dbus/bad_regex_01.sd', + 'dbus/bad_regex_02.sd', + 'dbus/bad_regex_03.sd', + 'dbus/bad_regex_04.sd', + 'dbus/bad_regex_05.sd', + 'dbus/bad_regex_06.sd', + 'file/bad_append_1.sd', + 'file/bad_append_2.sd', + 'file/bad_embedded_spaces_1.sd', + 'file/bad_re_brace_2.sd', + 'file/bad_re_brace_3.sd', + 'file/file/bad_append_1.sd', + 'file/file/bad_embedded_spaces_1.sd', + 'file/file/owner/bad_3.sd', + 'file/file/owner/bad_5.sd', + 'file/owner/bad_3.sd', + 'file/owner/bad_5.sd', + 'mount/bad_opt_10.sd', + 'mount/bad_opt_11.sd', + 'mount/bad_opt_12.sd', + 'mount/bad_opt_13.sd', + 'mount/bad_opt_14.sd', + 'mount/bad_opt_15.sd', + 'mount/bad_opt_16.sd', + 'mount/bad_opt_17.sd', + 'mount/bad_opt_18.sd', + 'mount/bad_opt_19.sd', + 'mount/bad_opt_1.sd', + 'mount/bad_opt_20.sd', + 'mount/bad_opt_21.sd', + 'mount/bad_opt_22.sd', + 'mount/bad_opt_23.sd', + 'mount/bad_opt_24.sd', + 'mount/bad_opt_2.sd', + 'mount/bad_opt_3.sd', + 'mount/bad_opt_4.sd', + 'mount/bad_opt_5.sd', + 'mount/bad_opt_6.sd', + 'mount/bad_opt_7.sd', + 'mount/bad_opt_8.sd', + 'mount/bad_opt_9.sd', + 'profile/flags/flags_bad10.sd', + 'profile/flags/flags_bad11.sd', + 'profile/flags/flags_bad12.sd', + 'profile/flags/flags_bad13.sd', + 'profile/flags/flags_bad15.sd', + 'profile/flags/flags_bad18.sd', + 'profile/flags/flags_bad19.sd', + 'profile/flags/flags_bad20.sd', + 'profile/flags/flags_bad2.sd', + 'profile/flags/flags_bad3.sd', + 'profile/flags/flags_bad4.sd', + 'profile/flags/flags_bad5.sd', + 'profile/flags/flags_bad6.sd', + 'profile/flags/flags_bad7.sd', + 'profile/flags/flags_bad8.sd', + 'profile/flags/flags_bad_debug_1.sd', + 'profile/flags/flags_bad_debug_2.sd', + 'profile/flags/flags_bad_debug_3.sd', + 'profile/flags/flags_bad_debug_4.sd', + 'profile/simple_bad_no_close_brace4.sd', + 'ptrace/bad_05.sd', # actually contains a capability rule with invalid (ptrace-related) keyword + 'ptrace/bad_06.sd', # actually contains a capability rule with invalid (ptrace-related) keyword + 'ptrace/bad_10.sd', # peer with invalid regex + 'signal/bad_21.sd', # invalid regex + 'unix/bad_attr_1.sd', + 'unix/bad_attr_2.sd', + 'unix/bad_attr_3.sd', + 'unix/bad_attr_4.sd', + 'unix/bad_bind_1.sd', + 'unix/bad_bind_2.sd', + 'unix/bad_create_1.sd', + 'unix/bad_create_2.sd', + 'unix/bad_listen_1.sd', + 'unix/bad_listen_2.sd', + 'unix/bad_modifier_1.sd', + 'unix/bad_modifier_2.sd', + 'unix/bad_modifier_3.sd', + 'unix/bad_modifier_4.sd', + 'unix/bad_opt_1.sd', + 'unix/bad_opt_2.sd', + 'unix/bad_opt_3.sd', + 'unix/bad_opt_4.sd', + 'unix/bad_peer_1.sd', + 'unix/bad_regex_01.sd', + 'unix/bad_regex_02.sd', + 'unix/bad_regex_03.sd', + 'unix/bad_regex_04.sd', + 'unix/bad_shutdown_1.sd', + 'unix/bad_shutdown_2.sd', + 'vars/boolean/boolean_bad_1.sd', + 'vars/boolean/boolean_bad_2.sd', + 'vars/boolean/boolean_bad_3.sd', + 'vars/boolean/boolean_bad_4.sd', + 'vars/boolean/boolean_bad_6.sd', + 'vars/boolean/boolean_bad_7.sd', + 'vars/boolean/boolean_bad_8.sd', + 'vars/vars_bad_3.sd', + 'vars/vars_bad_4.sd', + 'vars/vars_bad_5.sd', + 'vars/vars_bad_7.sd', + 'vars/vars_bad_8.sd', + 'vars/vars_bad_trailing_comma_1.sd', + 'vars/vars_bad_trailing_comma_2.sd', + 'vars/vars_bad_trailing_comma_3.sd', + 'vars/vars_bad_trailing_comma_4.sd', + 'vars/vars_dbus_bad_01.sd', + 'vars/vars_dbus_bad_02.sd', + 'vars/vars_dbus_bad_03.sd', + 'vars/vars_dbus_bad_04.sd', + 'vars/vars_dbus_bad_05.sd', + 'vars/vars_dbus_bad_06.sd', + 'vars/vars_dbus_bad_07.sd', + 'vars/vars_file_evaluation_7.sd', + 'vars/vars_file_evaluation_8.sd', + + # profile name in var doesn't start with / + 'vars/vars_profile_name_bad_1.sd', + + # profile name is undefined variable + 'vars/vars_profile_name_bad_2.sd', + 'vars/vars_profile_name_23.sd', + + # attachment in var doesn't start with / + 'vars/vars_profile_name_07.sd', + 'vars/vars_profile_name_08.sd', + 'vars/vars_profile_name_12.sd', + 'vars/vars_profile_name_13.sd', + 'vars/vars_profile_name_15.sd', + 'vars/vars_profile_name_22.sd', + + 'vars/vars_recursion_1.sd', + 'vars/vars_recursion_2.sd', + 'vars/vars_recursion_3.sd', + 'vars/vars_recursion_4.sd', + 'vars/vars_simple_assignment_10.sd', + 'vars/vars_simple_assignment_3.sd', + 'vars/vars_simple_assignment_8.sd', + 'vars/vars_simple_assignment_9.sd', + 'xtrans/simple_bad_conflicting_x_10.sd', + 'xtrans/simple_bad_conflicting_x_11.sd', + 'xtrans/simple_bad_conflicting_x_12.sd', + 'xtrans/simple_bad_conflicting_x_13.sd', + 'xtrans/simple_bad_conflicting_x_14.sd', + 'xtrans/simple_bad_conflicting_x_15.sd', + 'xtrans/simple_bad_conflicting_x_1.sd', + 'xtrans/simple_bad_conflicting_x_2.sd', + 'xtrans/simple_bad_conflicting_x_3.sd', + 'xtrans/simple_bad_conflicting_x_4.sd', + 'xtrans/simple_bad_conflicting_x_5.sd', + 'xtrans/simple_bad_conflicting_x_6.sd', + 'xtrans/simple_bad_conflicting_x_7.sd', + 'xtrans/simple_bad_conflicting_x_8.sd', + 'xtrans/simple_bad_conflicting_x_9.sd', + 'xtrans/x-conflict.sd', +] + +# testcases with lines that don't match any regex and end up as "unknown line" +unknown_line = [ + # 'other' keyword + 'file/allow/ok_other_1.sd', + 'file/allow/ok_other_2.sd', + 'file/ok_other_1.sd', + 'file/ok_other_2.sd', + 'file/ok_other_3.sd', + + # permissions before path + 'file/file/front_perms_ok_1.sd', + 'file/front_perms_ok_1.sd', + 'profile/local/local_named_profile_ok1.sd', + 'profile/local/local_profile_ok1.sd', + 'xtrans/simple_ok_cx_1.sd', + + # permissions before path and owner / audit {...} blocks + 'file/file/owner/ok_1.sd', + 'file/owner/ok_1.sd', + 'profile/entry_mods_audit_ok1.sd', + + # namespace + 'profile/profile_ns_named_ok1.sd', # profile keyword? + 'profile/profile_ns_named_ok2.sd', # profile keyword? + 'profile/profile_ns_named_ok3.sd', # profile keyword? + 'profile/profile_ns_ok1.sd', + 'profile/profile_ns_ok2.sd', + 'profile/profile_ns_ok3.sd', # profile keyword? + 'profile/re_named_ok4.sd', # profile keyword + 'profile/re_ok4.sd', +] + +# testcases with various unexpected failures +syntax_failure = [ + # profile keyword? + 'profile/re_named_ok2.sd', + + # Syntax Error: Unexpected hat definition found (external hat) + 'change_hat/new_style4.sd', + + # Syntax Errors caused by boolean conditions (parse_profile_data() gets confused by the closing '}') + 'conditional/defined_1.sd', + 'conditional/defined_2.sd', + 'conditional/else_1.sd', + 'conditional/else_2.sd', + 'conditional/else_3.sd', + 'conditional/else_if_1.sd', + 'conditional/else_if_2.sd', + 'conditional/else_if_3.sd', + 'conditional/else_if_5.sd', + 'conditional/ok_1.sd', + 'conditional/ok_2.sd', + 'conditional/ok_3.sd', + 'conditional/ok_4.sd', + 'conditional/ok_5.sd', + 'conditional/ok_6.sd', + 'conditional/ok_7.sd', + 'conditional/ok_8.sd', + 'conditional/ok_9.sd', + 'conditional/stress_1.sd', + + # unexpected uppercase vs. lowercase in *x rules + 'file/ok_5.sd', # Invalid mode UX + 'file/ok_2.sd', # Invalid mode RWM + 'file/ok_4.sd', # Invalid mode iX + 'xtrans/simple_ok_pix_1.sd', # Invalid mode pIx + 'xtrans/simple_ok_pux_1.sd', # Invalid mode rPux + + # misc + 'vars/vars_simple_assignment_12.sd', # Redefining existing variable @{BAR} ('\' not handled) + 'rewrite/alias_good_5.sd', # Values added to a non-existing variable @{FOO} (defined in include, lp:1331856) + 'bare_include_tests/ok_2.sd', # two #include<...> in one line +] + +class TestParseParserTests(AATest): + tests = [] # filled by parse_test_profiles() + + def _run_test(self, params, expected): + with open_file_read(params['file']) as f_in: + data = f_in.readlines() + + if params['disabled']: + # skip disabled testcases + return + + if params['tools_wrong']: + # if the tools are marked as being wrong about a profile, expect the opposite result + # this makes sure we notice any behaviour change, especially not being wrong anymore + expected = not expected + + if expected: + apparmor.parse_profile_data(data, params['file'], 0) + else: + with self.assertRaises(AppArmorException): + apparmor.parse_profile_data(data, params['file'], 0) + +def parse_test_profiles(file_with_path): + '''parse the test-related headers of a profile (for example EXRESULT) and add the profile to the set of tests''' + exresult = None + exresult_found = False + description = None + todo = False + disabled = False + + with open_file_read(file_with_path) as f_in: + data = f_in.readlines() + + relfile = os.path.relpath(file_with_path, apparmor.profile_dir) + + for line in data: + if line.startswith('#=EXRESULT '): + exresult = line.split()[1] + if exresult == 'PASS': + exresult == True + exresult_found = True + elif exresult == 'FAIL': + exresult = False + exresult_found = True + else: + raise Exception('%s contains unknown EXRESULT %s' % (file_with_path, exresult)) + + elif line.upper().startswith('#=DESCRIPTION '): + description = line.split()[1] + + elif line.rstrip() == '#=TODO': + todo = True + + elif line.rstrip() == '#=DISABLED': + disabled = True + + if not exresult_found: + raise Exception('%s does not contain EXRESULT' % file_with_path) + + if not description: + raise Exception('%s does not contain description' % file_with_path) + + tools_wrong = False + if relfile in exception_not_raised: + if exresult: + raise Exception("%s listed in exception_not_raised, but has EXRESULT PASS" % file_with_path) + tools_wrong = 'EXCEPTION_NOT_RAISED' + elif relfile.startswith(skip_startswith): + return 1 # XXX *** SKIP *** those tests + elif relfile in unknown_line: + if not exresult: + raise Exception("%s listed in unknown_line, but has EXRESULT FAIL" % file_with_path) + tools_wrong = 'UNKNOWN_LINE' + elif relfile in syntax_failure: + if not exresult: + raise Exception("%s listed in syntax_failure, but has EXRESULT FAIL" % file_with_path) + tools_wrong = 'SYNTAX_FAILURE' + + params = { + 'file': file_with_path, + 'relfile': relfile, + 'todo': todo, + 'disabled': disabled, + 'tools_wrong': tools_wrong, + 'exresult': exresult, + } + + TestParseParserTests.tests.append((params, exresult)) + return 0 + + +def find_and_setup_test_profiles(profile_dir): + '''find all profiles in the given profile_dir, excluding + - skippable files + - include directories + - files in the main directory (readme, todo etc.) + ''' + skipped = 0 + + profile_dir = os.path.abspath(profile_dir) + + apparmor.profile_dir = profile_dir + + print('Searching for parser simpe_tests... (this will take a while)') + + for root, dirs, files in os.walk(profile_dir): + relpath = os.path.relpath(root, profile_dir) + + if relpath == '.': + # include files are checked as part of the profiles that include them (also, they don't contain EXRESULT) + dirs.remove('includes') + dirs.remove('include_tests') + dirs.remove('includes-preamble') + + for file in files: + file_with_path = os.path.join(root, file) + if not apparmor.is_skippable_file(file) and relpath != '.': + skipped += parse_test_profiles(file_with_path) + + if skipped: + print('Skipping %s test profiles listed in skip_startswith.' % skipped) + + print('Running %s parser simple_tests...' % len(TestParseParserTests.tests)) + + +find_and_setup_test_profiles('../../parser/tst/simple_tests/') + +setup_all_loops(__name__) +if __name__ == '__main__': + unittest.main(verbosity=1) # reduced verbosity due to the big number of tests diff --git a/utils/test/test-ptrace.py b/utils/test/test-ptrace.py new file mode 100644 index 000000000..3258db49b --- /dev/null +++ b/utils/test/test-ptrace.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python +# ---------------------------------------------------------------------- +# Copyright (C) 2015 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 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 unittest +from collections import namedtuple +from common_test import AATest, setup_all_loops + +from apparmor.rule.ptrace import PtraceRule, PtraceRuleset +from apparmor.rule import BaseRule +from apparmor.common import AppArmorException, AppArmorBug +from apparmor.logparser import ReadLog +from apparmor.translations import init_translation +_ = init_translation() + +exp = namedtuple('exp', ['audit', 'allow_keyword', 'deny', 'comment', + 'access', 'all_access', 'peer', 'all_peers']) + +# # --- tests for single PtraceRule --- # + +class PtraceTest(AATest): + def _compare_obj(self, obj, expected): + self.assertEqual(expected.allow_keyword, obj.allow_keyword) + self.assertEqual(expected.audit, obj.audit) + self.assertEqual(expected.access, obj.access) + if obj.peer: + self.assertEqual(expected.peer, obj.peer.regex) + else: + self.assertEqual(expected.peer, obj.peer) + self.assertEqual(expected.all_access, obj.all_access) + self.assertEqual(expected.all_peers, obj.all_peers) + self.assertEqual(expected.deny, obj.deny) + self.assertEqual(expected.comment, obj.comment) + +class PtraceTestParse(PtraceTest): + tests = [ + # PtraceRule object audit allow deny comment access all? peer all? + ('ptrace,' , exp(False, False, False, '', None , True , None, True )), +# ('ptrace (),' , exp(False, False, False, '', None , True , None, True )), # XXX also broken in SignalRule? + ('ptrace read,' , exp(False, False, False, '', {'read'}, False, None, True )), + ('ptrace (read, tracedby),' , exp(False, False, False, '', {'read', 'tracedby'}, False, None, True )), + ('ptrace read,' , exp(False, False, False, '', {'read'}, False, None, True )), + ('deny ptrace read, # cmt' , exp(False, False, True , ' # cmt', {'read'}, False, None, True )), + ('audit allow ptrace,' , exp(True , True , False, '', None , True , None, True )), + ('ptrace peer=unconfined,' , exp(False, False, False, '', None , True , 'unconfined', False )), + ('ptrace read,' , exp(False, False, False, '', {'read'}, False, None, True )), + ('ptrace peer=/foo,' , exp(False, False, False, '', None , True , '/foo', False )), + ('ptrace r peer=/foo,' , exp(False, False, False, '', {'r'}, False, '/foo', False )), + ] + + def _run_test(self, rawrule, expected): + self.assertTrue(PtraceRule.match(rawrule)) + obj = PtraceRule.parse(rawrule) + self.assertEqual(rawrule.strip(), obj.raw_rule) + self._compare_obj(obj, expected) + +class PtraceTestParseInvalid(PtraceTest): + tests = [ + ('ptrace foo,' , AppArmorException), + ('ptrace foo bar,' , AppArmorException), + ('ptrace foo int,' , AppArmorException), + ('ptrace read bar,' , AppArmorException), + ('ptrace read tracedby,' , AppArmorException), + ('ptrace peer=,' , AppArmorException), + ] + + def _run_test(self, rawrule, expected): + self.assertTrue(PtraceRule.match(rawrule)) # the above invalid rules still match the main regex! + with self.assertRaises(expected): + PtraceRule.parse(rawrule) + +class PtraceTestParseFromLog(PtraceTest): + def test_ptrace_from_log(self): + parser = ReadLog('', '', '', '', '') + event = 'type=AVC msg=audit(1409700683.304:547661): apparmor="DENIED" operation="ptrace" profile="/home/ubuntu/bzr/apparmor/tests/regression/apparmor/ptrace" pid=22465 comm="ptrace" requested_mask="tracedby" denied_mask="tracedby" peer="/home/ubuntu/bzr/apparmor/tests/regression/apparmor/ptrace"' + + + parsed_event = parser.parse_event(event) + + self.assertEqual(parsed_event, { + 'request_mask': 'tracedby', + 'denied_mask': 'tracedby', + 'error_code': 0, + 'magic_token': 0, + 'parent': 0, + 'profile': '/home/ubuntu/bzr/apparmor/tests/regression/apparmor/ptrace', + 'peer': '/home/ubuntu/bzr/apparmor/tests/regression/apparmor/ptrace', + 'operation': 'ptrace', + 'resource': None, + 'info': None, + 'aamode': 'REJECTING', + 'time': 1409700683, + 'active_hat': None, + 'pid': 22465, + 'task': 0, + 'attr': None, + 'name2': None, + 'name': None, + }) + + obj = PtraceRule(parsed_event['denied_mask'], parsed_event['peer'], log_event=parsed_event) + + # audit allow deny comment access all? peer all? + expected = exp(False, False, False, '', {'tracedby'}, False, '/home/ubuntu/bzr/apparmor/tests/regression/apparmor/ptrace', False) + + self._compare_obj(obj, expected) + + self.assertEqual(obj.get_raw(1), ' ptrace tracedby peer=/home/ubuntu/bzr/apparmor/tests/regression/apparmor/ptrace,') + +class PtraceFromInit(PtraceTest): + tests = [ + # PtraceRule object audit allow deny comment access all? peer all? + (PtraceRule('r', 'unconfined', deny=True) , exp(False, False, True , '' , {'r'}, False, 'unconfined', False)), + (PtraceRule(('r', 'read'), '/bin/foo') , exp(False, False, False, '' , {'r', 'read'},False, '/bin/foo', False)), + (PtraceRule(PtraceRule.ALL, '/bin/foo') , exp(False, False, False, '' , None, True, '/bin/foo', False )), + (PtraceRule('rw', '/bin/foo') , exp(False, False, False, '' , {'rw'}, False, '/bin/foo', False )), + (PtraceRule('rw', PtraceRule.ALL) , exp(False, False, False, '' , {'rw'}, False, None, True )), + (PtraceRule(PtraceRule.ALL, PtraceRule.ALL) , exp(False, False, False, '' , None , True, None, True )), + ] + + def _run_test(self, obj, expected): + self._compare_obj(obj, expected) + +class InvalidPtraceInit(AATest): + tests = [ + # init params expected exception + (['' , '/foo' ] , AppArmorBug), # empty access + (['read', '' ] , AppArmorBug), # empty peer + ([' ', '/foo' ] , AppArmorBug), # whitespace access + (['read', ' ' ] , AppArmorBug), # whitespace peer + (['xyxy', '/foo' ] , AppArmorException), # invalid access + # XXX is 'invalid peer' possible at all? + ([dict(), '/foo' ] , AppArmorBug), # wrong type for access + ([None , '/foo' ] , AppArmorBug), # wrong type for access + (['read', dict() ] , AppArmorBug), # wrong type for peer + (['read', None ] , AppArmorBug), # wrong type for peer + ] + + def _run_test(self, params, expected): + with self.assertRaises(expected): + PtraceRule(params[0], params[1]) + + def test_missing_params_1(self): + with self.assertRaises(TypeError): + PtraceRule() + + def test_missing_params_2(self): + with self.assertRaises(TypeError): + PtraceRule('r') + +class InvalidPtraceTest(AATest): + def _check_invalid_rawrule(self, rawrule): + obj = None + self.assertFalse(PtraceRule.match(rawrule)) + with self.assertRaises(AppArmorException): + obj = PtraceRule(PtraceRule.parse(rawrule)) + + self.assertIsNone(obj, 'PtraceRule handed back an object unexpectedly') + + def test_invalid_ptrace_missing_comma(self): + self._check_invalid_rawrule('ptrace') # missing comma + + def test_invalid_non_PtraceRule(self): + self._check_invalid_rawrule('dbus,') # not a ptrace rule + + def test_empty_data_1(self): + obj = PtraceRule('read', '/foo') + obj.access = '' + # no access set, and ALL not set + with self.assertRaises(AppArmorBug): + obj.get_clean(1) + + def test_empty_data_2(self): + obj = PtraceRule('read', '/foo') + obj.peer = '' + # no ptrace set, and ALL not set + with self.assertRaises(AppArmorBug): + obj.get_clean(1) + + +class WritePtraceTestAATest(AATest): + def _run_test(self, rawrule, expected): + self.assertTrue(PtraceRule.match(rawrule)) + obj = PtraceRule.parse(rawrule) + clean = obj.get_clean() + raw = obj.get_raw() + + self.assertEqual(expected.strip(), clean, 'unexpected clean rule') + self.assertEqual(rawrule.strip(), raw, 'unexpected raw rule') + + tests = [ + # raw rule clean rule + ('ptrace,' , 'ptrace,'), + (' ptrace , # foo ' , 'ptrace, # foo'), + (' audit ptrace read,' , 'audit ptrace read,'), + (' audit ptrace (read ),' , 'audit ptrace read,'), + (' audit ptrace (read , tracedby ),' , 'audit ptrace (read tracedby),'), + (' deny ptrace read ,# foo bar' , 'deny ptrace read, # foo bar'), + (' deny ptrace ( read ), ' , 'deny ptrace read,'), + (' allow ptrace ,# foo bar' , 'allow ptrace, # foo bar'), + ('ptrace,' , 'ptrace,'), + ('ptrace (trace),' , 'ptrace trace,'), + ('ptrace (tracedby),' , 'ptrace tracedby,'), + ('ptrace (read),' , 'ptrace read,'), + ('ptrace (readby),' , 'ptrace readby,'), + ('ptrace (trace read),' , 'ptrace (read trace),'), + ('ptrace (read tracedby),' , 'ptrace (read tracedby),'), + ('ptrace r,' , 'ptrace r,'), + ('ptrace w,' , 'ptrace w,'), + ('ptrace rw,' , 'ptrace rw,'), + ('ptrace read,' , 'ptrace read,'), + ('ptrace (tracedby),' , 'ptrace tracedby,'), + ('ptrace w,' , 'ptrace w,'), + ('ptrace read peer=foo,' , 'ptrace read peer=foo,'), + ('ptrace tracedby peer=foo,' , 'ptrace tracedby peer=foo,'), + ('ptrace (read tracedby) peer=/usr/bin/bar,' , 'ptrace (read tracedby) peer=/usr/bin/bar,'), + ('ptrace (trace read) peer=/usr/bin/bar,' , 'ptrace (read trace) peer=/usr/bin/bar,'), + ('ptrace wr peer=/sbin/baz,' , 'ptrace wr peer=/sbin/baz,'), + ] + + def test_write_manually(self): + obj = PtraceRule('read', '/foo', allow_keyword=True) + + expected = ' allow ptrace read peer=/foo,' + + self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule') + self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule') + + +class PtraceCoveredTest(AATest): + def _run_test(self, param, expected): + obj = PtraceRule.parse(self.rule) + check_obj = PtraceRule.parse(param) + + self.assertTrue(PtraceRule.match(param)) + + self.assertEqual(obj.is_equal(check_obj), expected[0], 'Mismatch in is_equal, expected %s' % expected[0]) + self.assertEqual(obj.is_equal(check_obj, True), expected[1], 'Mismatch in is_equal/strict, expected %s' % expected[1]) + + self.assertEqual(obj.is_covered(check_obj), expected[2], 'Mismatch in is_covered, expected %s' % expected[2]) + self.assertEqual(obj.is_covered(check_obj, True, True), expected[3], 'Mismatch in is_covered/exact, expected %s' % expected[3]) + +class PtraceCoveredTest_01(PtraceCoveredTest): + rule = 'ptrace read,' + + tests = [ + # rule equal strict equal covered covered exact + ('ptrace,' , [ False , False , False , False ]), + ('ptrace read,' , [ True , True , True , True ]), + ('ptrace read peer=unconfined,' , [ False , False , True , True ]), + ('ptrace read, # comment' , [ True , False , True , True ]), + ('allow ptrace read,' , [ True , False , True , True ]), + ('ptrace read,' , [ True , False , True , True ]), + ('audit ptrace read,' , [ False , False , False , False ]), + ('audit ptrace,' , [ False , False , False , False ]), + ('ptrace tracedby,' , [ False , False , False , False ]), + ('audit deny ptrace read,' , [ False , False , False , False ]), + ('deny ptrace read,' , [ False , False , False , False ]), + ] + +class PtraceCoveredTest_02(PtraceCoveredTest): + rule = 'audit ptrace read,' + + tests = [ + # rule equal strict equal covered covered exact + ( 'ptrace read,' , [ False , False , True , False ]), + ('audit ptrace read,' , [ True , True , True , True ]), + ( 'ptrace,' , [ False , False , False , False ]), + ('audit ptrace,' , [ False , False , False , False ]), + ('ptrace tracedby,' , [ False , False , False , False ]), + ] + +class PtraceCoveredTest_03(PtraceCoveredTest): + rule = 'ptrace,' + + tests = [ + # rule equal strict equal covered covered exact + ( 'ptrace,' , [ True , True , True , True ]), + ('allow ptrace,' , [ True , False , True , True ]), + ( 'ptrace read,' , [ False , False , True , True ]), + ( 'ptrace w,' , [ False , False , True , True ]), + ('audit ptrace,' , [ False , False , False , False ]), + ('deny ptrace,' , [ False , False , False , False ]), + ] + +class PtraceCoveredTest_04(PtraceCoveredTest): + rule = 'deny ptrace read,' + + tests = [ + # rule equal strict equal covered covered exact + ( 'deny ptrace read,' , [ True , True , True , True ]), + ('audit deny ptrace read,' , [ False , False , False , False ]), + ( 'ptrace read,' , [ False , False , False , False ]), # XXX should covered be true here? + ( 'deny ptrace tracedby,' , [ False , False , False , False ]), + ( 'deny ptrace,' , [ False , False , False , False ]), + ] + +class PtraceCoveredTest_05(PtraceCoveredTest): + rule = 'ptrace read peer=unconfined,' + + tests = [ + # rule equal strict equal covered covered exact + ('ptrace,' , [ False , False , False , False ]), + ('ptrace read,' , [ False , False , False , False ]), + ('ptrace read peer=unconfined,' , [ True , True , True , True ]), + ('ptrace peer=unconfined,' , [ False , False , False , False ]), + ('ptrace read, # comment' , [ False , False , False , False ]), + ('allow ptrace read,' , [ False , False , False , False ]), + ('allow ptrace read peer=unconfined,' , [ True , False , True , True ]), + ('allow ptrace read peer=/foo/bar,' , [ False , False , False , False ]), + ('allow ptrace read peer=/**,' , [ False , False , False , False ]), + ('allow ptrace read peer=**,' , [ False , False , False , False ]), + ('ptrace read,' , [ False , False , False , False ]), + ('ptrace read peer=unconfined,' , [ True , False , True , True ]), + ('audit ptrace read peer=unconfined,' , [ False , False , False , False ]), + ('audit ptrace,' , [ False , False , False , False ]), + ('ptrace tracedby,' , [ False , False , False , False ]), + ('audit deny ptrace read,' , [ False , False , False , False ]), + ('deny ptrace read,' , [ False , False , False , False ]), + ] + +class PtraceCoveredTest_06(PtraceCoveredTest): + rule = 'ptrace read peer=/foo/bar,' + + tests = [ + # rule equal strict equal covered covered exact + ('ptrace,' , [ False , False , False , False ]), + ('ptrace read,' , [ False , False , False , False ]), + ('ptrace read peer=/foo/bar,' , [ True , True , True , True ]), + ('ptrace read peer=/foo/*,' , [ False , False , False , False ]), + ('ptrace read peer=/**,' , [ False , False , False , False ]), + ('ptrace read peer=/what/*,' , [ False , False , False , False ]), + ('ptrace peer=/foo/bar,' , [ False , False , False , False ]), + ('ptrace read, # comment' , [ False , False , False , False ]), + ('allow ptrace read,' , [ False , False , False , False ]), + ('allow ptrace read peer=/foo/bar,' , [ True , False , True , True ]), + ('ptrace read,' , [ False , False , False , False ]), + ('ptrace read peer=/foo/bar,' , [ True , False , True , True ]), + ('ptrace read peer=/what/ever,' , [ False , False , False , False ]), + ('audit ptrace read peer=/foo/bar,' , [ False , False , False , False ]), + ('audit ptrace,' , [ False , False , False , False ]), + ('ptrace tracedby,' , [ False , False , False , False ]), + ('audit deny ptrace read,' , [ False , False , False , False ]), + ('deny ptrace read,' , [ False , False , False , False ]), + ] + +class PtraceCoveredTest_07(PtraceCoveredTest): + rule = 'ptrace read peer=**,' + + tests = [ + # rule equal strict equal covered covered exact + ('ptrace,' , [ False , False , False , False ]), + ('ptrace read,' , [ False , False , False , False ]), + ('ptrace read peer=/foo/bar,' , [ False , False , True , True ]), + ('ptrace read peer=/foo/*,' , [ False , False , True , True ]), + ('ptrace read peer=/**,' , [ False , False , True , True ]), + ('ptrace read peer=/what/*,' , [ False , False , True , True ]), + ('ptrace peer=/foo/bar,' , [ False , False , False , False ]), + ('ptrace read, # comment' , [ False , False , False , False ]), + ('allow ptrace read,' , [ False , False , False , False ]), + ('allow ptrace read peer=/foo/bar,' , [ False , False , True , True ]), + ('ptrace read,' , [ False , False , False , False ]), + ('ptrace read peer=/foo/bar,' , [ False , False , True , True ]), + ('ptrace read peer=/what/ever,' , [ False , False , True , True ]), + ('audit ptrace read peer=/foo/bar,' , [ False , False , False , False ]), + ('audit ptrace,' , [ False , False , False , False ]), + ('ptrace tracedby,' , [ False , False , False , False ]), + ('audit deny ptrace read,' , [ False , False , False , False ]), + ('deny ptrace read,' , [ False , False , False , False ]), + ] + +class PtraceCoveredTest_Invalid(AATest): + def test_borked_obj_is_covered_1(self): + obj = PtraceRule.parse('ptrace read peer=/foo,') + + testobj = PtraceRule('read', '/foo') + testobj.access = '' + + with self.assertRaises(AppArmorBug): + obj.is_covered(testobj) + + def test_borked_obj_is_covered_2(self): + obj = PtraceRule.parse('ptrace read peer=/foo,') + + testobj = PtraceRule('read', '/foo') + testobj.peer = '' + + with self.assertRaises(AppArmorBug): + obj.is_covered(testobj) + + def test_invalid_is_covered(self): + obj = PtraceRule.parse('ptrace read,') + + testobj = BaseRule() # different type + + with self.assertRaises(AppArmorBug): + obj.is_covered(testobj) + + def test_invalid_is_equal(self): + obj = PtraceRule.parse('ptrace read,') + + testobj = BaseRule() # different type + + with self.assertRaises(AppArmorBug): + obj.is_equal(testobj) + + +class PtraceLogprofHeaderTest(AATest): + tests = [ + ('ptrace,', [ _('Access mode'), _('ALL'), _('Peer'), _('ALL'), ]), + ('ptrace read,', [ _('Access mode'), 'read', _('Peer'), _('ALL'), ]), + ('deny ptrace,', [_('Qualifier'), 'deny', _('Access mode'), _('ALL'), _('Peer'), _('ALL'), ]), + ('allow ptrace read,', [_('Qualifier'), 'allow', _('Access mode'), 'read', _('Peer'), _('ALL'), ]), + ('audit ptrace read,', [_('Qualifier'), 'audit', _('Access mode'), 'read', _('Peer'), _('ALL'), ]), + ('audit deny ptrace read,', [_('Qualifier'), 'audit deny', _('Access mode'), 'read', _('Peer'), _('ALL'), ]), + ('ptrace (read, tracedby) peer=/foo,', [ _('Access mode'), 'read tracedby', _('Peer'), '/foo', ]), + ] + + def _run_test(self, params, expected): + obj = PtraceRule._parse(params) + self.assertEqual(obj.logprof_header(), expected) + +## --- tests for PtraceRuleset --- # + +class PtraceRulesTest(AATest): + def test_empty_ruleset(self): + ruleset = PtraceRuleset() + ruleset_2 = PtraceRuleset() + self.assertEqual([], ruleset.get_raw(2)) + self.assertEqual([], ruleset.get_clean(2)) + self.assertEqual([], ruleset_2.get_raw(2)) + self.assertEqual([], ruleset_2.get_clean(2)) + + def test_ruleset_1(self): + ruleset = PtraceRuleset() + rules = [ + 'ptrace peer=/foo,', + 'ptrace read,', + ] + + expected_raw = [ + 'ptrace peer=/foo,', + 'ptrace read,', + '', + ] + + expected_clean = [ + 'ptrace peer=/foo,', + 'ptrace read,', + '', + ] + + for rule in rules: + ruleset.add(PtraceRule.parse(rule)) + + self.assertEqual(expected_raw, ruleset.get_raw()) + self.assertEqual(expected_clean, ruleset.get_clean()) + + def test_ruleset_2(self): + ruleset = PtraceRuleset() + rules = [ + 'ptrace read peer=/foo,', + 'allow ptrace read,', + 'deny ptrace peer=/bar, # example comment', + ] + + expected_raw = [ + ' ptrace read peer=/foo,', + ' allow ptrace read,', + ' deny ptrace peer=/bar, # example comment', + '', + ] + + expected_clean = [ + ' deny ptrace peer=/bar, # example comment', + '', + ' allow ptrace read,', + ' ptrace read peer=/foo,', + '', + ] + + for rule in rules: + ruleset.add(PtraceRule.parse(rule)) + + self.assertEqual(expected_raw, ruleset.get_raw(1)) + self.assertEqual(expected_clean, ruleset.get_clean(1)) + + +class PtraceGlobTestAATest(AATest): + def setUp(self): + self.maxDiff = None + self.ruleset = PtraceRuleset() + + def test_glob_1(self): + self.assertEqual(self.ruleset.get_glob('ptrace read,'), 'ptrace,') + + # not supported or used yet + # def test_glob_2(self): + # self.assertEqual(self.ruleset.get_glob('ptrace read raw,'), 'ptrace read,') + + def test_glob_ext(self): + with self.assertRaises(NotImplementedError): + # get_glob_ext is not available for ptrace rules + self.ruleset.get_glob_ext('ptrace read peer=/foo,') + +#class PtraceDeleteTestAATest(AATest): +# pass + +setup_all_loops(__name__) +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/utils/test/test-ptrace_parse.py b/utils/test/test-ptrace_parse.py deleted file mode 100644 index d7dea4790..000000000 --- a/utils/test/test-ptrace_parse.py +++ /dev/null @@ -1,35 +0,0 @@ -#! /usr/bin/env python -# ------------------------------------------------------------------ -# -# Copyright (C) 2014 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. -# -# ------------------------------------------------------------------ - -import apparmor.aa as aa -import unittest -from common_test import AAParseTest, setup_regex_tests - -class AAParsePtraceTest(AAParseTest): - def setUp(self): - self.parse_function = aa.parse_ptrace_rule - - tests = [ - ('ptrace,', 'ptrace base keyword rule'), - ('ptrace (readby),', 'ptrace readby rule'), - ('ptrace (trace),', 'ptrace trace rule'), - ('ptrace (trace read),', 'ptrace multi-perm rule'), - ('ptrace r,', 'ptrace r rule'), - ('ptrace w,', 'ptrace w rule'), - ('ptrace rw,', 'ptrace rw rule'), - ('ptrace read peer=foo,', 'ptrace peer rule 1'), - ('ptrace (trace read) peer=/usr/bin/bar,', 'ptrace peer rule 2'), - ('ptrace wr peer=/sbin/baz,', 'ptrace peer rule 3'), - ] - -if __name__ == '__main__': - setup_regex_tests(AAParsePtraceTest) - unittest.main(verbosity=2) diff --git a/utils/test/test-regex_matches.py b/utils/test/test-regex_matches.py index b7fdc9d62..3059ebf19 100644 --- a/utils/test/test-regex_matches.py +++ b/utils/test/test-regex_matches.py @@ -14,7 +14,7 @@ import unittest from common_test import AATest, setup_all_loops from apparmor.common import AppArmorBug, AppArmorException -from apparmor.regex import strip_quotes, parse_profile_start_line, re_match_include, RE_PROFILE_START, RE_PROFILE_CAP +from apparmor.regex import strip_quotes, parse_profile_start_line, re_match_include, RE_PROFILE_START, RE_PROFILE_CAP, RE_PROFILE_PTRACE, RE_PROFILE_SIGNAL class AARegexTest(AATest): @@ -296,21 +296,17 @@ class AARegexSignal(AARegexTest): '''Tests for RE_PROFILE_SIGNAL''' def AASetup(self): - self.regex = aa.RE_PROFILE_SIGNAL + self.regex = RE_PROFILE_SIGNAL tests = [ - (' signal,', (None, None, 'signal,', None)), - (' audit signal,', ('audit', None, 'signal,', None)), - (' signal receive,', (None, None, 'signal receive,', None)), - (' signal (send, receive),', - (None, None, 'signal (send, receive),', None)), - (' audit signal (receive),', - ('audit', None, 'signal (receive),', None)), - (' signal (send, receive) set=(usr1 usr2),', - (None, None, 'signal (send, receive) set=(usr1 usr2),', None)), - (' signal send set=(hup, quit) peer=/usr/sbin/daemon,', - (None, None, - 'signal send set=(hup, quit) peer=/usr/sbin/daemon,', None)), + (' signal,', (None, None, 'signal,', None, None)), + (' audit signal,', ('audit', None, 'signal,', None, None)), + (' signal receive,', (None, None, 'signal receive,', 'receive', None)), + (' signal (send, receive),', (None, None, 'signal (send, receive),', '(send, receive)', None)), + (' audit signal (receive),', ('audit', None, 'signal (receive),', '(receive)', None)), + (' signal (send, receive) set=(usr1 usr2),', (None, None, 'signal (send, receive) set=(usr1 usr2),', '(send, receive) set=(usr1 usr2)', None)), + (' signal send set=(hup, quit) peer=/usr/sbin/daemon,', (None, None, 'signal send set=(hup, quit) peer=/usr/sbin/daemon,', + 'send set=(hup, quit) peer=/usr/sbin/daemon', None)), (' signalling,', False), (' audit signalling,', False), @@ -322,17 +318,16 @@ class AARegexPtrace(AARegexTest): '''Tests for RE_PROFILE_PTRACE''' def AASetup(self): - self.regex = aa.RE_PROFILE_PTRACE + self.regex = RE_PROFILE_PTRACE tests = [ - (' ptrace,', (None, None, 'ptrace,', None)), - (' audit ptrace,', ('audit', None, 'ptrace,', None)), - (' ptrace trace,', (None, None, 'ptrace trace,', None)), - (' ptrace (tracedby, readby),', - (None, None, 'ptrace (tracedby, readby),', None)), - (' audit ptrace (read),', ('audit', None, 'ptrace (read),', None)), - (' ptrace trace peer=/usr/sbin/daemon,', - (None, None, 'ptrace trace peer=/usr/sbin/daemon,', None)), + # audit allow rule rule details comment + (' ptrace,', (None, None, 'ptrace,', None, None)), + (' audit ptrace,', ('audit', None, 'ptrace,', None, None)), + (' ptrace trace,', (None, None, 'ptrace trace,', 'trace', None)), + (' ptrace (tracedby, readby),', (None, None, 'ptrace (tracedby, readby),', '(tracedby, readby)', None)), + (' audit ptrace (read),', ('audit', None, 'ptrace (read),', '(read)', None)), + (' ptrace trace peer=/usr/sbin/daemon,', (None, None, 'ptrace trace peer=/usr/sbin/daemon,', 'trace peer=/usr/sbin/daemon', None)), (' ptraceback,', False), (' audit ptraceback,', False), @@ -417,6 +412,12 @@ class AANamedRegexProfileStart_2(AANamedRegexTest): (' /foo (complain) {', { 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': None }), (' /foo flags=(complain) {', { 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': None }), (' /foo (complain) { # x', { 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': '# x'}), + (' /foo flags = ( complain ){#',{ 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': ' complain ', 'comment': '#'}), + (' @{foo} {', { 'plainprofile': '@{foo}', 'namedprofile': None, 'attachment': None, 'flags': None, 'comment': None }), + (' profile @{foo} {', { 'plainprofile': None, 'namedprofile': '@{foo}', 'attachment': None, 'flags': None, 'comment': None }), + (' profile @{foo} /bar {', { 'plainprofile': None, 'namedprofile': '@{foo}', 'attachment': '/bar', 'flags': None, 'comment': None }), + (' profile foo @{bar} {', { 'plainprofile': None, 'namedprofile': 'foo', 'attachment': '@{bar}', 'flags': None, 'comment': None }), + (' profile @{foo} @{bar} {', { 'plainprofile': None, 'namedprofile': '@{foo}', 'attachment': '@{bar}', 'flags': None, 'comment': None }), (' /foo {', { 'plainprofile': '/foo', 'namedprofile': None, 'leadingspace': ' ' }), ('/foo {', { 'plainprofile': '/foo', 'namedprofile': None, 'leadingspace': '' }), @@ -437,12 +438,18 @@ class Test_parse_profile_start_line(AATest): (' profile "foo bar" /foo {', { 'profile': 'foo bar', 'profile_keyword': True, 'plainprofile': None, 'namedprofile': 'foo bar','attachment': '/foo', 'flags': None, 'comment': None }), (' /foo (complain) {', { 'profile': '/foo', 'profile_keyword': False, 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': None }), (' /foo flags=(complain) {', { 'profile': '/foo', 'profile_keyword': False, 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': None }), + (' /foo flags = ( complain ){', { 'profile': '/foo', 'profile_keyword': False, 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': ' complain ', 'comment': None }), (' /foo (complain) { # x', { 'profile': '/foo', 'profile_keyword': False, 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': '# x'}), (' /foo {', { 'profile': '/foo', 'plainprofile': '/foo', 'namedprofile': None, 'leadingspace': ' ' }), ('/foo {', { 'profile': '/foo', 'plainprofile': '/foo', 'namedprofile': None, 'leadingspace': None }), (' profile foo {', { 'profile': 'foo', 'plainprofile': None, 'namedprofile': 'foo', 'leadingspace': ' ' }), ('profile foo {', { 'profile': 'foo', 'plainprofile': None, 'namedprofile': 'foo', 'leadingspace': None }), + (' @{foo} {', { 'profile': '@{foo}', 'plainprofile': '@{foo}', 'namedprofile': None, 'attachment': None, 'flags': None, 'comment': None }), + (' profile @{foo} {', { 'profile': '@{foo}', 'plainprofile': None, 'namedprofile': '@{foo}', 'attachment': None, 'flags': None, 'comment': None }), + (' profile @{foo} /bar {', { 'profile': '@{foo}', 'plainprofile': None, 'namedprofile': '@{foo}', 'attachment': '/bar', 'flags': None, 'comment': None }), + (' profile foo @{bar} {', { 'profile': 'foo', 'plainprofile': None, 'namedprofile': 'foo', 'attachment': '@{bar}', 'flags': None, 'comment': None }), + (' profile @{foo} @{bar} {', { 'profile': '@{foo}', 'plainprofile': None, 'namedprofile': '@{foo}', 'attachment': '@{bar}', 'flags': None, 'comment': None }), ] def _run_test(self, line, expected): diff --git a/utils/test/test-rlimit.py b/utils/test/test-rlimit.py index 8cda5403f..86d9cf641 100644 --- a/utils/test/test-rlimit.py +++ b/utils/test/test-rlimit.py @@ -43,10 +43,13 @@ class RlimitTestParse(RlimitTest): tests = [ # rawrule audit allow deny comment rlimit value all/infinity? ('set rlimit as <= 2047MB,' , exp(False, False, False, '' , 'as' , '2047MB' , False)), + ('set rlimit as <= 2047 MB,' , exp(False, False, False, '' , 'as' , '2047 MB' , False)), ('set rlimit cpu <= 1024,' , exp(False, False, False, '' , 'cpu' , '1024' , False)), ('set rlimit stack <= 1024GB,' , exp(False, False, False, '' , 'stack' , '1024GB' , False)), + ('set rlimit stack <= 1024 GB,' , exp(False, False, False, '' , 'stack' , '1024 GB' , False)), ('set rlimit rtprio <= 10, # comment' , exp(False, False, False, ' # comment' , 'rtprio' , '10' , False)), ('set rlimit core <= 44444KB,' , exp(False, False, False, '' , 'core' , '44444KB' , False)), + ('set rlimit core <= 44444 KB,' , exp(False, False, False, '' , 'core' , '44444 KB' , False)), ('set rlimit rttime <= 60ms,' , exp(False, False, False, '' , 'rttime' , '60ms' , False)), ('set rlimit cpu <= infinity,' , exp(False, False, False, '' , 'cpu' , None , True )), ('set rlimit nofile <= 256,' , exp(False, False, False, '' , 'nofile' , '256' , False)), @@ -117,6 +120,7 @@ class RlimitFromInit(RlimitTest): tests = [ # RlimitRule object audit allow deny comment rlimit value all/infinity? (RlimitRule('as', '2047MB') , exp(False, False, False, '' , 'as' , '2047MB' , False)), + (RlimitRule('as', '2047 MB') , exp(False, False, False, '' , 'as' , '2047 MB' , False)), (RlimitRule('cpu', '1024') , exp(False, False, False, '' , 'cpu' , '1024' , False)), (RlimitRule('rttime', '60minutes') , exp(False, False, False, '' , 'rttime' , '60minutes', False)), (RlimitRule('nice', '-10') , exp(False, False, False, '' , 'nice' , '-10' , False)), @@ -254,9 +258,11 @@ class RlimitCoveredTest_01(RlimitCoveredTest): ('set rlimit cpu <= 150seconds,', [ True , False , True , True ]), ('set rlimit cpu <= 300seconds,', [ False , False , False , False ]), ('set rlimit cpu <= 1minutes,' , [ False , False , True , True ]), - ('set rlimit cpu <= 1m,' , [ False , False , True , True ]), + ('set rlimit cpu <= 1min,' , [ False , False , True , True ]), ('set rlimit cpu <= 3minutes,' , [ False , False , False , False ]), ('set rlimit cpu <= 1hour,' , [ False , False , False , False ]), + ('set rlimit cpu <= 2 days,' , [ False , False , False , False ]), + ('set rlimit cpu <= 1 week,' , [ False , False , False , False ]), ] class RlimitCoveredTest_02(RlimitCoveredTest): @@ -270,7 +276,9 @@ class RlimitCoveredTest_02(RlimitCoveredTest): ('set rlimit data <= 4194304,' , [ True , False , True , True ]), ('set rlimit data <= 4096KB,' , [ True , False , True , True ]), ('set rlimit data <= 4MB,' , [ True , True , True , True ]), + ('set rlimit data <= 4 MB,' , [ True , False , True , True ]), ('set rlimit data <= 6MB,' , [ False , False , False , False ]), + ('set rlimit data <= 6 MB,' , [ False , False , False , False ]), ('set rlimit data <= 1GB,' , [ False , False , False , False ]), ] @@ -403,7 +411,7 @@ class RlimitGlobTestAATest(AATest): # self.assertEqual(self.ruleset.get_glob('rlimit /foo -> /bar,'), 'rlimit -> /bar,') def test_glob_ext(self): - with self.assertRaises(AppArmorBug): + with self.assertRaises(NotImplementedError): # get_glob_ext is not available for rlimit rules self.ruleset.get_glob_ext('set rlimit cpu <= 100,') @@ -418,6 +426,7 @@ class RlimitDeleteTestAATest(AATest): class RlimitSplit_unitTest(AATest): tests = [ ('40MB' , ( 40, 'MB',)), + ('40 MB' , ( 40, 'MB',)), ('40' , ( 40, '', )), ] @@ -451,16 +460,21 @@ class RlimitTime_to_intTest(AATest): self.obj = RlimitRule('cpu', '1') tests = [ + ('40' , 0.00004), ('30us' , 0.00003), ('40ms' , 0.04), - ('40' , 40), ('40seconds', 40), ('2minutes' , 2*60), ('2hours' , 2*60*60), + ('1 day' , 1*60*60*24), + ('2 weeks' , 2*60*60*24*7), ] def _run_test(self, params, expected): - self.assertEqual(self.obj.time_to_int(params, 'seconds'), expected) + self.assertEqual(self.obj.time_to_int(params, 'us'), expected) + + def test_with_seconds_as_default(self): + self.assertEqual(self.obj.time_to_int('40', 'seconds'), 40) def test_with_ms_as_default(self): self.assertEqual(self.obj.time_to_int('40', 'ms'), 0.04) diff --git a/utils/test/test-signal.py b/utils/test/test-signal.py new file mode 100644 index 000000000..0fd3f71eb --- /dev/null +++ b/utils/test/test-signal.py @@ -0,0 +1,586 @@ +#!/usr/bin/env python +# ---------------------------------------------------------------------- +# Copyright (C) 2015 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 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 unittest +from collections import namedtuple +from common_test import AATest, setup_all_loops + +from apparmor.rule.signal import SignalRule, SignalRuleset +from apparmor.rule import BaseRule +from apparmor.common import AppArmorException, AppArmorBug +from apparmor.logparser import ReadLog +from apparmor.translations import init_translation +_ = init_translation() + +exp = namedtuple('exp', ['audit', 'allow_keyword', 'deny', 'comment', + 'access', 'all_access', 'signal', 'all_signals', 'peer', 'all_peers']) + +# --- tests for single SignalRule --- # + +class SignalTest(AATest): + def _compare_obj(self, obj, expected): + self.assertEqual(expected.allow_keyword, obj.allow_keyword) + self.assertEqual(expected.audit, obj.audit) + self.assertEqual(expected.access, obj.access) + self.assertEqual(expected.signal, obj.signal) + if obj.peer: + self.assertEqual(expected.peer, obj.peer.regex) + else: + self.assertEqual(expected.peer, obj.peer) + self.assertEqual(expected.all_access, obj.all_access) + self.assertEqual(expected.all_signals, obj.all_signals) + self.assertEqual(expected.all_peers, obj.all_peers) + self.assertEqual(expected.deny, obj.deny) + self.assertEqual(expected.comment, obj.comment) + +class SignalTestParse(SignalTest): + tests = [ + # SignalRule object audit allow deny comment access all? signal all? peer all? + ('signal,' , exp(False, False, False, '', None , True , None, True, None, True )), + ('signal send,' , exp(False, False, False, '', {'send'}, False, None, True, None, True )), + ('signal (send, receive),' , exp(False, False, False, '', {'send', 'receive'}, False, None, True, None, True )), + ('signal send set=quit,' , exp(False, False, False, '', {'send'}, False, {'quit'}, False, None, True )), + ('deny signal send set=quit, # cmt' , exp(False, False, True , ' # cmt', {'send'}, False, {'quit'}, False, None, True )), + ('audit allow signal set=int,' , exp(True , True , False, '', None , True , {'int'}, False, None, True )), + ('signal set=quit peer=unconfined,' , exp(False, False, False, '', None , True , {'quit'}, False, 'unconfined', False )), + ('signal send set=(quit),' , exp(False, False, False, '', {'send'}, False, {'quit'}, False, None, True )), + ('signal send set=(quit, int),' , exp(False, False, False, '', {'send'}, False, {'quit', 'int'}, False, None, True )), + ('signal set=(quit, int),' , exp(False, False, False, '', None, True, {'quit', 'int'}, False, None, True )), + ('signal send set = ( quit , int ) ,' , exp(False, False, False, '', {'send'}, False, {'quit', 'int'}, False, None, True )), + ('signal peer=/foo,' , exp(False, False, False, '', None , True , None, True, '/foo', False )), + ('signal r set=quit set=int peer=/foo,' , exp(False, False, False, '', {'r'}, False, {'quit', 'int'}, False, '/foo', False )), + ] + + def _run_test(self, rawrule, expected): + self.assertTrue(SignalRule.match(rawrule)) + obj = SignalRule.parse(rawrule) + self.assertEqual(rawrule.strip(), obj.raw_rule) + self._compare_obj(obj, expected) + +class SignalTestParseInvalid(SignalTest): + tests = [ + ('signal foo,' , AppArmorException), + ('signal foo bar,' , AppArmorException), + ('signal foo int,' , AppArmorException), + ('signal send bar,' , AppArmorException), + ('signal send receive,' , AppArmorException), + ('signal set=,' , AppArmorException), + ('signal set=int set=,' , AppArmorException), + ('signal set=invalid,' , AppArmorException), + ('signal peer=,' , AppArmorException), + ] + + def _run_test(self, rawrule, expected): + self.assertTrue(SignalRule.match(rawrule)) # the above invalid rules still match the main regex! + with self.assertRaises(expected): + SignalRule.parse(rawrule) + +class SignalTestParseFromLog(SignalTest): + def test_signal_from_log(self): + parser = ReadLog('', '', '', '', '') + event = 'type=AVC msg=audit(1409438250.564:201): apparmor="DENIED" operation="signal" profile="/usr/bin/pulseaudio" pid=2531 comm="pulseaudio" requested_mask="send" denied_mask="send" signal=term peer="/usr/bin/pulseaudio///usr/lib/pulseaudio/pulse/gconf-helper"' + + parsed_event = parser.parse_event(event) + + self.assertEqual(parsed_event, { + 'request_mask': 'send', + 'denied_mask': 'send', + 'error_code': 0, + 'magic_token': 0, + 'parent': 0, + 'profile': '/usr/bin/pulseaudio', + 'signal': 'term', + 'peer': '/usr/bin/pulseaudio///usr/lib/pulseaudio/pulse/gconf-helper', + 'operation': 'signal', + 'resource': None, + 'info': None, + 'aamode': 'REJECTING', + 'time': 1409438250, + 'active_hat': None, + 'pid': 2531, + 'task': 0, + 'attr': None, + 'name2': None, + 'name': None, + }) + + obj = SignalRule(parsed_event['denied_mask'], parsed_event['signal'], parsed_event['peer'], log_event=parsed_event) + + # audit allow deny comment access all? signal all? peer all? + expected = exp(False, False, False, '', {'send'}, False, {'term'}, False, '/usr/bin/pulseaudio///usr/lib/pulseaudio/pulse/gconf-helper', False) + + self._compare_obj(obj, expected) + + self.assertEqual(obj.get_raw(1), ' signal send set=term peer=/usr/bin/pulseaudio///usr/lib/pulseaudio/pulse/gconf-helper,') + +class SignalFromInit(SignalTest): + tests = [ + # SignalRule object audit allow deny comment access all? signal all? peer all? + (SignalRule('r', 'hup', 'unconfined', deny=True) , exp(False, False, True , '' , {'r'}, False, {'hup'}, False, 'unconfined', False)), + (SignalRule(('r', 'send'), ('hup', 'int'), '/bin/foo') , exp(False, False, False, '' , {'r', 'send'},False, {'hup', 'int'}, False, '/bin/foo', False)), + (SignalRule(SignalRule.ALL, 'int', '/bin/foo') , exp(False, False, False, '' , None, True, {'int'}, False, '/bin/foo', False )), + (SignalRule('rw', SignalRule.ALL, '/bin/foo') , exp(False, False, False, '' , {'rw'}, False, None, True, '/bin/foo', False )), + (SignalRule('rw', ('int'), SignalRule.ALL) , exp(False, False, False, '' , {'rw'}, False, {'int'}, False, None, True )), + (SignalRule(SignalRule.ALL, SignalRule.ALL, SignalRule.ALL) , exp(False, False, False, '' , None , True, None, True, None, True )), + ] + + def _run_test(self, obj, expected): + self._compare_obj(obj, expected) + +class InvalidSignalInit(AATest): + tests = [ + # init params expected exception + (['send', '' , '/foo' ] , AppArmorBug), # empty signal + (['' , 'int' , '/foo' ] , AppArmorBug), # empty access + (['send', 'int' , '' ] , AppArmorBug), # empty peer + ([' ', 'int' , '/foo' ] , AppArmorBug), # whitespace access + (['send', ' ' , '/foo' ] , AppArmorBug), # whitespace signal + (['send', 'int' , ' ' ] , AppArmorBug), # whitespace peer + (['xyxy', 'int' , '/foo' ] , AppArmorException), # invalid access + (['send', 'xyxy', '/foo' ] , AppArmorException), # invalid signal + # XXX is 'invalid peer' possible at all? + ([dict(), 'int' , '/foo' ] , AppArmorBug), # wrong type for access + ([None , 'int' , '/foo' ] , AppArmorBug), # wrong type for access + (['send', dict(), '/foo' ] , AppArmorBug), # wrong type for signal + (['send', None , '/foo' ] , AppArmorBug), # wrong type for signal + (['send', 'int' , dict() ] , AppArmorBug), # wrong type for peer + (['send', 'int' , None ] , AppArmorBug), # wrong type for peer + ] + + def _run_test(self, params, expected): + with self.assertRaises(expected): + SignalRule(params[0], params[1], params[2]) + + def test_missing_params_1(self): + with self.assertRaises(TypeError): + SignalRule() + + def test_missing_params_2(self): + with self.assertRaises(TypeError): + SignalRule('r') + + def test_missing_params_3(self): + with self.assertRaises(TypeError): + SignalRule('r', 'int') + +class InvalidSignalTest(AATest): + def _check_invalid_rawrule(self, rawrule): + obj = None + self.assertFalse(SignalRule.match(rawrule)) + with self.assertRaises(AppArmorException): + obj = SignalRule(SignalRule.parse(rawrule)) + + self.assertIsNone(obj, 'SignalRule handed back an object unexpectedly') + + def test_invalid_signal_missing_comma(self): + self._check_invalid_rawrule('signal') # missing comma + + def test_invalid_non_SignalRule(self): + self._check_invalid_rawrule('dbus,') # not a signal rule + + def test_empty_data_1(self): + obj = SignalRule('send', 'quit', '/foo') + obj.access = '' + # no access set, and ALL not set + with self.assertRaises(AppArmorBug): + obj.get_clean(1) + + def test_empty_data_2(self): + obj = SignalRule('send', 'quit', '/foo') + obj.signal = '' + # no signal set, and ALL not set + with self.assertRaises(AppArmorBug): + obj.get_clean(1) + + def test_empty_data_3(self): + obj = SignalRule('send', 'quit', '/foo') + obj.peer = '' + # no signal set, and ALL not set + with self.assertRaises(AppArmorBug): + obj.get_clean(1) + + +class WriteSignalTestAATest(AATest): + def _run_test(self, rawrule, expected): + self.assertTrue(SignalRule.match(rawrule)) + obj = SignalRule.parse(rawrule) + clean = obj.get_clean() + raw = obj.get_raw() + + self.assertEqual(expected.strip(), clean, 'unexpected clean rule') + self.assertEqual(rawrule.strip(), raw, 'unexpected raw rule') + + tests = [ + # raw rule clean rule + (' signal , # foo ' , 'signal, # foo'), + (' audit signal send,' , 'audit signal send,'), + (' audit signal (send ),' , 'audit signal send,'), + (' audit signal (send , receive ),' , 'audit signal (receive send),'), + (' deny signal send set=quit,# foo bar' , 'deny signal send set=quit, # foo bar'), + (' deny signal send set=(quit), ' , 'deny signal send set=quit,'), + (' deny signal send set=(int , quit),' , 'deny signal send set=(int quit),'), + (' deny signal send set=(quit, int ),' , 'deny signal send set=(int quit),'), + (' deny signal send ,# foo bar' , 'deny signal send, # foo bar'), + (' allow signal set=int ,# foo bar' , 'allow signal set=int, # foo bar'), + ('signal,' , 'signal,'), + ('signal (receive),' , 'signal receive,'), + ('signal (send),' , 'signal send,'), + ('signal (send receive),' , 'signal (receive send),'), + ('signal r,' , 'signal r,'), + ('signal w,' , 'signal w,'), + ('signal rw,' , 'signal rw,'), + ('signal send set=("hup"),' , 'signal send set=hup,'), + ('signal (receive) set=kill,' , 'signal receive set=kill,'), + ('signal w set=(quit int),' , 'signal w set=(int quit),'), + ('signal receive peer=foo,' , 'signal receive peer=foo,'), + ('signal (send receive) peer=/usr/bin/bar,' , 'signal (receive send) peer=/usr/bin/bar,'), + ('signal wr set=(pipe, usr1) peer=/sbin/baz,' , 'signal wr set=(pipe usr1) peer=/sbin/baz,'), + ] + + def test_write_manually(self): + obj = SignalRule('send', 'quit', '/foo', allow_keyword=True) + + expected = ' allow signal send set=quit peer=/foo,' + + self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule') + self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule') + + +class SignalCoveredTest(AATest): + def _run_test(self, param, expected): + obj = SignalRule.parse(self.rule) + check_obj = SignalRule.parse(param) + + self.assertTrue(SignalRule.match(param)) + + self.assertEqual(obj.is_equal(check_obj), expected[0], 'Mismatch in is_equal, expected %s' % expected[0]) + self.assertEqual(obj.is_equal(check_obj, True), expected[1], 'Mismatch in is_equal/strict, expected %s' % expected[1]) + + self.assertEqual(obj.is_covered(check_obj), expected[2], 'Mismatch in is_covered, expected %s' % expected[2]) + self.assertEqual(obj.is_covered(check_obj, True, True), expected[3], 'Mismatch in is_covered/exact, expected %s' % expected[3]) + +class SignalCoveredTest_01(SignalCoveredTest): + rule = 'signal send,' + + tests = [ + # rule equal strict equal covered covered exact + ('signal,' , [ False , False , False , False ]), + ('signal send,' , [ True , True , True , True ]), + ('signal send peer=unconfined,' , [ False , False , True , True ]), + ('signal send, # comment' , [ True , False , True , True ]), + ('allow signal send,' , [ True , False , True , True ]), + ('signal send,' , [ True , False , True , True ]), + ('signal send set=quit,' , [ False , False , True , True ]), + ('signal send set=int,' , [ False , False , True , True ]), + ('audit signal send,' , [ False , False , False , False ]), + ('audit signal,' , [ False , False , False , False ]), + ('signal receive,' , [ False , False , False , False ]), + ('signal set=int,' , [ False , False , False , False ]), + ('audit deny signal send,' , [ False , False , False , False ]), + ('deny signal send,' , [ False , False , False , False ]), + ] + +class SignalCoveredTest_02(SignalCoveredTest): + rule = 'audit signal send,' + + tests = [ + # rule equal strict equal covered covered exact + ( 'signal send,' , [ False , False , True , False ]), + ('audit signal send,' , [ True , True , True , True ]), + ( 'signal send set=quit,' , [ False , False , True , False ]), + ('audit signal send set=quit,' , [ False , False , True , True ]), + ( 'signal,' , [ False , False , False , False ]), + ('audit signal,' , [ False , False , False , False ]), + ('signal receive,' , [ False , False , False , False ]), + ] + +class SignalCoveredTest_03(SignalCoveredTest): + rule = 'signal send set=quit,' + + tests = [ + # rule equal strict equal covered covered exact + ( 'signal send set=quit,' , [ True , True , True , True ]), + ('allow signal send set=quit,' , [ True , False , True , True ]), + ( 'signal send,' , [ False , False , False , False ]), + ( 'signal,' , [ False , False , False , False ]), + ( 'signal send set=int,' , [ False , False , False , False ]), + ('audit signal,' , [ False , False , False , False ]), + ('audit signal send set=quit,' , [ False , False , False , False ]), + ('audit signal set=quit,' , [ False , False , False , False ]), + ( 'signal send,' , [ False , False , False , False ]), + ( 'signal,' , [ False , False , False , False ]), + ] + +class SignalCoveredTest_04(SignalCoveredTest): + rule = 'signal,' + + tests = [ + # rule equal strict equal covered covered exact + ( 'signal,' , [ True , True , True , True ]), + ('allow signal,' , [ True , False , True , True ]), + ( 'signal send,' , [ False , False , True , True ]), + ( 'signal w set=quit,' , [ False , False , True , True ]), + ( 'signal set=int,' , [ False , False , True , True ]), + ( 'signal send set=quit,' , [ False , False , True , True ]), + ('audit signal,' , [ False , False , False , False ]), + ('deny signal,' , [ False , False , False , False ]), + ] + +class SignalCoveredTest_05(SignalCoveredTest): + rule = 'deny signal send,' + + tests = [ + # rule equal strict equal covered covered exact + ( 'deny signal send,' , [ True , True , True , True ]), + ('audit deny signal send,' , [ False , False , False , False ]), + ( 'signal send,' , [ False , False , False , False ]), # XXX should covered be true here? + ( 'deny signal receive,' , [ False , False , False , False ]), + ( 'deny signal,' , [ False , False , False , False ]), + ] + +class SignalCoveredTest_06(SignalCoveredTest): + rule = 'signal send peer=unconfined,' + + tests = [ + # rule equal strict equal covered covered exact + ('signal,' , [ False , False , False , False ]), + ('signal send,' , [ False , False , False , False ]), + ('signal send peer=unconfined,' , [ True , True , True , True ]), + ('signal peer=unconfined,' , [ False , False , False , False ]), + ('signal send, # comment' , [ False , False , False , False ]), + ('allow signal send,' , [ False , False , False , False ]), + ('allow signal send peer=unconfined,' , [ True , False , True , True ]), + ('allow signal send peer=/foo/bar,' , [ False , False , False , False ]), + ('allow signal send peer=/**,' , [ False , False , False , False ]), + ('allow signal send peer=**,' , [ False , False , False , False ]), + ('signal send,' , [ False , False , False , False ]), + ('signal send peer=unconfined,' , [ True , False , True , True ]), + ('signal send set=quit,' , [ False , False , False , False ]), + ('signal send set=int peer=unconfined,',[ False , False , True , True ]), + ('audit signal send peer=unconfined,' , [ False , False , False , False ]), + ('audit signal,' , [ False , False , False , False ]), + ('signal receive,' , [ False , False , False , False ]), + ('signal set=int,' , [ False , False , False , False ]), + ('audit deny signal send,' , [ False , False , False , False ]), + ('deny signal send,' , [ False , False , False , False ]), + ] + +class SignalCoveredTest_07(SignalCoveredTest): + rule = 'signal send peer=/foo/bar,' + + tests = [ + # rule equal strict equal covered covered exact + ('signal,' , [ False , False , False , False ]), + ('signal send,' , [ False , False , False , False ]), + ('signal send peer=/foo/bar,' , [ True , True , True , True ]), + ('signal send peer=/foo/*,' , [ False , False , False , False ]), + ('signal send peer=/**,' , [ False , False , False , False ]), + ('signal send peer=/what/*,' , [ False , False , False , False ]), + ('signal peer=/foo/bar,' , [ False , False , False , False ]), + ('signal send, # comment' , [ False , False , False , False ]), + ('allow signal send,' , [ False , False , False , False ]), + ('allow signal send peer=/foo/bar,' , [ True , False , True , True ]), + ('signal send,' , [ False , False , False , False ]), + ('signal send peer=/foo/bar,' , [ True , False , True , True ]), + ('signal send peer=/what/ever,' , [ False , False , False , False ]), + ('signal send set=quit,' , [ False , False , False , False ]), + ('signal send set=int peer=/foo/bar,' , [ False , False , True , True ]), + ('audit signal send peer=/foo/bar,' , [ False , False , False , False ]), + ('audit signal,' , [ False , False , False , False ]), + ('signal receive,' , [ False , False , False , False ]), + ('signal set=int,' , [ False , False , False , False ]), + ('audit deny signal send,' , [ False , False , False , False ]), + ('deny signal send,' , [ False , False , False , False ]), + ] + +class SignalCoveredTest_08(SignalCoveredTest): + rule = 'signal send peer=**,' + + tests = [ + # rule equal strict equal covered covered exact + ('signal,' , [ False , False , False , False ]), + ('signal send,' , [ False , False , False , False ]), + ('signal send peer=/foo/bar,' , [ False , False , True , True ]), + ('signal send peer=/foo/*,' , [ False , False , True , True ]), + ('signal send peer=/**,' , [ False , False , True , True ]), + ('signal send peer=/what/*,' , [ False , False , True , True ]), + ('signal peer=/foo/bar,' , [ False , False , False , False ]), + ('signal send, # comment' , [ False , False , False , False ]), + ('allow signal send,' , [ False , False , False , False ]), + ('allow signal send peer=/foo/bar,' , [ False , False , True , True ]), + ('signal send,' , [ False , False , False , False ]), + ('signal send peer=/foo/bar,' , [ False , False , True , True ]), + ('signal send peer=/what/ever,' , [ False , False , True , True ]), + ('signal send set=quit,' , [ False , False , False , False ]), + ('signal send set=int peer=/foo/bar,' , [ False , False , True , True ]), + ('audit signal send peer=/foo/bar,' , [ False , False , False , False ]), + ('audit signal,' , [ False , False , False , False ]), + ('signal receive,' , [ False , False , False , False ]), + ('signal set=int,' , [ False , False , False , False ]), + ('audit deny signal send,' , [ False , False , False , False ]), + ('deny signal send,' , [ False , False , False , False ]), + ] + +class SignalCoveredTest_Invalid(AATest): + def test_borked_obj_is_covered_1(self): + obj = SignalRule.parse('signal send peer=/foo,') + + testobj = SignalRule('send', 'quit', '/foo') + testobj.access = '' + + with self.assertRaises(AppArmorBug): + obj.is_covered(testobj) + + def test_borked_obj_is_covered_2(self): + obj = SignalRule.parse('signal send set=quit peer=/foo,') + + testobj = SignalRule('send', 'quit', '/foo') + testobj.signal = '' + + with self.assertRaises(AppArmorBug): + obj.is_covered(testobj) + + def test_borked_obj_is_covered_3(self): + obj = SignalRule.parse('signal send set=quit peer=/foo,') + + testobj = SignalRule('send', 'quit', '/foo') + testobj.peer = '' + + with self.assertRaises(AppArmorBug): + obj.is_covered(testobj) + + def test_invalid_is_covered(self): + obj = SignalRule.parse('signal send,') + + testobj = BaseRule() # different type + + with self.assertRaises(AppArmorBug): + obj.is_covered(testobj) + + def test_invalid_is_equal(self): + obj = SignalRule.parse('signal send,') + + testobj = BaseRule() # different type + + with self.assertRaises(AppArmorBug): + obj.is_equal(testobj) + +class SignalLogprofHeaderTest(AATest): + tests = [ + ('signal,', [ _('Access mode'), _('ALL'), _('Signal'), _('ALL'), _('Peer'), _('ALL'), ]), + ('signal send,', [ _('Access mode'), 'send', _('Signal'), _('ALL'), _('Peer'), _('ALL'), ]), + ('signal send set=quit,', [ _('Access mode'), 'send', _('Signal'), 'quit', _('Peer'), _('ALL'), ]), + ('deny signal,', [_('Qualifier'), 'deny', _('Access mode'), _('ALL'), _('Signal'), _('ALL'), _('Peer'), _('ALL'), ]), + ('allow signal send,', [_('Qualifier'), 'allow', _('Access mode'), 'send', _('Signal'), _('ALL'), _('Peer'), _('ALL'), ]), + ('audit signal send set=quit,', [_('Qualifier'), 'audit', _('Access mode'), 'send', _('Signal'), 'quit', _('Peer'), _('ALL'), ]), + ('audit deny signal send,', [_('Qualifier'), 'audit deny', _('Access mode'), 'send', _('Signal'), _('ALL'), _('Peer'), _('ALL'), ]), + ('signal set=(int, quit),', [ _('Access mode'), _('ALL'), _('Signal'), 'int quit', _('Peer'), _('ALL'), ]), + ('signal set=( quit, int),', [ _('Access mode'), _('ALL'), _('Signal'), 'int quit', _('Peer'), _('ALL'), ]), + ('signal (send, receive) set=( quit, int) peer=/foo,', [ _('Access mode'), 'receive send', _('Signal'), 'int quit', _('Peer'), '/foo', ]), + ] + + def _run_test(self, params, expected): + obj = SignalRule._parse(params) + self.assertEqual(obj.logprof_header(), expected) + +## --- tests for SignalRuleset --- # + +class SignalRulesTest(AATest): + def test_empty_ruleset(self): + ruleset = SignalRuleset() + ruleset_2 = SignalRuleset() + self.assertEqual([], ruleset.get_raw(2)) + self.assertEqual([], ruleset.get_clean(2)) + self.assertEqual([], ruleset_2.get_raw(2)) + self.assertEqual([], ruleset_2.get_clean(2)) + + def test_ruleset_1(self): + ruleset = SignalRuleset() + rules = [ + 'signal set=int,', + 'signal send,', + ] + + expected_raw = [ + 'signal set=int,', + 'signal send,', + '', + ] + + expected_clean = [ + 'signal send,', + 'signal set=int,', + '', + ] + + for rule in rules: + ruleset.add(SignalRule.parse(rule)) + + self.assertEqual(expected_raw, ruleset.get_raw()) + self.assertEqual(expected_clean, ruleset.get_clean()) + + def test_ruleset_2(self): + ruleset = SignalRuleset() + rules = [ + 'signal send set=int,', + 'allow signal send,', + 'deny signal set=quit, # example comment', + ] + + expected_raw = [ + ' signal send set=int,', + ' allow signal send,', + ' deny signal set=quit, # example comment', + '', + ] + + expected_clean = [ + ' deny signal set=quit, # example comment', + '', + ' allow signal send,', + ' signal send set=int,', + '', + ] + + for rule in rules: + ruleset.add(SignalRule.parse(rule)) + + self.assertEqual(expected_raw, ruleset.get_raw(1)) + self.assertEqual(expected_clean, ruleset.get_clean(1)) + + +class SignalGlobTestAATest(AATest): + def setUp(self): + self.maxDiff = None + self.ruleset = SignalRuleset() + + def test_glob_1(self): + self.assertEqual(self.ruleset.get_glob('signal send,'), 'signal,') + + # not supported or used yet + # def test_glob_2(self): + # self.assertEqual(self.ruleset.get_glob('signal send raw,'), 'signal send,') + + def test_glob_ext(self): + with self.assertRaises(NotImplementedError): + # get_glob_ext is not available for signal rules + self.ruleset.get_glob_ext('signal send set=int,') + +#class SignalDeleteTestAATest(AATest): +# pass + +setup_all_loops(__name__) +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/utils/test/test-signal_parse.py b/utils/test/test-signal_parse.py deleted file mode 100644 index b9d46e557..000000000 --- a/utils/test/test-signal_parse.py +++ /dev/null @@ -1,38 +0,0 @@ -#! /usr/bin/env python -# ------------------------------------------------------------------ -# -# Copyright (C) 2014 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. -# -# ------------------------------------------------------------------ - -import apparmor.aa as aa -import unittest -from common_test import AAParseTest, setup_regex_tests - -class AAParseSignalTest(AAParseTest): - def setUp(self): - self.parse_function = aa.parse_signal_rule - - tests = [ - ('signal,', 'signal base keyword rule'), - ('signal (receive),', 'signal receive rule'), - ('signal (send),', 'signal send rule'), - ('signal (send receive),', 'signal multiple perms rule'), - ('signal r,', 'signal r rule'), - ('signal w,', 'signal w rule'), - ('signal rw,', 'signal rw rule'), - ('signal send set=("hup"),', 'signal set rule 1'), - ('signal (receive) set=kill,', 'signal set rule 2'), - ('signal w set=(quit int),', 'signal set rule 3'), - ('signal receive peer=foo,', 'signal peer rule 1'), - ('signal (send receive) peer=/usr/bin/bar,', 'signal peer rule 2'), - ('signal wr set=(pipe, usr1) peer=/sbin/baz,', 'signal peer rule 3'), - ] - -if __name__ == '__main__': - setup_regex_tests(AAParseSignalTest) - unittest.main(verbosity=2)