diff --git a/changehat/mod_apparmor/mod_apparmor.c b/changehat/mod_apparmor/mod_apparmor.c index a91314456..e6d283233 100644 --- a/changehat/mod_apparmor/mod_apparmor.c +++ b/changehat/mod_apparmor/mod_apparmor.c @@ -17,6 +17,7 @@ #include "http_config.h" #include "http_request.h" #include "http_log.h" +#include "http_main.h" #include "http_protocol.h" #include "util_filter.h" #include "apr.h" @@ -35,9 +36,18 @@ #define DEFAULT_HAT "HANDLING_UNTRUSTED_INPUT" #define DEFAULT_URI_HAT "DEFAULT_URI" +/* Compatibility with apache 2.2 */ +#if AP_SERVER_MAJORVERSION_NUMBER == 2 && AP_SERVER_MINORVERSION_NUMBER < 3 + #define APLOG_TRACE1 APLOG_DEBUG + server_rec *ap_server_conf = NULL; +#endif + +#ifdef APLOG_USE_MODULE + APLOG_USE_MODULE(apparmor); +#endif module AP_MODULE_DECLARE_DATA apparmor_module; -static unsigned int magic_token = 0; +static unsigned long magic_token = 0; static int inside_default_hat = 0; typedef struct { @@ -68,9 +78,10 @@ immunix_init (apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) apr_file_read (file, (void *) &magic_token, &size); apr_file_close (file); } else { - ap_log_error (APLOG_MARK, APLOG_ERR, 0, NULL, "Failed to open /dev/urandom"); + ap_log_error(APLOG_MARK, APLOG_ERR, errno, ap_server_conf, + "Failed to open /dev/urandom"); } - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "Opened /dev/urandom successfully"); + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf, "Opened /dev/urandom successfully"); return OK; } @@ -83,35 +94,31 @@ immunix_child_init (apr_pool_t *p, server_rec *s) { int ret; - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "init: calling change_hat"); - ret = change_hat (DEFAULT_HAT, magic_token); + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf, + "init: calling change_hat with '%s'", DEFAULT_HAT); + ret = aa_change_hat(DEFAULT_HAT, magic_token); if (ret < 0) { - change_hat (NULL, magic_token); - ap_log_error (APLOG_MARK, APLOG_ERR, 0, NULL, "Failed to change_hat to '%s'", - DEFAULT_HAT); + ap_log_error(APLOG_MARK, APLOG_ERR, errno, ap_server_conf, + "Failed to change_hat to '%s'", DEFAULT_HAT); } else { inside_default_hat = 1; } } -#ifdef DEBUG static void -debug_dump_uri (apr_uri_t * uri) +debug_dump_uri(request_rec *r) { - if (uri) - ap_log_error (APLOG_MARK, APLOG_ERR, 0, NULL, "Dumping uri info " + apr_uri_t *uri = &r->parsed_uri; + if (uri) + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "Dumping uri info " "scheme='%s' host='%s' path='%s' query='%s' fragment='%s'", uri->scheme, uri->hostname, uri->path, uri->query, uri->fragment); else - ap_log_error (APLOG_MARK, APLOG_ERR, 0, NULL, "Asked to dump NULL uri"); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "Asked to dump NULL uri"); } -#else -static void -debug_dump_uri (apr_uri_t * __unused uri) { } -#endif - + /* immunix_enter_hat will attempt to change_hat in the following order: (1) to a hatname in a location directive @@ -128,9 +135,12 @@ immunix_enter_hat (request_rec *r) ap_get_module_config (r->per_dir_config, &apparmor_module); immunix_srv_cfg * scfg = (immunix_srv_cfg *) ap_get_module_config (r->server->module_config, &apparmor_module); + const char *aa_hat_array[5] = { NULL, NULL, NULL, NULL, NULL }; + int i = 0; + char *aa_con, *aa_mode, *aa_hat; - debug_dump_uri (&r->parsed_uri); - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "in immunix_enter_hat (%s) n:0x%lx p:0x%lx main:0x%lx", + debug_dump_uri(r); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "in immunix_enter_hat (%s) n:0x%lx p:0x%lx main:0x%lx", dcfg->path, (unsigned long) r->next, (unsigned long) r->prev, (unsigned long) r->main); @@ -139,41 +149,79 @@ immunix_enter_hat (request_rec *r) return OK; if (inside_default_hat) { - change_hat (NULL, magic_token); + aa_change_hat(NULL, magic_token); inside_default_hat = 0; } if (dcfg != NULL && dcfg->hat_name != NULL) { - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "calling change_hat [dcfg] %s", dcfg->hat_name); - sd_ret = change_hat (dcfg->hat_name, magic_token); - if (sd_ret < 0) { - change_hat (NULL, magic_token); - } else { - return OK; - } + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "[dcfg] adding hat '%s' to aa_change_hat vector", dcfg->hat_name); + aa_hat_array[i++] = dcfg->hat_name; } - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "calling change_hat [uri] %s", r->uri); - sd_ret = change_hat (r->uri, magic_token); - if (sd_ret < 0) { - change_hat (NULL, magic_token); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "[uri] adding uri '%s' to aa_change_hat vector", r->uri); + aa_hat_array[i++] = r->uri; + + if (scfg) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "Dumping scfg info: " + "scfg='0x%lx' scfg->hat_name='%s'", + (unsigned long) scfg, scfg->hat_name); } else { - return OK; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "scfg is null"); } - - if (scfg != NULL && scfg->hat_name != NULL) { - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "calling change_hat [scfg] %s", scfg->hat_name); - sd_ret = change_hat (scfg->hat_name, magic_token); - if (sd_ret < 0) { - change_hat (NULL, magic_token); - } else { - return OK; + if (scfg != NULL) { + if (scfg->hat_name != NULL) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "[scfg] adding hat '%s' to aa_change_hat vector", scfg->hat_name); + aa_hat_array[i++] = scfg->hat_name; + } else { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "[scfg] adding server_name '%s' to aa_change_hat vector", + r->server->server_hostname); + aa_hat_array[i++] = r->server->server_hostname; } } - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "calling change_hat DEFAULT_URI"); - sd_ret = change_hat (DEFAULT_URI_HAT, magic_token); - if (sd_ret < 0) change_hat (NULL, magic_token); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "[default] adding '%s' to aa_change_hat vector", DEFAULT_URI_HAT); + aa_hat_array[i++] = DEFAULT_URI_HAT; + + sd_ret = aa_change_hatv(aa_hat_array, magic_token); + if (sd_ret < 0) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, errno, r, "aa_change_hatv call failed"); + } + + /* Check to see if a defined AAHatName or AADefaultHatName would + * apply, but wasn't the hat we landed up in; report a warning if + * that's the case. */ + sd_ret = aa_getcon(&aa_con, &aa_mode); + if (sd_ret < 0) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, errno, r, "aa_getcon call failed"); + } else { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "AA checks: aa_getcon result is '%s', mode '%s'", aa_con, aa_mode); + /* TODO: use libapparmor get hat_name fn here once it is implemented */ + aa_hat = strstr(aa_con, "//"); + if (aa_hat != NULL && strcmp(aa_mode, "enforce") == 0) { + aa_hat += 2; /* skip "//" */ + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "AA checks: apache is in hat '%s', mode '%s'", aa_hat, aa_mode); + if (dcfg != NULL && dcfg->hat_name != NULL) { + if (strcmp(aa_hat, dcfg->hat_name) != 0) + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "AAHatName '%s' applies, but does not appear to be a hat in the apache apparmor policy", + dcfg->hat_name); + } else if (scfg != NULL && scfg->hat_name != NULL) { + if (strcmp(aa_hat, scfg->hat_name) != 0 && + strcmp(aa_hat, r->uri) != 0) + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, + "AADefaultHatName '%s' applies, but does not appear to be a hat in the apache apparmor policy", + scfg->hat_name); + } + } + free(aa_con); + } return OK; } @@ -186,14 +234,18 @@ immunix_exit_hat (request_rec *r) ap_get_module_config (r->per_dir_config, &apparmor_module); /* immunix_srv_cfg * scfg = (immunix_srv_cfg *) ap_get_module_config (r->server->module_config, &apparmor_module); */ - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "exiting change_hat - dir hat %s path %s", dcfg->hat_name, dcfg->path); - change_hat (NULL, magic_token); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "exiting change_hat: dir hat %s dir path %s", + dcfg->hat_name, dcfg->path); - sd_ret = change_hat (DEFAULT_HAT, magic_token); + /* can convert the following back to aa_change_hat() when the + * aa_change_hat() bug addressed in trunk commit 2329 lands in most + * system libapparmors */ + aa_change_hatv(NULL, magic_token); + + sd_ret = aa_change_hat(DEFAULT_HAT, magic_token); if (sd_ret < 0) { - change_hat (NULL, magic_token); - ap_log_error (APLOG_MARK, APLOG_ERR, 0, NULL, "Failed to change_hat to '%s'", - DEFAULT_HAT); + ap_log_rerror(APLOG_MARK, APLOG_ERR, errno, r, + "Failed to change_hat to '%s'", DEFAULT_HAT); } else { inside_default_hat = 1; } @@ -204,7 +256,7 @@ immunix_exit_hat (request_rec *r) static const char * aa_cmd_ch_path (cmd_parms * cmd, void * mconfig, const char * parm1) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL, "config change hat %s", + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, ap_server_conf, "directory config change hat %s", parm1 ? parm1 : "DEFAULT"); immunix_dir_cfg * dcfg = mconfig; if (parm1 != NULL) { @@ -221,7 +273,7 @@ static const char * immunix_cmd_ch_path (cmd_parms * cmd, void * mconfig, const char * parm1) { if (path_warn_once == 0) { - ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, NULL, "ImmHatName is " + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, "ImmHatName is " "deprecated, please use AAHatName instead"); path_warn_once = 1; } @@ -231,9 +283,10 @@ immunix_cmd_ch_path (cmd_parms * cmd, void * mconfig, const char * parm1) static const char * aa_cmd_ch_srv (cmd_parms * cmd, void * mconfig, const char * parm1) { - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, NULL, "config change hat %s", + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, ap_server_conf, "server config change hat %s", parm1 ? parm1 : "DEFAULT"); - immunix_srv_cfg * scfg = mconfig; + immunix_srv_cfg * scfg = (immunix_srv_cfg *) + ap_get_module_config(cmd->server->module_config, &apparmor_module); if (parm1 != NULL) { scfg->hat_name = parm1; } else { @@ -248,7 +301,7 @@ static const char * immunix_cmd_ch_srv (cmd_parms * cmd, void * mconfig, const char * parm1) { if (srv_warn_once == 0) { - ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, NULL, "ImmDefaultHatName is " + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, "ImmDefaultHatName is " "deprecated, please use AADefaultHatName instead"); srv_warn_once = 1; } @@ -260,9 +313,9 @@ immunix_create_dir_config (apr_pool_t * p, char * path) { immunix_dir_cfg * newcfg = (immunix_dir_cfg *) apr_pcalloc(p, sizeof(* newcfg)); - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "in immunix_create_dir (%s)", path ? path : ":no path:"); + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf, "in immunix_create_dir (%s)", path ? path : ":no path:"); if (newcfg == NULL) { - ap_log_error (APLOG_MARK, APLOG_ERR, 0, NULL, "immunix_create_dir: couldn't alloc dir config"); + ap_log_error(APLOG_MARK, APLOG_ERR, 0, ap_server_conf, "immunix_create_dir: couldn't alloc dir config"); return NULL; } newcfg->path = apr_pstrdup (p, path ? path : ":no path:"); @@ -277,7 +330,7 @@ immunix_merge_dir_config (apr_pool_t * p, void * parent, void * child) { immunix_dir_cfg * newcfg = (immunix_dir_cfg *) apr_pcalloc(p, sizeof(* newcfg)); - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "in immunix_merge_dir ()"); + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf, "in immunix_merge_dir ()"); if (newcfg == NULL) return NULL; @@ -290,9 +343,9 @@ immunix_create_srv_config (apr_pool_t * p, server_rec * srv) { immunix_srv_cfg * newcfg = (immunix_srv_cfg *) apr_pcalloc(p, sizeof(* newcfg)); - ap_log_error (APLOG_MARK, APLOG_DEBUG, 0, NULL, "in immunix_create_srv"); + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf, "in immunix_create_srv"); if (newcfg == NULL) { - ap_log_error (APLOG_MARK, APLOG_ERR, 0, NULL, "immunix_create_srv: couldn't alloc srv config"); + ap_log_error(APLOG_MARK, APLOG_ERR, 0, ap_server_conf, "immunix_create_srv: couldn't alloc srv config"); return NULL; } diff --git a/changehat/mod_apparmor/mod_apparmor.pod b/changehat/mod_apparmor/mod_apparmor.pod index d9755f8cf..75616416c 100644 --- a/changehat/mod_apparmor/mod_apparmor.pod +++ b/changehat/mod_apparmor/mod_apparmor.pod @@ -41,10 +41,12 @@ apparmor is also functioning. Once mod_apparmor is loaded within Apache, all requests to Apache will cause mod_apparmor to attempt to change into a hat named by the URI -(e.g. /app/some.cgi). If no such hat is found, it will fall back to +(e.g. /app/some.cgi). If no such hat is found, it will first fall +back by attempting to change into a hat that matches the ServerName +for the server/vhost. If that hat is not found, it will fall back to attempting to use the hat DEFAULT_URI; if that also does not exist, -it will fall back to using the global Apache profile. Most static web -pages can simply make use of the DEFAULT_URI hat. +it will fall back to using the global Apache profile. Most static +web pages can simply make use of the DEFAULT_URI hat. Additionally, before any requests come in to Apache, mod_apparmor will attempt to change hat into the HANDLING_UNTRUSTED_INPUT hat. @@ -70,13 +72,14 @@ behavior described above. =item B -AADefaultHatName allows you to specify a default hat to be used for -virtual hosts and other Apache server directives, so that you can have -different defaults for different virtual hosts. This can be overridden by -the AAHatName directive and is checked for only if there isn't a matching -AAHatName or hat named by the URI. If the AADefaultHatName hat does not -exist, it falls back to the DEFAULT_URI hat if it exists (as described -above). +AADefaultHatName allows you to specify a default hat to be used +for virtual hosts and other Apache server directives, so that you +can have different defaults for different virtual hosts. This can +be overridden by the AAHatName directive and is checked for only if +there isn't a matching AAHatName or hat named by the URI. The default +value of AADefaultHatName is the ServerName for the server/vhost +configuration. If the AADefaultHatName hat does not exist, it falls +back to the DEFAULT_URI hat if it exists (as described above). =back @@ -98,8 +101,9 @@ applies, otherwise it will 2. try to aa_change_hat(2) into the URI itself, otherwise it will -3. try to aa_change_hat(2) into an AADefaultHatName hat if it has been defined -for the server/vhost, otherwise it will +3. try to aa_change_hat(2) into an AADefaultHatName hat, either the +ServerName (the default) or the configuration value specified by the +AADefaultHatName directive, for the server/vhost, otherwise it will 4. try to aa_change_hat(2) into the DEFAULT_URI hat, if it exists, otherwise it will @@ -112,7 +116,8 @@ will mod_apparmor() currently only supports apache2, and has only been tested with the prefork MPM configuration -- threaded configurations of Apache -may not work correctly. +may not work correctly. For Apache 2.4 users, you should enable the mpm_prefork +module. There are likely other bugs lurking about; if you find any, please report them at L. diff --git a/utils/Immunix/AppArmor.pm b/deprecated/utils/Immunix/AppArmor.pm similarity index 100% rename from utils/Immunix/AppArmor.pm rename to deprecated/utils/Immunix/AppArmor.pm diff --git a/utils/Immunix/Config.pm b/deprecated/utils/Immunix/Config.pm similarity index 100% rename from utils/Immunix/Config.pm rename to deprecated/utils/Immunix/Config.pm diff --git a/utils/Immunix/Reports.pm b/deprecated/utils/Immunix/Reports.pm similarity index 100% rename from utils/Immunix/Reports.pm rename to deprecated/utils/Immunix/Reports.pm diff --git a/utils/Immunix/Repository.pm b/deprecated/utils/Immunix/Repository.pm similarity index 100% rename from utils/Immunix/Repository.pm rename to deprecated/utils/Immunix/Repository.pm diff --git a/utils/Immunix/Severity.pm b/deprecated/utils/Immunix/Severity.pm similarity index 100% rename from utils/Immunix/Severity.pm rename to deprecated/utils/Immunix/Severity.pm diff --git a/deprecated/utils/Makefile b/deprecated/utils/Makefile new file mode 100644 index 000000000..8e46d1272 --- /dev/null +++ b/deprecated/utils/Makefile @@ -0,0 +1,69 @@ +# ---------------------------------------------------------------------- +# Copyright (c) 1999, 2004-2009 NOVELL (All rights reserved) +# Copyright (c) 2010-2011, 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. +# +# 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. +# ---------------------------------------------------------------------- + +# NOTE: this Makefile has been adjusted from the original to assist in +# the installation of the Immunix perl modules, if they're still needed +# by users. Because the utilities conflict with their replacments, make +# install *will* *not* install them. + +NAME = apparmor-utils +all: +COMMONDIR=../../common/ + +include common/Make.rules + +COMMONDIR_EXISTS=$(strip $(shell [ -d ${COMMONDIR} ] && echo true)) +ifeq ($(COMMONDIR_EXISTS), true) +common/Make.rules: $(COMMONDIR)/Make.rules + ln -sf $(COMMONDIR) . +endif + +MODDIR = Immunix +PERLTOOLS = aa-genprof aa-logprof aa-autodep aa-audit aa-complain aa-enforce \ + aa-unconfined aa-disable +MODULES = ${MODDIR}/AppArmor.pm ${MODDIR}/Repository.pm \ + ${MODDIR}/Config.pm ${MODDIR}/Severity.pm + +all: + +# need some better way of determining this +DESTDIR=/ +BINDIR=${DESTDIR}/usr/sbin +CONFDIR=${DESTDIR}/etc/apparmor +VENDOR_PERL=$(shell perl -e 'use Config; print $$Config{"vendorlib"};') +PERLDIR=${DESTDIR}${VENDOR_PERL}/${MODDIR} + +.PHONY: install +install: + install -d ${PERLDIR} + install -m 644 ${MODULES} ${PERLDIR} + +.PHONY: clean +ifndef VERBOSE +.SILENT: clean +endif +clean: _clean + rm -f core core.* *.o *.s *.a *~ + rm -f Make.rules + rm -rf staging/ build/ + +.PHONY: check +.SILENT: check +check: + for i in ${MODULES} ${PERLTOOLS} ; do \ + perl -c $$i || exit 1; \ + done diff --git a/deprecated/utils/aa-audit b/deprecated/utils/aa-audit new file mode 100755 index 000000000..8ddec1cac --- /dev/null +++ b/deprecated/utils/aa-audit @@ -0,0 +1,132 @@ +#!/usr/bin/perl +# ---------------------------------------------------------------------- +# Copyright (c) 2005 Novell, Inc. All Rights Reserved. +# Copyright (c) 2011 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 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact Novell, Inc. +# +# To contact Novell about this file by physical or electronic mail, +# you may find current contact information at www.novell.com. +# ---------------------------------------------------------------------- + +use strict; +use FindBin; +use Getopt::Long; + +use Immunix::AppArmor; + +use Data::Dumper; + +use Locale::gettext; +use POSIX; + +# initialize the local poo +setlocale(LC_MESSAGES, ""); +textdomain("apparmor-utils"); + +$UI_Mode = "text"; + +# options variables +my $help = ''; + +GetOptions( + 'dir|d=s' => \$profiledir, + 'help|h' => \$help, +); + +# tell 'em how to use it... +&usage && exit if $help; + +# let's convert it to full path... +$profiledir = get_full_path($profiledir); + +unless (-d $profiledir) { + UI_Important("Can't find AppArmor profiles in $profiledir."); + exit 1; +} + +# what are we profiling? +my @profiling = @ARGV; + +unless (@profiling) { + @profiling = (UI_GetString("Please enter the program to switch to audit mode: ", "")); +} + +for my $profiling (@profiling) { + + next unless $profiling; + + my $fqdbin; + if (-e $profiling) { + $fqdbin = get_full_path($profiling); + chomp($fqdbin); + } else { + if ($profiling !~ /\//) { + opendir(DIR,$profiledir); + my @tmp_fqdbin = grep ( /$profiling/, readdir(DIR)); + closedir(DIR); + if (scalar @tmp_fqdbin eq 1) { + $fqdbin = "$profiledir/$tmp_fqdbin[0]"; + } else { + my $which = which($profiling); + if ($which) { + $fqdbin = get_full_path($which); + } + } + } + } + + if (-e $fqdbin) { + + my $filename; + if ($fqdbin =~ /^$profiledir\//) { + $filename = $fqdbin; + } else { + $filename = getprofilefilename($fqdbin); + } + + # argh, skip directories + next unless -f $filename; + + # skip rpm backup files + next if isSkippableFile($filename); + + printf(gettext('Setting %s to audit mode.'), $fqdbin); + print "\n"; + setprofileflags($filename, "audit"); + + my $cmd_info = qx(cat $filename | $parser -I$profiledir -r 2>&1 1>/dev/null); + if ($? != 0) { + UI_Info($cmd_info); + exit $?; + } + +# if check_for_subdomain(); + } else { + if ($profiling =~ /^[^\/]+$/) { + UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); + exit 1; + } else { + UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); + exit 1; + } + } +} + +exit 0; + +sub usage { + UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ program to switch to audit mode ]"), $0)); + exit 0; +} + diff --git a/deprecated/utils/aa-autodep b/deprecated/utils/aa-autodep new file mode 100755 index 000000000..3d28b642a --- /dev/null +++ b/deprecated/utils/aa-autodep @@ -0,0 +1,122 @@ +#!/usr/bin/perl +# ---------------------------------------------------------------------- +# Copyright (c) 2005 Novell, Inc. All Rights Reserved. +# Copyright (c) 2011 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 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact Novell, Inc. +# +# To contact Novell about this file by physical or electronic mail, +# you may find current contact information at www.novell.com. +# ---------------------------------------------------------------------- + +use strict; +use FindBin; +use Getopt::Long; + +use Immunix::AppArmor; + +use Data::Dumper; + +use Locale::gettext; +use POSIX; + +# force $PATH to be sane +$ENV{PATH} = "/bin:/sbin:/usr/bin:/usr/sbin"; + +# initialize the local poo +setlocale(LC_MESSAGES, ""); +textdomain("apparmor-utils"); + +$UI_Mode = "text"; + +# options variables +my $help = ''; +my $force = undef; + +GetOptions( + 'force' => \$force, + 'dir|d=s' => \$profiledir, + 'help|h' => \$help, +); + +# tell 'em how to use it... +&usage && exit if $help; + +my $sd_mountpoint = check_for_subdomain(); + +# let's convert it to full path... +$profiledir = get_full_path($profiledir); + +unless (-d $profiledir) { + UI_Important(sprintf(gettext('Can\'t find AppArmor profiles in %s.'), $profiledir)); + exit 1; +} + +# what are we profiling? +my @profiling = @ARGV; + +unless (@profiling) { + @profiling = (UI_GetString(gettext("Please enter the program to create a profile for: "), "")); +} + +for my $profiling (@profiling) { + + next unless $profiling; + + my $fqdbin; + if (-e $profiling) { + $fqdbin = get_full_path($profiling); + chomp($fqdbin); + } else { + if ($profiling !~ /\//) { + my $which = which($profiling); + if ($which) { + $fqdbin = get_full_path($which); + } + } + } + + # make sure that the app they're requesting to profile is not marked as + # not allowed to have it's own profile + if ($qualifiers{$fqdbin}) { + unless ($qualifiers{$fqdbin} =~ /p/) { + UI_Info(sprintf(gettext('%s is currently marked as a program that should not have it\'s own profile. Usually, programs are marked this way if creating a profile for them is likely to break the rest of the system. If you know what you\'re doing and are certain you want to create a profile for this program, edit the corresponding entry in the [qualifiers] section in /etc/apparmor/logprof.conf.'), $fqdbin)); + exit 1; + } + } + + if (-e $fqdbin) { + if (-e getprofilefilename($fqdbin) && !$force) { + UI_Info(sprintf(gettext('Profile for %s already exists - skipping.'), $fqdbin)); + } else { + autodep($fqdbin); + reload($fqdbin) if $sd_mountpoint; + } + } else { + if ($profiling =~ /^[^\/]+$/) { + UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); + exit 1; + } else { + UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); + exit 1; + } + } +} + +exit 0; + +sub usage { + UI_Info("usage: $0 [ --force ] [ -d /path/to/profiles ]"); + exit 0; +} + diff --git a/deprecated/utils/aa-complain b/deprecated/utils/aa-complain new file mode 100755 index 000000000..5e497e05f --- /dev/null +++ b/deprecated/utils/aa-complain @@ -0,0 +1,131 @@ +#!/usr/bin/perl +# ---------------------------------------------------------------------- +# Copyright (c) 2005 Novell, Inc. 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 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact Novell, Inc. +# +# To contact Novell about this file by physical or electronic mail, +# you may find current contact information at www.novell.com. +# ---------------------------------------------------------------------- + +use strict; +use FindBin; +use Getopt::Long; + +use Immunix::AppArmor; + +use Data::Dumper; + +use Locale::gettext; +use POSIX; + +# initialize the local poo +setlocale(LC_MESSAGES, ""); +textdomain("apparmor-utils"); + +$UI_Mode = "text"; + +# options variables +my $help = ''; + +GetOptions( + 'dir|d=s' => \$profiledir, + 'help|h' => \$help, +); + +# tell 'em how to use it... +&usage && exit if $help; + +# let's convert it to full path... +$profiledir = get_full_path($profiledir); + +unless (-d $profiledir) { + UI_Important("Can't find AppArmor profiles in $profiledir."); + exit 1; +} + +# what are we profiling? +my @profiling = @ARGV; + +unless (@profiling) { + @profiling = (UI_GetString(gettext("Please enter the program to switch to complain mode: "), "")); +} + +for my $profiling (@profiling) { + + next unless $profiling; + + my $fqdbin; + if (-e $profiling) { + $fqdbin = get_full_path($profiling); + chomp($fqdbin); + } else { + if ($profiling !~ /\//) { + opendir(DIR,$profiledir); + my @tmp_fqdbin = grep ( /$profiling/, readdir(DIR)); + closedir(DIR); + if (scalar @tmp_fqdbin eq 1) { + $fqdbin = "$profiledir/$tmp_fqdbin[0]"; + } else { + my $which = which($profiling); + if ($which) { + $fqdbin = get_full_path($which); + } + } + } + } + + if (-e $fqdbin) { + + my $filename; + if ($fqdbin =~ /^$profiledir\//) { + $filename = $fqdbin; + } else { + $filename = getprofilefilename($fqdbin); + } + + # argh, skip directories + next unless -f $filename; + + # skip rpm backup files + next if isSkippableFile($filename); + + printf(gettext('Setting %s to complain mode.'), $fqdbin); + print "\n"; + setprofileflags($filename, "complain"); + + my $cmd_info = qx(cat $filename | $parser -I$profiledir -r 2>&1 1>/dev/null); + if ($? != 0) { + UI_Info($cmd_info); + exit $?; + } + +# if check_for_subdomain(); + } else { + if ($profiling =~ /^[^\/]+$/) { + UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); + exit 1; + } else { + UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); + exit 1; + } + } +} + +exit 0; + +sub usage { + UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ program to switch to complain mode ]"), $0)); + exit 0; +} + diff --git a/deprecated/utils/aa-disable b/deprecated/utils/aa-disable new file mode 100755 index 000000000..2cc19a55a --- /dev/null +++ b/deprecated/utils/aa-disable @@ -0,0 +1,152 @@ +#!/usr/bin/perl +# ---------------------------------------------------------------------- +# Copyright (c) 2005-2010 Novell, Inc. All Rights Reserved. +# Copyright (c) 2011 Canonical, Inc. 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 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact Canonical, Inc. +# +# To contact Canonical about this file by physical or electronic mail, +# you may find current contact information at www.canonical.com. +# ---------------------------------------------------------------------- + +use strict; +use FindBin; +use Getopt::Long; + +use Immunix::AppArmor; + +use Data::Dumper; + +use Locale::gettext; +use POSIX; +use File::Basename; + +# initialize the local poo +setlocale(LC_MESSAGES, ""); +textdomain("apparmor-utils"); + +$UI_Mode = "text"; + +# options variables +my $help = ''; + +GetOptions( + 'dir|d=s' => \$profiledir, + 'help|h' => \$help, +); + +# tell 'em how to use it... +&usage && exit if $help; + +# let's convert it to full path... +$profiledir = get_full_path($profiledir); + +unless (-d $profiledir) { + UI_Important("Can't find AppArmor profiles in $profiledir."); + exit 1; +} + +my $disabledir = "$profiledir/disable"; +unless (-d $disabledir) { + UI_Important("Can't find AppArmor disable directory '$disabledir'."); + exit 1; +} + +# what are we profiling? +my @profiling = @ARGV; + +unless (@profiling) { + @profiling = (UI_GetString(gettext("Please enter the program whose profile should be disabled: "), "")); +} + +for my $profiling (@profiling) { + + next unless $profiling; + + my $fqdbin; + if (-e $profiling) { + $fqdbin = get_full_path($profiling); + chomp($fqdbin); + } else { + if ($profiling !~ /\//) { + opendir(DIR,$profiledir); + my @tmp_fqdbin = grep ( /$profiling/, readdir(DIR)); + closedir(DIR); + if (scalar @tmp_fqdbin eq 1) { + $fqdbin = "$profiledir/$tmp_fqdbin[0]"; + } else { + my $which = which($profiling); + if ($which) { + $fqdbin = get_full_path($which); + } + } + } + } + + if (-e $fqdbin) { + + my $filename; + if ($fqdbin =~ /^$profiledir\//) { + $filename = $fqdbin; + } else { + $filename = getprofilefilename($fqdbin); + } + + # argh, skip directories + next unless -f $filename; + + # skip package manager backup files + next if isSkippableFile($filename); + + my ($bname, $dname, $suffix) = File::Basename::fileparse($filename); + if ($bname eq "") { + UI_Info(sprintf(gettext('Could not find basename for %s.'), $filename)); + exit 1; + } + + printf(gettext('Disabling %s.'), $fqdbin); + print "\n"; + + my $link = "$disabledir/$bname"; + if (! -e $link) { + if (symlink($filename, $link) != 1) { + UI_Info(sprintf(gettext('Could not create %s symlink.'), $link)); + exit 1; + } + } + + my $cmd_info = qx(cat $filename | $parser -I$profiledir -R 2>&1 1>/dev/null); + if ($? != 0) { + UI_Info($cmd_info); + exit $?; + } + +# if check_for_subdomain(); + } else { + if ($profiling =~ /^[^\/]+$/) { + UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); + exit 1; + } else { + UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); + exit 1; + } + } +} + +exit 0; + +sub usage { + UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ program to have profile disabled ]"), $0)); + exit 0; +} + diff --git a/deprecated/utils/aa-enforce b/deprecated/utils/aa-enforce new file mode 100755 index 000000000..06415ba5b --- /dev/null +++ b/deprecated/utils/aa-enforce @@ -0,0 +1,142 @@ +#!/usr/bin/perl +# ---------------------------------------------------------------------- +# Copyright (c) 2005 Novell, Inc. All Rights Reserved. +# Copyright (c) 2011 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 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact Novell, Inc. +# +# To contact Novell about this file by physical or electronic mail, +# you may find current contact information at www.novell.com. +# ---------------------------------------------------------------------- + +use strict; +use FindBin; +use Getopt::Long; + +use Immunix::AppArmor; + +use Data::Dumper; + +use Locale::gettext; +use POSIX; + +# initialize the local poo +setlocale(LC_MESSAGES, ""); +textdomain("apparmor-utils"); + +$UI_Mode = "text"; + +# options variables +my $help = ''; + +GetOptions( + 'dir|d=s' => \$profiledir, + 'help|h' => \$help, +); + +# tell 'em how to use it... +&usage && exit if $help; + +# let's convert it to full path... +$profiledir = get_full_path($profiledir); + +unless (-d $profiledir) { + UI_Important("Can't find AppArmor profiles in $profiledir."); + exit 1; +} + +# what are we profiling? +my @profiling = @ARGV; + +unless (@profiling) { + @profiling = (UI_GetString(gettext("Please enter the program to switch to enforce mode: "), "")); +} + +for my $profiling (@profiling) { + + next unless $profiling; + + my $fqdbin; + if (-e $profiling) { + $fqdbin = get_full_path($profiling); + chomp($fqdbin); + } else { + if ($profiling !~ /\//) { + opendir(DIR,$profiledir); + my @tmp_fqdbin = grep ( /$profiling/, readdir(DIR)); + closedir(DIR); + if (scalar @tmp_fqdbin eq 1) { + $fqdbin = "$profiledir/$tmp_fqdbin[0]"; + } else { + my $which = which($profiling); + if ($which) { + $fqdbin = get_full_path($which); + } + } + } + } + + if (-e $fqdbin) { + my $filename; + if ($fqdbin =~ /^$profiledir\//) { + $filename = $fqdbin; + } else { + $filename = getprofilefilename($fqdbin); + } + + # argh, skip directories + next unless -f $filename; + + # skip rpm backup files + next if isSkippableFile($filename); + + printf(gettext('Setting %s to enforce mode.'), $fqdbin); + print "\n"; + setprofileflags($filename, ""); + + # remove symlink in $profiledir/force-complain as well + my $complainlink = $filename; + $complainlink =~ s/^$profiledir/$profiledir\/force-complain/; + -e $complainlink and unlink($complainlink); + + # remove symlink in $profiledir/disable as well + my $disablelink = $filename; + $disablelink =~ s/^$profiledir/$profiledir\/disable/; + -e $disablelink and unlink($disablelink); + + my $cmd_info = qx(cat $filename | $parser -I$profiledir -r 2>&1 1>/dev/null); + if ($? != 0) { + UI_Info($cmd_info); + exit $?; + } + + +# if check_for_subdomain(); + } else { + if ($profiling =~ /^[^\/]+$/) { + UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); + exit 1; + } else { + UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); + exit 1; + } + } +} + +exit 0; + +sub usage { + UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ program to switch to enforce mode ]"), $0)); + exit 0; +} + diff --git a/utils/aa-eventd b/deprecated/utils/aa-eventd similarity index 100% rename from utils/aa-eventd rename to deprecated/utils/aa-eventd diff --git a/deprecated/utils/aa-genprof b/deprecated/utils/aa-genprof new file mode 100755 index 000000000..33046042b --- /dev/null +++ b/deprecated/utils/aa-genprof @@ -0,0 +1,216 @@ +#!/usr/bin/perl +# ---------------------------------------------------------------------- +# Copyright (c) 2005 Novell, Inc. 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 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact Novell, Inc. +# +# To contact Novell about this file by physical or electronic mail, +# you may find current contact information at www.novell.com. +# ---------------------------------------------------------------------- + +use strict; +use Getopt::Long; + +use Immunix::AppArmor; + +use Data::Dumper; + +use Locale::gettext; +use POSIX; + +sub sysctl_read($) { + my $path = shift; + my $value = undef; + if (open(SYSCTL, "<$path")) { + $value = int(); + } + close(SYSCTL); + return $value; +} + +sub sysctl_write($$) { + my $path = shift; + my $value = shift; + return if (!defined($value)); + if (open(SYSCTL, ">$path")) { + print SYSCTL $value; + close(SYSCTl); + } +} + +# force $PATH to be sane +$ENV{PATH} = "/bin:/sbin:/usr/bin:/usr/sbin"; + +# initialize the local poo +setlocale(LC_MESSAGES, ""); +textdomain("apparmor-utils"); + +# options variables +my $help = ''; + +GetOptions( + 'file|f=s' => \$filename, + 'dir|d=s' => \$profiledir, + 'help|h' => \$help, +); + +# tell 'em how to use it... +&usage && exit if $help; + +my $sd_mountpoint = check_for_subdomain(); +unless ($sd_mountpoint) { + fatal_error(gettext("AppArmor does not appear to be started. Please enable AppArmor and try again.")); +} + +# let's convert it to full path... +$profiledir = get_full_path($profiledir); + +unless (-d $profiledir) { + fatal_error "Can't find AppArmor profiles in $profiledir."; +} + +# what are we profiling? +my $profiling = shift; + +unless ($profiling) { + $profiling = UI_GetString(gettext("Please enter the program to profile: "), "") + || exit 0; +} + +my $fqdbin; +if (-e $profiling) { + $fqdbin = get_full_path($profiling); + chomp($fqdbin); +} else { + if ($profiling !~ /\//) { + my $which = which($profiling); + if ($which) { + $fqdbin = get_full_path($which); + } + } +} + +unless ($fqdbin && -e $fqdbin) { + if ($profiling =~ /^[^\/]+$/) { + fatal_error(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' in the other window in order to find the fully-qualified path.'), $profiling, $profiling)); + } else { + fatal_error(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); + } +} + + +# make sure that the app they're requesting to profile is not marked as +# not allowed to have it's own profile +check_qualifiers($fqdbin); + +# load all the include files +loadincludes(); + +my $profilefilename = getprofilefilename($fqdbin); +if (-e $profilefilename) { + $helpers{$fqdbin} = getprofileflags($profilefilename) || "enforce"; +} else { + autodep($fqdbin); + $helpers{$fqdbin} = "enforce"; +} + +if ($helpers{$fqdbin} eq "enforce") { + complain($fqdbin); + reload($fqdbin); +} + +# When reading from syslog, it is possible to hit the default kernel +# printk ratelimit. This will result in audit entries getting skipped, +# making profile generation inaccurate. When using genprof, disable +# the printk ratelimit, and restore it on exit. +my $ratelimit_sysctl = "/proc/sys/kernel/printk_ratelimit"; +my $ratelimit_saved = sysctl_read($ratelimit_sysctl); +END { sysctl_write($ratelimit_sysctl, $ratelimit_saved); } +sysctl_write($ratelimit_sysctl, 0); + +UI_Info(gettext("\nBefore you begin, you may wish to check if a\nprofile already exists for the application you\nwish to confine. See the following wiki page for\nmore information:\nhttp://wiki.apparmor.net/index.php/Profiles")); + +UI_Important(gettext("Please start the application to be profiled in \nanother window and exercise its functionality now.\n\nOnce completed, select the \"Scan\" button below in \norder to scan the system logs for AppArmor events. \n\nFor each AppArmor event, you will be given the \nopportunity to choose whether the access should be \nallowed or denied.")); + +my $syslog = 1; +my $logmark = ""; +my $done_profiling = 0; + +$syslog = 0 if (-e "/var/log/audit/audit.log"); + +while (not $done_profiling) { + if ($syslog) { + $logmark = `date | md5sum`; + chomp $logmark; + $logmark = $1 if $logmark =~ /^([0-9a-f]+)/; + system("$logger -p kern.warn 'GenProf: $logmark'"); + } else { + $logmark = last_audit_entry_time(); + } + eval { + + my $q = {}; + $q->{headers} = [ gettext("Profiling"), $fqdbin ]; + $q->{functions} = [ "CMD_SCAN", "CMD_FINISHED" ]; + $q->{default} = "CMD_SCAN"; + + my ($ans, $arg) = UI_PromptUser($q); + + if ($ans eq "CMD_SCAN") { + + my $lp_ret = do_logprof_pass($logmark); + + $done_profiling = 1 if $lp_ret eq "FINISHED"; + + } else { + + $done_profiling = 1; + + } + }; + if ($@) { + if ($@ =~ /FINISHING/) { + $done_profiling = 1; + } else { + die $@; + } + } +} + +for my $p (sort keys %helpers) { + if ($helpers{$p} eq "enforce") { + enforce($p); + reload($p); + } +} + +UI_Info(gettext("Reloaded AppArmor profiles in enforce mode.")); +UI_Info(gettext("\nPlease consider contributing your new profile! See\nthe following wiki page for more information:\nhttp://wiki.apparmor.net/index.php/Profiles\n")); +UI_Info(sprintf(gettext('Finished generating profile for %s.'), $fqdbin)); +exit 0; + +sub usage { + UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ -f /path/to/logfile ] [ program to profile ]"), $0)); + exit 0; +} + +sub last_audit_entry_time { + local $_ = `tail -1 /var/log/audit/audit.log`; + my $logmark; + if (/^*msg\=audit\((\d+\.\d+\:\d+).*\).*$/) { + $logmark = $1; + } else { + $logmark = ""; + } + return $logmark; +} diff --git a/deprecated/utils/aa-logprof b/deprecated/utils/aa-logprof new file mode 100755 index 000000000..b4c34b993 --- /dev/null +++ b/deprecated/utils/aa-logprof @@ -0,0 +1,72 @@ +#!/usr/bin/perl +# ---------------------------------------------------------------------- +# Copyright (c) 2005 Novell, Inc. 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 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact Novell, Inc. +# +# To contact Novell about this file by physical or electronic mail, +# you may find current contact information at www.novell.com. +# ---------------------------------------------------------------------- + +use strict; +use Data::Dumper; +use Getopt::Long; +use Locale::gettext; +use POSIX; + +use Immunix::AppArmor; + +# force $PATH to be sane +$ENV{PATH} = "/bin:/sbin:/usr/bin:/usr/sbin"; + +# initialize the local poo +setlocale(LC_MESSAGES, ""); +textdomain("apparmor-utils"); + +setup_yast(); + +# options variables +my $help = ''; +my $logmark; + +GetOptions( + 'file|f=s' => \$filename, + 'dir|d=s' => \$profiledir, + 'logmark|m=s' => \$logmark, + 'help|h' => \$help, +); + +# tell 'em how to use it... +&usage && exit if $help; + +# let's convert it to full path... +$profiledir = get_full_path($profiledir); + +unless (-d $profiledir) { + fatal_error "Can't find AppArmor profiles in $profiledir."; +} + +# load all the include files +loadincludes(); + +do_logprof_pass($logmark); + +shutdown_yast(); + +exit 0; + +sub usage { + UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ -f /path/to/logfile ] [ -m \"mark in log to start processing after\""), $0)); + exit 0; +} + diff --git a/utils/aa-repo.pl b/deprecated/utils/aa-repo.pl similarity index 100% rename from utils/aa-repo.pl rename to deprecated/utils/aa-repo.pl diff --git a/deprecated/utils/aa-unconfined b/deprecated/utils/aa-unconfined new file mode 100755 index 000000000..a5dac3e25 --- /dev/null +++ b/deprecated/utils/aa-unconfined @@ -0,0 +1,113 @@ +#!/usr/bin/perl -w +# ---------------------------------------------------------------------- +# Copyright (c) 2005 Novell, Inc. 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 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact Novell, Inc. +# +# To contact Novell about this file by physical or electronic mail, +# you may find current contact information at www.novell.com. +# ---------------------------------------------------------------------- +# +# unconfined - +# audit local system for processes listening on network connections +# that are not currently running with a profile. + +use strict; +use Getopt::Long; + +use Immunix::AppArmor; +use Locale::gettext; +use POSIX; + +setlocale(LC_MESSAGES, ""); +textdomain("apparmor-utils"); + +# options variables +my $paranoid = ''; +my $help = ''; + +GetOptions( + 'paranoid' => \$paranoid, + 'help|h' => \$help, +); + +# tell 'em how to use it... +&usage && exit if $help; + +sub usage { + printf(gettext("Usage: %s [ --paranoid ]\n"), $0); + exit 0; +} + +my $subdomainfs = check_for_subdomain(); + +die gettext("AppArmor does not appear to be started. Please enable AppArmor and try again.") . "\n" + unless $subdomainfs; + +my @pids; +if ($paranoid) { + opendir(PROC, "/proc") or die gettext("Can't read /proc\n"); + @pids = grep { /^\d+$/ } readdir(PROC); + closedir(PROC); +} else { + if (open(NETSTAT, "LANG=C /bin/netstat -nlp |")) { + while () { + chomp; + push @pids, $5 + if /^(tcp|udp)\s+\d+\s+\d+\s+\S+\:(\d+)\s+\S+\:(\*|\d+)\s+(LISTEN|\s+)\s+(\d+)\/(\S+)/; + } + close(NETSTAT); + } +} + +for my $pid (sort { $a <=> $b } @pids) { + my $prog = readlink "/proc/$pid/exe" or next; + my $attr; + if (open(CURRENT, "/proc/$pid/attr/current")) { + while () { + chomp; + $attr = $_ if (/^\// || /^null/); + } + close(CURRENT); + } + my $cmdline = `cat /proc/$pid/cmdline`; + my $pname = (split(/\0/, $cmdline))[0]; + if ($pname =~ /\// && !($pname eq $prog)) { + $pname = "($pname) "; + } else { + $pname = ""; + } + if (not $attr) { + if ($prog =~ m/^(\/usr\/bin\/python|\/usr\/bin\/perl|\/bin\/bash)$/) { + + #my $scriptname = (split(/\0/, `cat /proc/$pid/cmdline`))[1]; + $cmdline =~ s/\0/ /g; + $cmdline =~ s/\s+$//; + chomp $cmdline; + print "$pid $prog ($cmdline) " . gettext("not confined\n"); + } else { + print "$pid $prog $pname" . gettext("not confined\n"); + } + } else { + if ($prog =~ m/^(\/usr\/bin\/python|\/usr\/bin\/perl|\/bin\/bash)$/) { + + #my $scriptname = (split(/\0/, `cat /proc/$pid/cmdline`))[1]; + $cmdline =~ s/\0/ /g; + $cmdline =~ s/\s+$//; + chomp $cmdline; + print "$pid $prog ($cmdline) " . gettext("confined by") . " '$attr'\n"; + } else { + print "$pid $prog $pname" . gettext("confined by") . " '$attr'\n"; + } + } +} diff --git a/utils/convert-profile.pl b/deprecated/utils/convert-profile.pl similarity index 100% rename from utils/convert-profile.pl rename to deprecated/utils/convert-profile.pl diff --git a/utils/repair_obsolete_profiles b/deprecated/utils/repair_obsolete_profiles similarity index 100% rename from utils/repair_obsolete_profiles rename to deprecated/utils/repair_obsolete_profiles diff --git a/libraries/libapparmor/src/Makefile.am b/libraries/libapparmor/src/Makefile.am index 0c9a88a39..00d2b85f9 100644 --- a/libraries/libapparmor/src/Makefile.am +++ b/libraries/libapparmor/src/Makefile.am @@ -18,8 +18,8 @@ INCLUDES = $(all_includes) # release, then # - set AA_LIB_AGE to 0. # -AA_LIB_CURRENT = 1 -AA_LIB_REVISION = 2 +AA_LIB_CURRENT = 2 +AA_LIB_REVISION = 0 AA_LIB_AGE = 0 SUFFIXES = .pc.in .pc diff --git a/libraries/libapparmor/src/kernel_interface.c b/libraries/libapparmor/src/kernel_interface.c index 4a16c3860..3fa328b1b 100644 --- a/libraries/libapparmor/src/kernel_interface.c +++ b/libraries/libapparmor/src/kernel_interface.c @@ -355,7 +355,7 @@ int aa_change_hat(const char *subprofile, unsigned long token) int rc = -1; int len = 0; char *buf = NULL; - const char *fmt = "changehat %016x^%s"; + const char *fmt = "changehat %016lx^%s"; /* both may not be null */ if (!(token || subprofile)) { diff --git a/parser/libapparmor_re/parse.y b/parser/libapparmor_re/parse.y index b29523fde..b410437b7 100644 --- a/parser/libapparmor_re/parse.y +++ b/parser/libapparmor_re/parse.y @@ -60,7 +60,7 @@ static inline Chars* insert_char_range(Chars* cset, uchar a, uchar b) %lex-param {YYLEX_PARAM} %parse-param {Node **root} %parse-param {const char *text} -%name-prefix = "regex_" +%name-prefix "regex_" %token CHAR %type regex_char cset_char1 cset_char cset_charN diff --git a/parser/parser_alias.c b/parser/parser_alias.c index aea9f1af4..c3e03d58d 100644 --- a/parser/parser_alias.c +++ b/parser/parser_alias.c @@ -177,8 +177,10 @@ static void process_name(const void *nodep, VISIT value, int __unused level) return; /* aliases create alternate names */ alt = (struct alt_name *) calloc(1, sizeof(struct alt_name)); - if (!alt) + if (!alt) { + free(n); return; + } alt->name = n; alt->next = prof->altnames; prof->altnames = alt; diff --git a/parser/parser_include.c b/parser/parser_include.c index 0ad865dd6..3f97a972a 100644 --- a/parser/parser_include.c +++ b/parser/parser_include.c @@ -133,13 +133,19 @@ void set_base_dir(char *dir) int add_search_dir(const char *dir) { char *t; + size_t len; + if (npath >= MAX_PATH) { PERROR(_("Error: Could not add directory %s to search path.\n"), dir); return 0; } - if (!dir || strlen(dir) <= 0) + if (!dir) + return 1; + + len = strlen(dir); + if (len == 0) return 1; t = strdup(dir); @@ -149,8 +155,8 @@ int add_search_dir(const char *dir) } /*strip trailing /'s */ - while (t[strlen(t) - 1] == '/') - t[strlen(t) - 1] = 0; + while (len > 0 && t[--len] == '/') + t[len] = '\0'; path[npath] = t; npath++; diff --git a/parser/parser_lex.l b/parser/parser_lex.l index ad2f0f748..1b6005336 100644 --- a/parser/parser_lex.l +++ b/parser/parser_lex.l @@ -273,8 +273,7 @@ LT_EQUAL <= { (\<([^\> \t\n]+)\>|\"([^\" \t\n]+)\") { /* */ - char *filename = strdup(yytext); - filename[strlen(filename) - 1] = '\0'; + char *filename = strndup(yytext, yyleng - 1); include_filename(filename + 1, *filename == '<'); free(filename); yy_pop_state(); @@ -488,7 +487,12 @@ LT_EQUAL <= } } -#include/.*\r?\n { PUSH(INCLUDE); } +#include/.*\r?\n { + /* Don't use push here as we don't want #include echoed out. It needs + * to be handled specially + */ + yy_push_state(INCLUDE); +} #.*\r?\n { /* normal comment */ DUMP_AND_DEBUG("comment(%d): %s\n", current_lineno, yytext); @@ -536,7 +540,6 @@ LT_EQUAL <= {OPEN_PAREN} { PUSH_AND_RETURN(LIST_VAL_MODE, TOK_OPENPAREN); } {VARIABLE_NAME} { - DUMP_PREPROCESS; int token = get_keyword_token(yytext); int state = INITIAL; diff --git a/parser/parser_main.c b/parser/parser_main.c index f6cf6cc3b..72a382507 100644 --- a/parser/parser_main.c +++ b/parser/parser_main.c @@ -87,6 +87,8 @@ char *cacheloc = NULL; /* per-profile settings */ int force_complain = 0; +/* Make sure to update BOTH the short and long_options */ +static const char *short_options = "adf:h::rRVvI:b:BCD:NSm:qQn:XKTWkL:O:po:"; struct option long_options[] = { {"add", 0, 0, 'a'}, {"binary", 0, 0, 'B'}, @@ -570,7 +572,7 @@ static int process_arg(int c, char *optarg) break; default: display_usage(progname); - exit(0); + exit(1); break; } @@ -583,7 +585,7 @@ static int process_args(int argc, char *argv[]) int count = 0; option = OPTION_ADD; - while ((c = getopt_long(argc, argv, "adf:h::rRVvI:b:BCD:NSm:qQn:XKTWkO:po:", long_options, &o)) != -1) + while ((c = getopt_long(argc, argv, short_options, long_options, &o)) != -1) { count += process_arg(c, optarg); } @@ -611,6 +613,7 @@ static int process_config_file(const char *name) while ((c = getopt_long_file(f, long_options, &optarg, &o)) != -1) process_arg(c, optarg); + fclose(f); return 1; } diff --git a/parser/parser_misc.c b/parser/parser_misc.c index 101ef2a22..2dfb1bb6b 100644 --- a/parser/parser_misc.c +++ b/parser/parser_misc.c @@ -174,6 +174,9 @@ static struct keyword_table rlimit_table[] = { #endif #ifdef RLIMIT_RTPRIO {"rtprio", RLIMIT_RTPRIO}, +#endif +#ifdef RLIMIT_RTTIME + {"rttime", RLIMIT_RTTIME}, #endif /* terminate */ {NULL, 0} diff --git a/parser/parser_regex.c b/parser/parser_regex.c index 0cc67285f..8de30bb75 100644 --- a/parser/parser_regex.c +++ b/parser/parser_regex.c @@ -24,7 +24,10 @@ #include #define _(s) gettext(s) +#include #include +#include + /* #define DEBUG */ @@ -39,9 +42,11 @@ enum error_type { e_no_error, e_parse_error, - e_buffer_overflow }; +/* match any char except \000 0 or more times */ +static const char *default_match_pattern = "[^\\000]*"; + /* Filters out multiple slashes (except if the first two are slashes, * that's a distinct namespace in linux) and trailing slashes. * NOTE: modifies in place the contents of the path argument */ @@ -374,11 +379,6 @@ static pattern_t convert_aaregex_to_pcre(const char *aare, int anchor, } /* check error again, as above STORE may have set it */ if (error != e_no_error) { - if (error == e_buffer_overflow) { - PERROR(_("%s: Internal buffer overflow detected, %d characters exceeded\n"), - progname, PATH_MAX); - } - PERROR(_("%s: Unable to parse input line '%s'\n"), progname, aare); @@ -634,7 +634,7 @@ static int build_list_val_expr(std::string& buffer, struct value_list *list) int pos; if (!list) { - buffer.append("[^\\000]*"); + buffer.append(default_match_pattern); return TRUE; } @@ -667,12 +667,18 @@ static int convert_entry(std::string& buffer, char *entry) if (ptype == ePatternInvalid) return FALSE; } else { - buffer.append("[^\\000]*"); + buffer.append(default_match_pattern); } return TRUE; } +static int clear_and_convert_entry(std::string& buffer, char *entry) +{ + buffer.clear(); + return convert_entry(buffer, entry); +} + static int build_mnt_flags(char *buffer, int size, unsigned int flags, unsigned int inv_flags) { @@ -681,7 +687,7 @@ static int build_mnt_flags(char *buffer, int size, unsigned int flags, if (flags == MS_ALL_FLAGS) { /* all flags are optional */ - len = snprintf(p, size, "[^\\000]*"); + len = snprintf(p, size, "%s", default_match_pattern); if (len < 0 || len >= size) return FALSE; return TRUE; @@ -721,7 +727,7 @@ static int build_mnt_opts(std::string& buffer, struct value_list *opts) int pos; if (!opts) { - buffer.append("[^\\000]*"); + buffer.append(default_match_pattern); return TRUE; } @@ -772,12 +778,9 @@ static int process_mnt_entry(aare_ruleset_t *dfarules, struct mnt_entry *entry) vec[0] = mntbuf.c_str(); } /* skip device */ - devbuf.clear(); - if (!convert_entry(devbuf, NULL)) - goto fail; - vec[1] = devbuf.c_str(); + vec[1] = default_match_pattern; /* skip type */ - vec[2] = devbuf.c_str(); + vec[2] = default_match_pattern; flags = entry->flags; inv_flags = entry->inv_flags; @@ -823,14 +826,11 @@ static int process_mnt_entry(aare_ruleset_t *dfarules, struct mnt_entry *entry) if (!convert_entry(mntbuf, entry->mnt_point)) goto fail; vec[0] = mntbuf.c_str(); - devbuf.clear(); - if (!convert_entry(devbuf, entry->device)) + if (!clear_and_convert_entry(devbuf, entry->device)) goto fail; vec[1] = devbuf.c_str(); - typebuf.clear(); - if (!convert_entry(typebuf, NULL)) - goto fail; - vec[2] = typebuf.c_str(); + /* skip type */ + vec[2] = default_match_pattern; flags = entry->flags; inv_flags = entry->inv_flags; @@ -858,11 +858,8 @@ static int process_mnt_entry(aare_ruleset_t *dfarules, struct mnt_entry *entry) goto fail; vec[0] = mntbuf.c_str(); /* skip device and type */ - devbuf.clear(); - if (!convert_entry(devbuf, NULL)) - goto fail; - vec[1] = devbuf.c_str(); - vec[2] = devbuf.c_str(); + vec[1] = default_match_pattern; + vec[2] = default_match_pattern; flags = entry->flags; inv_flags = entry->inv_flags; @@ -888,15 +885,11 @@ static int process_mnt_entry(aare_ruleset_t *dfarules, struct mnt_entry *entry) if (!convert_entry(mntbuf, entry->mnt_point)) goto fail; vec[0] = mntbuf.c_str(); - devbuf.clear(); - if (!convert_entry(devbuf, entry->device)) + if (!clear_and_convert_entry(devbuf, entry->device)) goto fail; vec[1] = devbuf.c_str(); /* skip type */ - typebuf.clear(); - if (!convert_entry(typebuf, NULL)) - goto fail; - vec[2] = typebuf.c_str(); + vec[2] = default_match_pattern; flags = entry->flags; inv_flags = entry->inv_flags; @@ -923,8 +916,7 @@ static int process_mnt_entry(aare_ruleset_t *dfarules, struct mnt_entry *entry) if (!convert_entry(mntbuf, entry->mnt_point)) goto fail; vec[0] = mntbuf.c_str(); - devbuf.clear(); - if (!convert_entry(devbuf, entry->device)) + if (!clear_and_convert_entry(devbuf, entry->device)) goto fail; vec[1] = devbuf.c_str(); typebuf.clear(); @@ -985,8 +977,7 @@ static int process_mnt_entry(aare_ruleset_t *dfarules, struct mnt_entry *entry) if (!convert_entry(mntbuf, entry->mnt_point)) goto fail; vec[0] = mntbuf.c_str(); - devbuf.clear(); - if (!convert_entry(devbuf, entry->device)) + if (!clear_and_convert_entry(devbuf, entry->device)) goto fail; vec[1] = devbuf.c_str(); if (!aare_add_rule_vec(dfarules, entry->deny, entry->allow, @@ -1015,7 +1006,7 @@ static int process_dbus_entry(aare_ruleset_t *dfarules, struct dbus_entry *entry std::string pathbuf; std::string ifacebuf; std::string memberbuf; - char buffer[128]; + std::ostringstream buffer; const char *vec[6]; pattern_t ptype; @@ -1024,8 +1015,8 @@ static int process_dbus_entry(aare_ruleset_t *dfarules, struct dbus_entry *entry if (!entry) /* shouldn't happen */ return TRUE; - sprintf(buffer, "\\x%02x", AA_CLASS_DBUS); - busbuf.append(buffer); + buffer << "\\x" << std::setfill('0') << std::setw(2) << std::hex << AA_CLASS_DBUS; + busbuf.append(buffer.str()); if (entry->bus) { ptype = convert_aaregex_to_pcre(entry->bus, 0, busbuf, &pos); @@ -1033,7 +1024,7 @@ static int process_dbus_entry(aare_ruleset_t *dfarules, struct dbus_entry *entry goto fail; } else { /* match any char except \000 0 or more times */ - busbuf.append("[^\\000]*"); + busbuf.append(default_match_pattern); } vec[0] = busbuf.c_str(); @@ -1044,7 +1035,7 @@ static int process_dbus_entry(aare_ruleset_t *dfarules, struct dbus_entry *entry vec[1] = namebuf.c_str(); } else { /* match any char except \000 0 or more times */ - vec[1] = "[^\\000]*"; + vec[1] = default_match_pattern; } if (entry->peer_label) { @@ -1055,7 +1046,7 @@ static int process_dbus_entry(aare_ruleset_t *dfarules, struct dbus_entry *entry vec[2] = peer_labelbuf.c_str(); } else { /* match any char except \000 0 or more times */ - vec[2] = "[^\\000]*"; + vec[2] = default_match_pattern; } if (entry->path) { @@ -1065,7 +1056,7 @@ static int process_dbus_entry(aare_ruleset_t *dfarules, struct dbus_entry *entry vec[3] = pathbuf.c_str(); } else { /* match any char except \000 0 or more times */ - vec[3] = "[^\\000]*"; + vec[3] = default_match_pattern; } if (entry->interface) { @@ -1075,7 +1066,7 @@ static int process_dbus_entry(aare_ruleset_t *dfarules, struct dbus_entry *entry vec[4] = ifacebuf.c_str(); } else { /* match any char except \000 0 or more times */ - vec[4] = "[^\\000]*"; + vec[4] = default_match_pattern; } if (entry->member) { @@ -1085,7 +1076,7 @@ static int process_dbus_entry(aare_ruleset_t *dfarules, struct dbus_entry *entry vec[5] = memberbuf.c_str(); } else { /* match any char except \000 0 or more times */ - vec[5] = "[^\\000]*"; + vec[5] = default_match_pattern; } if (entry->mode & AA_DBUS_BIND) { @@ -1252,41 +1243,36 @@ static int test_filter_slashes(void) do { \ std::string tbuf; \ std::string tbuf2 = "testprefix"; \ - char *test_string; \ char *output_string = NULL; \ std::string expected_str2; \ pattern_t ptype; \ int pos; \ \ - test_string = strdup((input)); \ - ptype = convert_aaregex_to_pcre(test_string, 0, tbuf, &pos); \ + ptype = convert_aaregex_to_pcre((input), 0, tbuf, &pos); \ asprintf(&output_string, "simple regex conversion for '%s'\texpected = '%s'\tresult = '%s'", \ - (input), expected_str, tbuf.c_str()); \ + (input), (expected_str), tbuf.c_str()); \ MY_TEST(strcmp(tbuf.c_str(), (expected_str)) == 0, output_string); \ MY_TEST(ptype == (expected_type), "simple regex conversion type check for '" input "'"); \ free(output_string); \ /* ensure convert_aaregex_to_pcre appends only to passed ref string */ \ expected_str2 = tbuf2; \ expected_str2.append((expected_str)); \ - ptype = convert_aaregex_to_pcre(test_string, 0, tbuf2, &pos); \ + ptype = convert_aaregex_to_pcre((input), 0, tbuf2, &pos); \ asprintf(&output_string, "simple regex conversion for '%s'\texpected = '%s'\tresult = '%s'", \ (input), expected_str2.c_str(), tbuf2.c_str()); \ MY_TEST((tbuf2 == expected_str2), output_string); \ - free(test_string); free(output_string); \ + free(output_string); \ } \ while (0) #define MY_REGEX_FAIL_TEST(input) \ do { \ std::string tbuf; \ - char *test_string; \ pattern_t ptype; \ int pos; \ \ - test_string = strdup((input)); \ - ptype = convert_aaregex_to_pcre(test_string, 0, tbuf, &pos); \ + ptype = convert_aaregex_to_pcre((input), 0, tbuf, &pos); \ MY_TEST(ptype == ePatternInvalid, "simple regex conversion invalid type check for '" input "'"); \ - free(test_string); \ } \ while (0) diff --git a/parser/parser_variable.c b/parser/parser_variable.c index 8d50a264d..1e76258aa 100644 --- a/parser/parser_variable.c +++ b/parser/parser_variable.c @@ -137,11 +137,11 @@ void free_var_string(struct var_string *var) static void trim_trailing_slash(std::string& str) { - for (std::string::reverse_iterator rit = str.rbegin(); - rit != str.rend() && *rit == '/'; ++rit) { - /* yuck, reverse_iterators are ugly */ - str.erase(--rit.base()); - } + std::size_t found = str.find_last_not_of('/'); + if (found != std::string::npos) + str.erase(found + 1); + else + str.clear(); // str is all '/' } static void write_replacement(const char separator, const char* value, @@ -177,10 +177,11 @@ static int expand_by_alternations(struct set_value **valuelist, exit(1); } + free(*name); + value = get_next_set_value(valuelist); if (!value) { /* only one entry for the variable, so just sub it in */ - free(*name); if (asprintf(name, "%s%s%s", split_var->prefix ? split_var->prefix : "", first_value, @@ -201,7 +202,6 @@ static int expand_by_alternations(struct set_value **valuelist, write_replacement(',', value, replacement, filter_leading_slash, filter_trailing_slash); } - free(*name); if (asprintf(name, "%s%s}%s", split_var->prefix ? split_var->prefix : "", replacement.c_str(), diff --git a/parser/parser_yacc.y b/parser/parser_yacc.y index f17658c4c..498533d54 100644 --- a/parser/parser_yacc.y +++ b/parser/parser_yacc.y @@ -754,6 +754,7 @@ rules: rules TOK_SET TOK_RLIMIT TOK_ID TOK_LE TOK_VALUE TOK_END_OF_RULE value = RLIM_INFINITY; } else { const char *seconds = "seconds"; + const char *milliseconds = "ms"; const char *minutes = "minutes"; const char *hours = "hours"; const char *days = "days"; @@ -779,6 +780,22 @@ rules: rules TOK_SET TOK_RLIMIT TOK_ID TOK_LE TOK_VALUE TOK_END_OF_RULE yyerror("RLIMIT '%s' invalid value %s\n", $4, $6); } 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); + } + break; case RLIMIT_NOFILE: case RLIMIT_NPROC: case RLIMIT_LOCKS: diff --git a/parser/tst/simple_tests/file/allow/ok_other_1.sd b/parser/tst/simple_tests/file/allow/ok_other_1.sd new file mode 100644 index 000000000..4e2104ba6 --- /dev/null +++ b/parser/tst/simple_tests/file/allow/ok_other_1.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION simple allow other flag test +#=EXRESULT PASS + +profile test { + allow other /tmp/** rw, +} diff --git a/parser/tst/simple_tests/file/allow/ok_other_2.sd b/parser/tst/simple_tests/file/allow/ok_other_2.sd new file mode 100644 index 000000000..bc13ce045 --- /dev/null +++ b/parser/tst/simple_tests/file/allow/ok_other_2.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION simple audit allow other flag test +#=EXRESULT PASS + +profile test { + audit allow other /tmp/** rw, +} diff --git a/parser/tst/simple_tests/file/ok_other_2.sd b/parser/tst/simple_tests/file/ok_other_2.sd new file mode 100644 index 000000000..d2eeb7402 --- /dev/null +++ b/parser/tst/simple_tests/file/ok_other_2.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION simple deny other flag test +#=EXRESULT PASS + +profile test { + deny other /tmp/** rw, +} diff --git a/parser/tst/simple_tests/file/ok_other_3.sd b/parser/tst/simple_tests/file/ok_other_3.sd new file mode 100644 index 000000000..2972f34a3 --- /dev/null +++ b/parser/tst/simple_tests/file/ok_other_3.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION simple other flag test +#=EXRESULT PASS + +profile test { + audit other /tmp/** rw, +} diff --git a/parser/tst/simple_tests/rlimits/ok_rlimit_18.sd b/parser/tst/simple_tests/rlimits/ok_rlimit_18.sd new file mode 100644 index 000000000..f2747f10d --- /dev/null +++ b/parser/tst/simple_tests/rlimits/ok_rlimit_18.sd @@ -0,0 +1,7 @@ +# +#=DESCRIPTION simple realtime time rlimit test +#=EXRESULT PASS + +profile rlimit { + set rlimit rttime <= 60minutes, +} diff --git a/parser/tst/valgrind_simple.py b/parser/tst/valgrind_simple.py index 87d9186fa..868e3c175 100755 --- a/parser/tst/valgrind_simple.py +++ b/parser/tst/valgrind_simple.py @@ -24,23 +24,6 @@ VALGRIND_ERROR_CODE = 151 VALGRIND_ARGS = ['--leak-check=full', '--error-exitcode=%d' % (VALGRIND_ERROR_CODE)] VALGRIND_SUPPRESSIONS = ''' -{ - valgrind-add_search_dir-obsessive-overreads - Memcheck:Addr4 - fun:_Z*add_search_dir* - fun:_Z*process_arg* - fun:main -} - -{ - valgrind-yylex-obsessive-overreads - Memcheck:Addr4 - fun:_Z?yylex? - fun:_Z*yyparse* - fun:_Z*process_profile* - fun:main -} - { valgrind-serialize_profile-obsessive-overreads Memcheck:Addr4 diff --git a/profiles/Makefile b/profiles/Makefile index c70dcc7e4..6af6ffe8c 100644 --- a/profiles/Makefile +++ b/profiles/Makefile @@ -44,6 +44,7 @@ local: for profile in ${TOPLEVEL_PROFILES}; do \ fn=$$(basename $$profile); \ echo "# Site-specific additions and overrides for '$$fn'" > ${PROFILES_SOURCE}/local/$$fn; \ + grep "include\\s\\s*" "$$profile" >/dev/null || { echo "$$profile doesn't contain #include " ; exit 1; } ; \ done; \ .PHONY: install diff --git a/profiles/apparmor.d/abstractions/X b/profiles/apparmor.d/abstractions/X index f1c3e1cbb..89f829e55 100644 --- a/profiles/apparmor.d/abstractions/X +++ b/profiles/apparmor.d/abstractions/X @@ -35,6 +35,7 @@ # DRI /usr/lib{,32,64}/dri/** mr, /usr/lib/@{multiarch}/dri/** mr, + /usr/lib/fglrx/dri/** mr, /dev/dri/** rw, /etc/drirc r, owner @{HOME}/.drirc r, diff --git a/profiles/apparmor.d/abstractions/audio b/profiles/apparmor.d/abstractions/audio index ef9b4310c..e9643253e 100644 --- a/profiles/apparmor.d/abstractions/audio +++ b/profiles/apparmor.d/abstractions/audio @@ -56,7 +56,7 @@ owner @{HOME}/.pulse-cookie rwk, owner @{HOME}/.pulse/ rw, owner @{HOME}/.pulse/* rwk, owner /{,var/}run/user/*/pulse/ rw, -owner /{,var/}run/user/*/pulse/* rwk, +owner /{,var/}run/user/*/pulse/{native,pid} rwk, owner @{HOME}/.config/pulse/cookie rwk, owner /tmp/pulse-*/ rw, owner /tmp/pulse-*/* rw, diff --git a/profiles/apparmor.d/abstractions/fonts b/profiles/apparmor.d/abstractions/fonts index 85a86ab9b..e75166900 100644 --- a/profiles/apparmor.d/abstractions/fonts +++ b/profiles/apparmor.d/abstractions/fonts @@ -52,3 +52,6 @@ # poppler CMap tables /usr/share/poppler/cMap/** r, + + # data files for LibThai + /usr/share/libthai/thbrk.tri r, diff --git a/profiles/apparmor.d/abstractions/nameservice b/profiles/apparmor.d/abstractions/nameservice index 7681e0ddb..9492fa082 100644 --- a/profiles/apparmor.d/abstractions/nameservice +++ b/profiles/apparmor.d/abstractions/nameservice @@ -21,6 +21,12 @@ /etc/passwd r, /etc/protocols r, + # When using sssd, the passwd and group files are stored in an alternate path + # and the nss plugin also needs to talk to a pipe + /var/lib/sss/mc/group r, + /var/lib/sss/mc/passwd r, + /var/lib/sss/pipes/nss rw, + /etc/resolv.conf r, # on systems using resolvconf, /etc/resolv.conf is a symlink to # /{,var/}run/resolvconf/resolv.conf and a file sometimes referenced in @@ -50,7 +56,7 @@ /etc/default/nss r, # avahi-daemon is used for mdns4 resolution - /{,var/}run/avahi-daemon/socket w, + /{,var/}run/avahi-daemon/socket rw, # nis #include diff --git a/profiles/apparmor.d/abstractions/python b/profiles/apparmor.d/abstractions/python index f84512563..6454c529c 100644 --- a/profiles/apparmor.d/abstractions/python +++ b/profiles/apparmor.d/abstractions/python @@ -13,12 +13,12 @@ /usr/lib{,32,64}/python2.[4567]/**.{pyc,so} mr, /usr/lib{,32,64}/python2.[4567]/**.{egg,py,pth} r, /usr/lib{,32,64}/python2.[4567]/{site,dist}-packages/ r, - /usr/lib{,32,64}/python3.3/lib-dynload/*.so mr, + /usr/lib{,32,64}/python3.[234]/lib-dynload/*.so mr, /usr/local/lib{,32,64}/python2.[4567]/**.{pyc,so} mr, /usr/local/lib{,32,64}/python2.[4567]/**.{egg,py,pth} r, /usr/local/lib{,32,64}/python2.[4567]/{site,dist}-packages/ r, - /usr/local/lib{,32,64}/python3.3/lib-dynload/*.so mr, + /usr/local/lib{,32,64}/python3.[234]/lib-dynload/*.so mr, # Site-wide configuration /etc/python2.[4567]/** r, diff --git a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/multimedia b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/multimedia index 09ddaf78c..e7c55c5b4 100644 --- a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/multimedia +++ b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/multimedia @@ -55,3 +55,6 @@ # Virus scanners /usr/bin/clamscan Cx -> sanitized_helper, + + # gxine (LP: #1057642) + /var/lib/xine/gxine.desktop r, diff --git a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/ubuntu-integration b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/ubuntu-integration index d80b85e6e..0cd0928ef 100644 --- a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/ubuntu-integration +++ b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/ubuntu-integration @@ -33,3 +33,9 @@ /usr/lib/@{multiarch}/xfce4/exo-1/exo-helper-1 ixr, /etc/xdg/xdg-xubuntu/xfce4/helpers.rc r, /etc/xdg/xfce4/helpers.rc r, + + # unity webapps integration. Could go in its own abstraction + owner /run/user/*/dconf/user rw, + owner @{HOME}/.local/share/unity-webapps/availableapps*.db rwk, + /usr/bin/debconf-communicate Cxr -> sanitized_helper, + owner @{HOME}/.config/libaccounts-glib/accounts.db rk, diff --git a/profiles/apparmor.d/abstractions/ubuntu-helpers b/profiles/apparmor.d/abstractions/ubuntu-helpers index 42bd431bb..4da98e690 100644 --- a/profiles/apparmor.d/abstractions/ubuntu-helpers +++ b/profiles/apparmor.d/abstractions/ubuntu-helpers @@ -38,6 +38,9 @@ profile sanitized_helper { network inet, network inet6, + # Allow all DBus communications + dbus, + # Allow exec of anything, but under this profile. Allow transition # to other profiles if they exist. /bin/* Pixr, diff --git a/profiles/apparmor.d/abstractions/ubuntu-unity7-base b/profiles/apparmor.d/abstractions/ubuntu-unity7-base new file mode 100644 index 000000000..737581527 --- /dev/null +++ b/profiles/apparmor.d/abstractions/ubuntu-unity7-base @@ -0,0 +1,167 @@ +# vim:syntax=apparmor +# ------------------------------------------------------------------ +# +# Copyright (C) 2013-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. +# +# ------------------------------------------------------------------ + +# +# Rules common to applications running under Unity 7 +# + +#include + + # Allow connecting to session bus and where to connect to services + dbus (send) + bus=session + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member=Hello + peer=(name=org.freedesktop.DBus), + dbus (send) + bus=session + path=/org/freedesktop/{db,DB}us + interface=org.freedesktop.DBus + member={Add,Remove}Match + peer=(name=org.freedesktop.DBus), + # NameHasOwner and GetNameOwner could leak running processes and apps + # depending on how services are implemented + dbus (send) + bus=session + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member=GetNameOwner + peer=(name=org.freedesktop.DBus), + dbus (send) + bus=session + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member=NameHasOwner + peer=(name=org.freedesktop.DBus), + + # Allow starting services on the session bus (actual communications with + # the service are mediated elsewhere) + dbus (send) + bus=session + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member=StartServiceByName + peer=(name=org.freedesktop.DBus), + + # Allow connecting to system bus and where to connect to services. Put these + # here so we don't need to repeat these rules in multiple places (actual + # communications with any system services is mediated elsewhere). This does + # allow apps to brute-force enumerate system services, but our system + # services aren't a secret. + /{,var/}run/dbus/system_bus_socket rw, + dbus (send) + bus=system + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member=Hello + peer=(name=org.freedesktop.DBus), + dbus (send) + bus=system + path=/org/freedesktop/{db,DB}us + interface=org.freedesktop.DBus + member={Add,Remove}Match + peer=(name=org.freedesktop.DBus), + # NameHasOwner and GetNameOwner could leak running processes and apps + # depending on how services are implemented + dbus (send) + bus=system + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member=GetNameOwner + peer=(name=org.freedesktop.DBus), + dbus (send) + bus=system + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member=NameHasOwner + peer=(name=org.freedesktop.DBus), + + # + # Access required for connecting to/communication with Unity HUD + # + dbus (send) + bus=session + path="/com/canonical/hud", + dbus (send) + bus=session + interface="com.canonical.hud.*", + dbus (send) + bus=session + path="/com/canonical/hud/applications/*", + dbus (receive) + bus=session + path="/com/canonical/hud", + dbus (receive) + bus=session + interface="com.canonical.hud.*", + + # + # Allow access for connecting to/communication with the appmenu + # + # dbusmenu + dbus (send) + bus=session + interface="com.canonical.AppMenu.*", + dbus (receive, send) + bus=session + path=/com/canonical/menu/**, + + # gmenu + dbus (receive, send) + bus=session + interface=org.gtk.Actions, + dbus (receive, send) + bus=session + interface=org.gtk.Menus, + + # + # Access required for using freedesktop notifications + # + dbus (send) + bus=session + path=/org/freedesktop/Notifications + member=GetCapabilities, + dbus (send) + bus=session + path=/org/freedesktop/Notifications + member=GetServerInformation, + dbus (send) + bus=session + path=/org/freedesktop/Notifications + member=Notify, + dbus (receive) + bus=session + member="Notify" + peer=(name="org.freedesktop.DBus"), + dbus (receive) + bus=session + path=/org/freedesktop/Notifications + member=NotificationClosed, + dbus (send) + bus=session + path=/org/freedesktop/Notifications + member=CloseNotification, + + # accessibility + dbus (send) + bus=session + peer=(name=org.a11y.Bus), + dbus (receive) + bus=session + interface=org.a11y.atspi*, + dbus (receive, send) + bus=accessibility, + + # + # Deny potentially dangerous access + # + deny dbus bus=session path=/com/canonical/[Uu]nity/[Dd]ebug**, diff --git a/profiles/apparmor.d/abstractions/ubuntu-unity7-launcher b/profiles/apparmor.d/abstractions/ubuntu-unity7-launcher new file mode 100644 index 000000000..52f6cd438 --- /dev/null +++ b/profiles/apparmor.d/abstractions/ubuntu-unity7-launcher @@ -0,0 +1,7 @@ + # + # Access required for connecting to/communicating with the Unity Launcher + # + dbus (send) + bus=session + interface="com.canonical.Unity.LauncherEntry" + member="Update", diff --git a/profiles/apparmor.d/abstractions/ubuntu-unity7-messaging b/profiles/apparmor.d/abstractions/ubuntu-unity7-messaging new file mode 100644 index 000000000..828592eef --- /dev/null +++ b/profiles/apparmor.d/abstractions/ubuntu-unity7-messaging @@ -0,0 +1,7 @@ + # + # Access required for connecting to/communicating with the Unity messaging + # indicator + # + dbus (receive, send) + bus=session + path="/com/canonical/indicator/messages/*", diff --git a/profiles/apparmor.d/abstractions/user-download b/profiles/apparmor.d/abstractions/user-download index efa946068..ffe1a1ff5 100644 --- a/profiles/apparmor.d/abstractions/user-download +++ b/profiles/apparmor.d/abstractions/user-download @@ -1,6 +1,7 @@ # ------------------------------------------------------------------ # # Copyright (C) 2002-2006 Novell/SUSE +# 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 @@ -15,7 +16,9 @@ owner @{HOME}/[dD]ownload{,s}/ r, owner @{HOME}/[dD]ownload{,s}/** rwl, owner @{HOME}/[a-zA-Z0-9]* rwl, - owner @{HOME}/Desktop/ r, - owner @{HOME}/Desktop/* rwl, + owner @{HOME}/@{XDG_DESKTOP_DIR}/ r, + owner @{HOME}/@{XDG_DESKTOP_DIR}/* rwl, + owner @{HOME}/@{XDG_DOWNLOAD_DIR}/ r, + owner @{HOME}/@{XDG_DOWNLOAD_DIR}/* rwl, owner "@{HOME}/My Downloads/" r, owner "@{HOME}/My Downloads/**" rwl, diff --git a/profiles/apparmor.d/abstractions/user-write b/profiles/apparmor.d/abstractions/user-write index adf8773f7..79a550aac 100644 --- a/profiles/apparmor.d/abstractions/user-write +++ b/profiles/apparmor.d/abstractions/user-write @@ -1,6 +1,7 @@ # ------------------------------------------------------------------ # # Copyright (C) 2002-2006 Novell/SUSE +# 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 @@ -9,12 +10,12 @@ # ------------------------------------------------------------------ # per-user write directories - owner @{HOME}/ r, - owner @{HOME}/Desktop/ r, - owner @{HOME}/Documents/ r, - owner @{HOME}/Public/ r, - owner @{HOME}/[a-zA-Z0-9]*/ rw, - owner @{HOME}/[a-zA-Z0-9]* rwl, - owner @{HOME}/Desktop/** rwl, - owner @{HOME}/Documents/** rwl, - owner @{HOME}/Public/** rwl, + owner @{HOME}/ r, + owner @{HOME}/@{XDG_DESKTOP_DIR}/ r, + owner @{HOME}/@{XDG_DOCUMENTS_DIR}/ r, + owner @{HOME}/@{XDG_PUBLICSHARE_DIR}/ r, + owner @{HOME}/[a-zA-Z0-9]*/ rw, + owner @{HOME}/[a-zA-Z0-9]* rwl, + owner @{HOME}/@{XDG_DESKTOP_DIR}/** rwl, + owner @{HOME}/@{XDG_DOCUMENTS_DIR}/** rwl, + owner @{HOME}/@{XDG_PUBLICSHARE_DIR}/** rwl, diff --git a/profiles/apparmor.d/abstractions/winbind b/profiles/apparmor.d/abstractions/winbind index bc06f2c60..e982889ea 100644 --- a/profiles/apparmor.d/abstractions/winbind +++ b/profiles/apparmor.d/abstractions/winbind @@ -13,7 +13,9 @@ /tmp/.winbindd/pipe rw, /var/{lib,run}/samba/winbindd_privileged/pipe rw, /etc/samba/smb.conf r, + /etc/samba/dhcp.conf r, /usr/lib*/samba/valid.dat r, /usr/lib*/samba/upcase.dat r, /usr/lib*/samba/lowcase.dat r, + /usr/share/samba/codepages/{lowcase,upcase,valid}.dat r, diff --git a/profiles/apparmor.d/tunables/dovecot b/profiles/apparmor.d/tunables/dovecot new file mode 100644 index 000000000..702da58e0 --- /dev/null +++ b/profiles/apparmor.d/tunables/dovecot @@ -0,0 +1,20 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim:ft=apparmor + +# @{DOVECOT_MAILSTORE} is a space-separated list of all directories +# where dovecot is allowed to store and read mails +# +# The default value is quite broad to avoid breaking existing setups. +# Please change @{DOVECOT_MAILSTORE} to (only) contain the directory +# you use, and remove everything else. + +@{DOVECOT_MAILSTORE}=@{HOME}/Maildir/ @{HOME}/mail/ @{HOME}/Mail/ /var/vmail/ /var/mail/ /var/spool/mail/ + diff --git a/profiles/apparmor.d/tunables/global b/profiles/apparmor.d/tunables/global index 69d827b91..58d087fbe 100644 --- a/profiles/apparmor.d/tunables/global +++ b/profiles/apparmor.d/tunables/global @@ -1,7 +1,7 @@ # ------------------------------------------------------------------ # # Copyright (C) 2006-2009 Novell/SUSE -# Copyright (C) 2010-2011 Canonical Ltd. +# Copyright (C) 2010-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 @@ -17,3 +17,4 @@ #include #include #include +#include diff --git a/profiles/apparmor.d/tunables/xdg-user-dirs b/profiles/apparmor.d/tunables/xdg-user-dirs new file mode 100644 index 000000000..fcaf8d40d --- /dev/null +++ b/profiles/apparmor.d/tunables/xdg-user-dirs @@ -0,0 +1,24 @@ +# ------------------------------------------------------------------ +# +# 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. +# +# ------------------------------------------------------------------ + +# Define the common set of XDG user directories (usually defined in +# /etc/xdg/user-dirs.defaults) +@{XDG_DESKTOP_DIR}="Desktop" +@{XDG_DOWNLOAD_DIR}="Downloads" +@{XDG_TEMPLATES_DIR}="Templates" +@{XDG_PUBLICSHARE_DIR}="Public" +@{XDG_DOCUMENTS_DIR}="Documents" +@{XDG_MUSIC_DIR}="Music" +@{XDG_PICTURES_DIR}="Pictures" +@{XDG_VIDEOS_DIR}="Videos" + +# Also, include files in tunables/xdg-user-dirs.d for site-specific adjustments +# to the various XDG directories +#include diff --git a/profiles/apparmor.d/tunables/xdg-user-dirs.d/site.local b/profiles/apparmor.d/tunables/xdg-user-dirs.d/site.local new file mode 100644 index 000000000..8fcabfa0d --- /dev/null +++ b/profiles/apparmor.d/tunables/xdg-user-dirs.d/site.local @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------ +# +# 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. +# +# ------------------------------------------------------------------ + +# The following may be used to add additional entries such as for +# translations. See tunables/xdg-user-dirs for details. Eg: +#@{XDG_MUSIC_DIR}+="Musique" + +#@{XDG_DESKTOP_DIR}+="" +#@{XDG_DOWNLOAD_DIR}+="" +#@{XDG_TEMPLATES_DIR}+="" +#@{XDG_PUBLICSHARE_DIR}+="" +#@{XDG_DOCUMENTS_DIR}+="" +#@{XDG_MUSIC_DIR}+="" +#@{XDG_PICTURES_DIR}+="" +#@{XDG_VIDEOS_DIR}+="" diff --git a/profiles/apparmor.d/usr.lib.dovecot.anvil b/profiles/apparmor.d/usr.lib.dovecot.anvil new file mode 100644 index 000000000..00f77e32c --- /dev/null +++ b/profiles/apparmor.d/usr.lib.dovecot.anvil @@ -0,0 +1,25 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor + +#include + +/usr/lib/dovecot/anvil { + #include + + capability setgid, + capability setuid, + capability sys_chroot, + + /usr/lib/dovecot/anvil mr, + + # Site-specific additions and overrides. See local/README for details. + #include +} diff --git a/profiles/apparmor.d/usr.lib.dovecot.auth b/profiles/apparmor.d/usr.lib.dovecot.auth new file mode 100644 index 000000000..625f8d6a5 --- /dev/null +++ b/profiles/apparmor.d/usr.lib.dovecot.auth @@ -0,0 +1,42 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor + +#include + +/usr/lib/dovecot/auth { + #include + #include + #include + #include + + deny capability block_suspend, + + capability audit_write, + capability setgid, + capability setuid, + + /etc/my.cnf r, + /etc/my.cnf.d/ r, + /etc/my.cnf.d/*.cnf r, + + /etc/dovecot/dovecot-database.conf.ext r, + /etc/dovecot/dovecot-sql.conf.ext r, + /usr/lib/dovecot/auth mr, + + # kerberos replay cache + /var/tmp/imap_* rw, + /var/tmp/pop_* rw, + /var/tmp/sieve_* rw, + /var/tmp/smtp_* rw, + + # Site-specific additions and overrides. See local/README for details. + #include +} diff --git a/profiles/apparmor.d/usr.lib.dovecot.config b/profiles/apparmor.d/usr.lib.dovecot.config new file mode 100644 index 000000000..8ac39923e --- /dev/null +++ b/profiles/apparmor.d/usr.lib.dovecot.config @@ -0,0 +1,32 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor + +#include + +/usr/lib/dovecot/config { + #include + #include + #include + + deny capability block_suspend, + + capability dac_override, + capability setgid, + + + /etc/dovecot/** r, + /usr/bin/doveconf rix, + /usr/lib/dovecot/config mr, + /usr/lib/dovecot/managesieve Px, + + # Site-specific additions and overrides. See local/README for details. + #include +} diff --git a/profiles/apparmor.d/usr.lib.dovecot.deliver b/profiles/apparmor.d/usr.lib.dovecot.deliver index 80b0e9de0..1f4b05536 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.deliver +++ b/profiles/apparmor.d/usr.lib.dovecot.deliver @@ -1,6 +1,19 @@ -# Author: Dulmandakh Sukhbaatar +# ------------------------------------------------------------------ +# +# Copyright (C) 2009 Dulmandakh Sukhbaatar +# Copyright (C) 2009-2012 Canonical Ltd. +# Copyright (C) 2011-2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor #include +#include + /usr/lib/dovecot/deliver { #include #include @@ -8,20 +21,16 @@ capability setgid, capability setuid, + @{DOVECOT_MAILSTORE}/ rw, + @{DOVECOT_MAILSTORE}/** rwkl, + # http://www.postfix.org/SASL_README.html#server_dovecot /etc/dovecot/dovecot.conf r, /etc/dovecot/{auth,conf}.d/*.conf r, - /etc/dovecot/dovecot-postfix.conf r, + /etc/dovecot/dovecot-postfix.conf r, # ??? - @{HOME} r, - @{HOME}/Maildir/ rw, - @{HOME}/Maildir/** klrw, - @{HOME}/mail/ rw, - @{HOME}/mail/* klrw, - @{HOME}/mail/.imap/** klrw, + @{HOME} r, # ??? /usr/lib/dovecot/deliver mr, - /var/mail/* klrw, - /var/spool/mail/* klrw, # Site-specific additions and overrides. See local/README for details. #include diff --git a/profiles/apparmor.d/usr.lib.dovecot.dict b/profiles/apparmor.d/usr.lib.dovecot.dict new file mode 100644 index 000000000..021fdb11c --- /dev/null +++ b/profiles/apparmor.d/usr.lib.dovecot.dict @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor + +#include + +/usr/lib/dovecot/dict { + #include + #include + #include + + capability setgid, + capability setuid, + + network inet stream, + + /etc/dovecot/dovecot-database.conf.ext r, + /etc/dovecot/dovecot-dict-sql.conf.ext r, + /usr/lib/dovecot/dict mr, + + # Site-specific additions and overrides. See local/README for details. + #include +} diff --git a/profiles/apparmor.d/usr.lib.dovecot.dovecot-auth b/profiles/apparmor.d/usr.lib.dovecot.dovecot-auth index 556353630..25f125135 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.dovecot-auth +++ b/profiles/apparmor.d/usr.lib.dovecot.dovecot-auth @@ -1,6 +1,17 @@ -# Author: Kees Cook +# ------------------------------------------------------------------ +# +# Copyright (C) 2009-2013 Canonical Ltd. +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor #include + /usr/lib/dovecot/dovecot-auth { #include #include diff --git a/profiles/apparmor.d/usr.lib.dovecot.dovecot-lda b/profiles/apparmor.d/usr.lib.dovecot.dovecot-lda new file mode 100644 index 000000000..b74c92253 --- /dev/null +++ b/profiles/apparmor.d/usr.lib.dovecot.dovecot-lda @@ -0,0 +1,33 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor + +#include +#include + +/usr/lib/dovecot/dovecot-lda { + #include + #include + + capability setgid, + capability setuid, + + @{DOVECOT_MAILSTORE}/ rw, + @{DOVECOT_MAILSTORE}/** rwkl, + + /etc/dovecot/** r, + /proc/*/mounts r, + /{var/,}run/dovecot/mounts r, + /usr/bin/doveconf mrix, + /usr/lib/dovecot/dovecot-lda mrix, + + # Site-specific additions and overrides. See local/README for details. + #include +} diff --git a/profiles/apparmor.d/usr.lib.dovecot.imap b/profiles/apparmor.d/usr.lib.dovecot.imap index d490e3157..74ea18d3a 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.imap +++ b/profiles/apparmor.d/usr.lib.dovecot.imap @@ -1,6 +1,18 @@ -# Author: Kees Cook +# ------------------------------------------------------------------ +# +# Copyright (C) 2009-2010 Canonical Ltd. +# Copyright (C) 2011-2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor #include +#include + /usr/lib/dovecot/imap { #include #include @@ -8,18 +20,11 @@ capability setgid, capability setuid, - @{HOME} r, - @{HOME}/Maildir/ rw, - @{HOME}/Maildir/** klrw, - @{HOME}/Mail/ rw, - @{HOME}/Mail/* klrw, - @{HOME}/Mail/.imap/** klrw, - @{HOME}/mail/ rw, - @{HOME}/mail/* klrw, - @{HOME}/mail/.imap/** klrw, + @{DOVECOT_MAILSTORE}/ rw, + @{DOVECOT_MAILSTORE}/** rwkl, + + @{HOME} r, # ??? /usr/lib/dovecot/imap mr, - /var/mail/* klrw, - /var/spool/mail/* klrw, # Site-specific additions and overrides. See local/README for details. #include diff --git a/profiles/apparmor.d/usr.lib.dovecot.imap-login b/profiles/apparmor.d/usr.lib.dovecot.imap-login index 31a4afb6d..2e5eca482 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.imap-login +++ b/profiles/apparmor.d/usr.lib.dovecot.imap-login @@ -1,4 +1,14 @@ -# Author: Kees Cook +# ------------------------------------------------------------------ +# +# Copyright (C) 2009-2011 Canonical Ltd. +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor #include /usr/lib/dovecot/imap-login { diff --git a/profiles/apparmor.d/usr.lib.dovecot.lmtp b/profiles/apparmor.d/usr.lib.dovecot.lmtp new file mode 100644 index 000000000..2c43d4c39 --- /dev/null +++ b/profiles/apparmor.d/usr.lib.dovecot.lmtp @@ -0,0 +1,35 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor + +#include +#include + +/usr/lib/dovecot/lmtp { + #include + #include + + deny capability block_suspend, + + capability dac_override, + capability setgid, + capability setuid, + + @{DOVECOT_MAILSTORE}/ rw, + @{DOVECOT_MAILSTORE}/** rwkl, + + /proc/*/mounts r, + /tmp/dovecot.lmtp.* rw, + /usr/lib/dovecot/lmtp mr, + /{var/,}run/dovecot/mounts r, + + # Site-specific additions and overrides. See local/README for details. + #include +} diff --git a/profiles/apparmor.d/usr.lib.dovecot.log b/profiles/apparmor.d/usr.lib.dovecot.log new file mode 100644 index 000000000..34a417560 --- /dev/null +++ b/profiles/apparmor.d/usr.lib.dovecot.log @@ -0,0 +1,25 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor + +#include + +/usr/lib/dovecot/log { + #include + + deny capability block_suspend, + + capability setgid, + + /usr/lib/dovecot/log mr, + + # Site-specific additions and overrides. See local/README for details. + #include +} diff --git a/profiles/apparmor.d/usr.lib.dovecot.managesieve b/profiles/apparmor.d/usr.lib.dovecot.managesieve new file mode 100644 index 000000000..3b39f0d25 --- /dev/null +++ b/profiles/apparmor.d/usr.lib.dovecot.managesieve @@ -0,0 +1,23 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor + +#include + +/usr/lib/dovecot/managesieve { + #include + + /etc/dovecot/** r, + /usr/bin/doveconf rix, + /usr/lib/dovecot/managesieve mrix, + + # Site-specific additions and overrides. See local/README for details. + #include +} diff --git a/profiles/apparmor.d/usr.lib.dovecot.managesieve-login b/profiles/apparmor.d/usr.lib.dovecot.managesieve-login index 0a54cf307..1cda8a0c7 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.managesieve-login +++ b/profiles/apparmor.d/usr.lib.dovecot.managesieve-login @@ -1,4 +1,15 @@ -# Author: Dulmandakh Sukhbaatar +# ------------------------------------------------------------------ +# +# Copyright (c) 2009 Dulmandakh Sukhbaatar +# Copyright (C) 2009-2011 Canonical Ltd. +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor #include /usr/lib/dovecot/managesieve-login { diff --git a/profiles/apparmor.d/usr.lib.dovecot.pop3 b/profiles/apparmor.d/usr.lib.dovecot.pop3 index 841aea7cb..00782daf4 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.pop3 +++ b/profiles/apparmor.d/usr.lib.dovecot.pop3 @@ -1,6 +1,18 @@ -# Author: Kees Cook +# ------------------------------------------------------------------ +# +# Copyright (C) 2009-2010 Canonical Ltd. +# Copyright (C) 2011-2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor #include +#include + /usr/lib/dovecot/pop3 { #include #include @@ -8,13 +20,10 @@ capability setgid, capability setuid, - /var/mail/* klrw, - /var/spool/mail/* klrw, - @{HOME} r, - @{HOME}/mail/* klrw, - @{HOME}/mail/.imap/** klrw, - @{HOME}/Maildir/ rw, - @{HOME}/Maildir/** klrw, + @{DOVECOT_MAILSTORE}/ rw, + @{DOVECOT_MAILSTORE}/** rwkl, + + @{HOME} r, # ??? /usr/lib/dovecot/pop3 mr, # Site-specific additions and overrides. See local/README for details. diff --git a/profiles/apparmor.d/usr.lib.dovecot.pop3-login b/profiles/apparmor.d/usr.lib.dovecot.pop3-login index 01300ab28..b500f9c1c 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.pop3-login +++ b/profiles/apparmor.d/usr.lib.dovecot.pop3-login @@ -1,6 +1,17 @@ -# Author: Kees Cook +# ------------------------------------------------------------------ +# +# Copyright (C) 2009-2011 Canonical Ltd. +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor #include + /usr/lib/dovecot/pop3-login { #include #include diff --git a/profiles/apparmor.d/usr.lib.dovecot.ssl-params b/profiles/apparmor.d/usr.lib.dovecot.ssl-params new file mode 100644 index 000000000..704dfcb66 --- /dev/null +++ b/profiles/apparmor.d/usr.lib.dovecot.ssl-params @@ -0,0 +1,27 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor + +#include + +/usr/lib/dovecot/ssl-params { + #include + + deny capability block_suspend, + + capability setgid, + + /usr/lib/dovecot/ssl-params mr, + /var/lib/dovecot/ssl-parameters.dat rw, + /var/lib/dovecot/ssl-parameters.dat.tmp rwk, + + # Site-specific additions and overrides. See local/README for details. + #include +} diff --git a/profiles/apparmor.d/usr.sbin.apache2 b/profiles/apparmor.d/usr.sbin.apache2 new file mode 100644 index 000000000..95c477860 --- /dev/null +++ b/profiles/apparmor.d/usr.sbin.apache2 @@ -0,0 +1,83 @@ +# Author: Marc Deslauriers + +#include +/usr/sbin/apache2 { + + # This profile is completely permissive. + # It is designed to target specific applications using mod_apparmor, + # hats, and the apache2.d directory. + # + # In order to enable this profile, you must: + # + # 1- Enable it: + # sudo aa-enforce /etc/apparmor.d/usr.sbin.apache2 + # + # 2- Load the mpm_prefork and mod_apparmor modules: + # sudo a2dismod + # sudo a2enmod mpm_prefork + # sudo a2enmod apparmor + # sudo service apache2 restart + # + # 3- Place an appropriate profile containing the desired hat in the + # /etc/apparmor.d/apache2.d directory. Such profiles should probably + # include the "apache2-common" abstraction. + # + # 4- Use the "AADefaultHatName" apache configuration option to specify a + # hat to be used for a given apache virtualhost or "AAHatName" for + # a given apache directory or location directive. + # + # + # There is an example profile for phpsysinfo included in the + # apparmor-profiles package. To try it: + # + # 1- Install the phpsysinfo and the apparmor-profiles packages: + # sudo apt-get install phpsysinfo apparmor-profiles + # + # 2- Enable the main apache2 profile + # sudo aa-enforce /etc/apparmor.d/usr.sbin.apache2 + # + # 3- Configure apache with the following: + # + # AAHatName phpsysinfo + # + # + + #include + #include + + capability dac_override, + capability kill, + capability net_bind_service, + capability setgid, + capability setuid, + capability sys_tty_config, + + / rw, + /** mrwlkix, + + + ^DEFAULT_URI { + #include + #include + + / rw, + /** mrwlkix, + + } + + ^HANDLING_UNTRUSTED_INPUT { + #include + + / rw, + /** mrwlkix, + + } + + # This directory contains web application + # package-specific apparmor files. + + #include + + # 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 6b20b93d8..0bb30eb2c 100644 --- a/profiles/apparmor.d/usr.sbin.dnsmasq +++ b/profiles/apparmor.d/usr.sbin.dnsmasq @@ -29,6 +29,8 @@ /etc/dnsmasq.d/ r, /etc/dnsmasq.d/* r, /etc/ethers r, + /etc/NetworkManager/dnsmasq.d/ r, + /etc/NetworkManager/dnsmasq.d/* r, /usr/sbin/dnsmasq mr, @@ -56,6 +58,7 @@ /{,var/}run/nm-dns-dnsmasq.conf r, /{,var/}run/sendsigs.omit.d/*dnsmasq.pid w, /{,var/}run/NetworkManager/dnsmasq.conf r, + /{,var/}run/NetworkManager/dnsmasq.pid w, # Site-specific additions and overrides. See local/README for details. #include diff --git a/profiles/apparmor.d/usr.sbin.dovecot b/profiles/apparmor.d/usr.sbin.dovecot index 2ee8f8782..f676100a7 100644 --- a/profiles/apparmor.d/usr.sbin.dovecot +++ b/profiles/apparmor.d/usr.sbin.dovecot @@ -1,37 +1,61 @@ -# Author: Kees Cook +# ------------------------------------------------------------------ +# +# Copyright (C) 2009-2013 Canonical Ltd. +# Copyright (C) 2011-2013 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. +# +# ------------------------------------------------------------------ +# vim: ft=apparmor #include + /usr/sbin/dovecot { #include #include + #include #include #include #include capability chown, + capability dac_override, + capability fsetid, + capability kill, capability net_bind_service, capability setgid, capability setuid, capability sys_chroot, - capability fsetid, /etc/dovecot/** r, /etc/mtab r, /etc/lsb-release r, /etc/SuSE-release r, @{PROC}/@{pid}/mounts r, + @{PROC}/filesystems r, + /usr/bin/doveconf rix, + /usr/lib/dovecot/anvil Px, + /usr/lib/dovecot/auth Px, + /usr/lib/dovecot/config Px, + /usr/lib/dovecot/dict Px, /usr/lib/dovecot/dovecot-auth Pxmr, /usr/lib/dovecot/imap Pxmr, /usr/lib/dovecot/imap-login Pxmr, + /usr/lib/dovecot/lmtp Px, + /usr/lib/dovecot/log Px, + /usr/lib/dovecot/managesieve Px, + /usr/lib/dovecot/managesieve-login Pxmr, /usr/lib/dovecot/pop3 Px, /usr/lib/dovecot/pop3-login Pxmr, - # temporarily commented out while testing - #/usr/lib/dovecot/managesieve Px, - /usr/lib/dovecot/managesieve-login Pxmr, - /usr/lib/dovecot/ssl-build-param ixr, - /usr/sbin/dovecot mr, + /usr/lib/dovecot/ssl-build-param rix, + /usr/lib/dovecot/ssl-params Px, + /usr/sbin/dovecot mrix, /var/lib/dovecot/ w, - /var/lib/dovecot/* krw, + /var/lib/dovecot/* rwkl, + /var/spool/postfix/private/auth w, + /var/spool/postfix/private/dovecot-lmtp w, /{,var/}run/dovecot/ rw, /{,var/}run/dovecot/** rw, link /{,var/}run/dovecot/** -> /var/lib/dovecot/**, diff --git a/profiles/apparmor.d/usr.sbin.smbd b/profiles/apparmor.d/usr.sbin.smbd index fc257671a..e4072d951 100644 --- a/profiles/apparmor.d/usr.sbin.smbd +++ b/profiles/apparmor.d/usr.sbin.smbd @@ -36,7 +36,6 @@ /var/cache/samba/** rwk, /var/cache/samba/printing/printers.tdb mrw, /var/lib/samba/** rwk, - /var/lib/sss/mc/passwd r, /var/lib/sss/pubconf/kdcinfo.* r, /{,var/}run/cups/cups.sock rw, /{,var/}run/dbus/system_bus_socket rw, diff --git a/profiles/apparmor.d/usr.sbin.winbindd b/profiles/apparmor.d/usr.sbin.winbindd index d9ca72e15..47ae81a44 100644 --- a/profiles/apparmor.d/usr.sbin.winbindd +++ b/profiles/apparmor.d/usr.sbin.winbindd @@ -1,33 +1,32 @@ -# Last Modified: Mon Mar 26 20:28:18 2012 #include /usr/sbin/winbindd { #include #include + #include + + deny capability block_suspend, + + capability ipc_lock, + capability setuid, - /etc/samba/dhcp.conf r, /etc/samba/passdb.tdb rwk, /etc/samba/secrets.tdb rwk, @{PROC}/sys/kernel/core_pattern r, /tmp/.winbindd/ w, + /tmp/krb5cc_* rwk, /usr/lib*/samba/idmap/*.so mr, /usr/lib*/samba/nss_info/*.so mr, + /usr/lib*/samba/pdb/*.so mr, /usr/sbin/winbindd mr, - /var/lib/samba/account_policy.tdb rwk, - /var/lib/samba/gencache.tdb rwk, - /var/lib/samba/gencache_notrans.tdb rwk, - /var/lib/samba/group_mapping.tdb rwk, - /var/lib/samba/messages.tdb rwk, - /var/lib/samba/netsamlogon_cache.tdb rwk, - /var/lib/samba/serverid.tdb rwk, - /var/lib/samba/winbindd_cache.tdb rwk, - /var/lib/samba/winbindd_privileged/pipe w, - /var/log/samba/cores/ rw, - /var/log/samba/cores/winbindd/ rw, - /var/log/samba/cores/winbindd/** rw, - /var/log/samba/log.wb-* w, + /var/cache/samba/*.tdb rwk, + /var/lib/samba/smb_krb5/krb5.conf.* rw, + /var/lib/samba/smb_tmp_krb5.* rw, + /var/lib/samba/winbindd_cache.tdb* rwk, /var/log/samba/log.winbindd rw, /{var/,}run/samba/winbindd.pid rwk, + /{var/,}run/samba/winbindd/ rw, + /{var/,}run/samba/winbindd/pipe w, # Site-specific additions and overrides. See local/README for details. #include diff --git a/tests/regression/apparmor/Makefile b/tests/regression/apparmor/Makefile index 1c4d01a5a..5ae5f45f3 100644 --- a/tests/regression/apparmor/Makefile +++ b/tests/regression/apparmor/Makefile @@ -186,16 +186,16 @@ changehat_pthread: changehat_pthread.c changehat.h ${CC} ${CFLAGS} ${LDFLAGS} $< -o $@ ${LDLIBS} -pthread dbus_common.o: dbus_common.c dbus_common.h - ${CC} ${CFLAGS} ${LDFLAGS} $^ -c ${LDLIBS} $(shell pkg-config --cflags --libs dbus-1) + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} $(shell pkg-config --cflags --libs dbus-1) dbus_eavesdrop: dbus_eavesdrop.c dbus_common.o - ${CC} ${CFLAGS} ${LDFLAGS} $^ -o dbus_eavesdrop ${LDLIBS} $(shell pkg-config --cflags --libs dbus-1) + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} $(shell pkg-config --cflags --libs dbus-1) dbus_message: dbus_message.c dbus_common.o - ${CC} ${CFLAGS} ${LDFLAGS} $^ -o dbus_message ${LDLIBS} $(shell pkg-config --cflags --libs dbus-1) + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} $(shell pkg-config --cflags --libs dbus-1) dbus_service: dbus_message dbus_service.c dbus_common.o - ${CC} ${CFLAGS} ${LDFLAGS} $(filter-out dbus_message, $^) -o dbus_service ${LDLIBS} $(shell pkg-config --cflags --libs dbus-1) + ${CC} ${CFLAGS} ${LDFLAGS} $(filter-out dbus_message, $^) -o $@ ${LDLIBS} $(shell pkg-config --cflags --libs dbus-1) tests: all @if [ `whoami` = "root" ] ;\ diff --git a/utils/Makefile b/utils/Makefile index 744a50039..2a9893cc3 100644 --- a/utils/Makefile +++ b/utils/Makefile @@ -26,16 +26,14 @@ common/Make.rules: $(COMMONDIR)/Make.rules ln -sf $(COMMONDIR) . endif -MODDIR = Immunix -PERLTOOLS = aa-genprof aa-logprof aa-autodep aa-audit aa-complain aa-enforce \ - aa-unconfined aa-notify aa-disable aa-exec -TOOLS = ${PERLTOOLS} aa-decode aa-status -MODULES = ${MODDIR}/AppArmor.pm ${MODDIR}/Repository.pm \ - ${MODDIR}/Config.pm ${MODDIR}/Severity.pm -PYTOOLS = aa-easyprof +PERLTOOLS = aa-exec 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 -MANPAGES = ${TOOLS:=.8} logprof.conf.5 ${PYTOOLS:=.8} +MANPAGES = ${TOOLS:=.8} logprof.conf.5 all: ${MANPAGES} ${HTMLMANPAGES} $(MAKE) -C po all @@ -45,12 +43,10 @@ all: ${MANPAGES} ${HTMLMANPAGES} DESTDIR=/ BINDIR=${DESTDIR}/usr/sbin CONFDIR=${DESTDIR}/etc/apparmor -VENDOR_PERL=$(shell perl -e 'use Config; print $$Config{"vendorlib"};') -PERLDIR=${DESTDIR}${VENDOR_PERL}/${MODDIR} PYPREFIX=/usr -po/${NAME}.pot: ${TOOLS} ${PYTOOLS} - $(MAKE) -C po ${NAME}.pot NAME=${NAME} SOURCES="${TOOLS} ${MODULES} ${PYTOOLS}" +po/${NAME}.pot: ${TOOLS} + $(MAKE) -C po ${NAME}.pot NAME=${NAME} SOURCES="${TOOLS} ${MODULES}" .PHONY: install install: ${MANPAGES} ${HTMLMANPAGES} @@ -59,8 +55,6 @@ install: ${MANPAGES} ${HTMLMANPAGES} install -d ${BINDIR} ln -sf aa-status ${BINDIR}/apparmor_status install -m 755 ${TOOLS} ${BINDIR} - install -d ${PERLDIR} - install -m 644 ${MODULES} ${PERLDIR} $(MAKE) -C po install DESTDIR=${DESTDIR} NAME=${NAME} $(MAKE) install_manpages DESTDIR=${DESTDIR} $(MAKE) -C vim install DESTDIR=${DESTDIR} @@ -78,7 +72,7 @@ clean: _clean $(MAKE) -C vim clean rm -rf staging/ build/ rm -f apparmor/*.pyc - rm -rf test/__pycache__/ + rm -rf test/__pycache__/ apparmor/__pycache__/ # ${CAPABILITIES} is defined in common/Make.rules .PHONY: check_severity_db @@ -100,7 +94,7 @@ check: check_severity_db perl -c $$i || exit 1; \ done tmpfile=$$(mktemp --tmpdir aa-pyflakes-XXXXXX); \ - for i in ${PYTOOLS} apparmor aa-status test/*.py; do \ + for i in ${PYTOOLS} apparmor test/*.py; do \ echo Checking $$i; \ pyflakes $$i 2>&1 | grep -v "undefined name '_'" > $$tmpfile; \ test -s $$tmpfile && cat $$tmpfile && rm -f $$tmpfile && exit 1; \ diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 000000000..f7b0f4827 --- /dev/null +++ b/utils/README.md @@ -0,0 +1,3 @@ +Known Bugs: +Will allow multiple letters in the () due to translation/unicode issues with regexing the key. +User input will probably bug out in a different locale. diff --git a/utils/aa-audit b/utils/aa-audit index 8ddec1cac..092a8cd32 100755 --- a/utils/aa-audit +++ b/utils/aa-audit @@ -1,7 +1,6 @@ -#!/usr/bin/perl +#! /usr/bin/env python # ---------------------------------------------------------------------- -# Copyright (c) 2005 Novell, Inc. All Rights Reserved. -# Copyright (c) 2011 Canonical, Ltd. +# 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 @@ -12,121 +11,30 @@ # 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. -# -# To contact Novell about this file by physical or electronic mail, -# you may find current contact information at www.novell.com. # ---------------------------------------------------------------------- +import argparse +import traceback -use strict; -use FindBin; -use Getopt::Long; +import apparmor.tools -use Immunix::AppArmor; +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() -use Data::Dumper; +parser = argparse.ArgumentParser(description=_('Switch the given programs to audit mode')) +parser.add_argument('-d', '--dir', type=str, help=_('path to profiles')) +parser.add_argument('-r', '--remove', action='store_true', help=_('remove audit mode')) +parser.add_argument('program', type=str, nargs='+', help=_('name of program')) +parser.add_argument('--trace', action='store_true', help=_('Show full trace')) +args = parser.parse_args() -use Locale::gettext; -use POSIX; +try: + audit = apparmor.tools.aa_tools('audit', args) -# initialize the local poo -setlocale(LC_MESSAGES, ""); -textdomain("apparmor-utils"); - -$UI_Mode = "text"; - -# options variables -my $help = ''; - -GetOptions( - 'dir|d=s' => \$profiledir, - 'help|h' => \$help, -); - -# tell 'em how to use it... -&usage && exit if $help; - -# let's convert it to full path... -$profiledir = get_full_path($profiledir); - -unless (-d $profiledir) { - UI_Important("Can't find AppArmor profiles in $profiledir."); - exit 1; -} - -# what are we profiling? -my @profiling = @ARGV; - -unless (@profiling) { - @profiling = (UI_GetString("Please enter the program to switch to audit mode: ", "")); -} - -for my $profiling (@profiling) { - - next unless $profiling; - - my $fqdbin; - if (-e $profiling) { - $fqdbin = get_full_path($profiling); - chomp($fqdbin); - } else { - if ($profiling !~ /\//) { - opendir(DIR,$profiledir); - my @tmp_fqdbin = grep ( /$profiling/, readdir(DIR)); - closedir(DIR); - if (scalar @tmp_fqdbin eq 1) { - $fqdbin = "$profiledir/$tmp_fqdbin[0]"; - } else { - my $which = which($profiling); - if ($which) { - $fqdbin = get_full_path($which); - } - } - } - } - - if (-e $fqdbin) { - - my $filename; - if ($fqdbin =~ /^$profiledir\//) { - $filename = $fqdbin; - } else { - $filename = getprofilefilename($fqdbin); - } - - # argh, skip directories - next unless -f $filename; - - # skip rpm backup files - next if isSkippableFile($filename); - - printf(gettext('Setting %s to audit mode.'), $fqdbin); - print "\n"; - setprofileflags($filename, "audit"); - - my $cmd_info = qx(cat $filename | $parser -I$profiledir -r 2>&1 1>/dev/null); - if ($? != 0) { - UI_Info($cmd_info); - exit $?; - } - -# if check_for_subdomain(); - } else { - if ($profiling =~ /^[^\/]+$/) { - UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); - exit 1; - } else { - UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); - exit 1; - } - } -} - -exit 0; - -sub usage { - UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ program to switch to audit mode ]"), $0)); - exit 0; -} + audit.act() +except Exception as e: + if not args.trace: + print(e.value + "\n") + else: + traceback.print_exc() diff --git a/utils/aa-audit.pod b/utils/aa-audit.pod index 6d3f0f398..9898782fc 100644 --- a/utils/aa-audit.pod +++ b/utils/aa-audit.pod @@ -2,17 +2,30 @@ =head1 NAME -aa-audit - set a AppArmor security profile to I mode. +aa-audit - set an AppArmor security profile to I mode. =head1 SYNOPSIS -BexecutableE> [IexecutableE> ...]> +BexecutableE> [IexecutableE> ...] [I<-d /path/to/profiles>] [I<-r>]> + +=head1 OPTIONS + +B<-d --dir /path/to/profiles> + + Specifies where to look for the AppArmor security profile set. + Defaults to /etc/apparmor.d. + +B<-r --remove> + + Removes the audit mode for the profile. =head1 DESCRIPTION -B is used to set the audit mode for one or more profiles to audit. +B is used to set one or more profiles to audit mode. In this mode security policy is enforced and all access (successes and failures) are logged to the system log. +The I<--remove> option can be used to remove the audit mode for the profile. + =head1 BUGS If you find any bugs, please report them at diff --git a/utils/aa-autodep b/utils/aa-autodep index 3d28b642a..16ac97bdd 100755 --- a/utils/aa-autodep +++ b/utils/aa-autodep @@ -1,7 +1,6 @@ -#!/usr/bin/perl +#! /usr/bin/env python # ---------------------------------------------------------------------- -# Copyright (c) 2005 Novell, Inc. All Rights Reserved. -# Copyright (c) 2011 Canonical, Ltd. +# 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 @@ -12,111 +11,21 @@ # 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. -# -# To contact Novell about this file by physical or electronic mail, -# you may find current contact information at www.novell.com. # ---------------------------------------------------------------------- +import argparse -use strict; -use FindBin; -use Getopt::Long; +import apparmor.tools -use Immunix::AppArmor; +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() -use Data::Dumper; +parser = argparse.ArgumentParser(description=_('Generate a basic AppArmor profile by guessing requirements')) +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')) +args = parser.parse_args() -use Locale::gettext; -use POSIX; - -# force $PATH to be sane -$ENV{PATH} = "/bin:/sbin:/usr/bin:/usr/sbin"; - -# initialize the local poo -setlocale(LC_MESSAGES, ""); -textdomain("apparmor-utils"); - -$UI_Mode = "text"; - -# options variables -my $help = ''; -my $force = undef; - -GetOptions( - 'force' => \$force, - 'dir|d=s' => \$profiledir, - 'help|h' => \$help, -); - -# tell 'em how to use it... -&usage && exit if $help; - -my $sd_mountpoint = check_for_subdomain(); - -# let's convert it to full path... -$profiledir = get_full_path($profiledir); - -unless (-d $profiledir) { - UI_Important(sprintf(gettext('Can\'t find AppArmor profiles in %s.'), $profiledir)); - exit 1; -} - -# what are we profiling? -my @profiling = @ARGV; - -unless (@profiling) { - @profiling = (UI_GetString(gettext("Please enter the program to create a profile for: "), "")); -} - -for my $profiling (@profiling) { - - next unless $profiling; - - my $fqdbin; - if (-e $profiling) { - $fqdbin = get_full_path($profiling); - chomp($fqdbin); - } else { - if ($profiling !~ /\//) { - my $which = which($profiling); - if ($which) { - $fqdbin = get_full_path($which); - } - } - } - - # make sure that the app they're requesting to profile is not marked as - # not allowed to have it's own profile - if ($qualifiers{$fqdbin}) { - unless ($qualifiers{$fqdbin} =~ /p/) { - UI_Info(sprintf(gettext('%s is currently marked as a program that should not have it\'s own profile. Usually, programs are marked this way if creating a profile for them is likely to break the rest of the system. If you know what you\'re doing and are certain you want to create a profile for this program, edit the corresponding entry in the [qualifiers] section in /etc/apparmor/logprof.conf.'), $fqdbin)); - exit 1; - } - } - - if (-e $fqdbin) { - if (-e getprofilefilename($fqdbin) && !$force) { - UI_Info(sprintf(gettext('Profile for %s already exists - skipping.'), $fqdbin)); - } else { - autodep($fqdbin); - reload($fqdbin) if $sd_mountpoint; - } - } else { - if ($profiling =~ /^[^\/]+$/) { - UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); - exit 1; - } else { - UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); - exit 1; - } - } -} - -exit 0; - -sub usage { - UI_Info("usage: $0 [ --force ] [ -d /path/to/profiles ]"); - exit 0; -} +autodep = apparmor.tools.aa_tools('autodep', args) +autodep.act() diff --git a/utils/aa-autodep.pod b/utils/aa-autodep.pod index 441b49265..06ae2f2b5 100644 --- a/utils/aa-autodep.pod +++ b/utils/aa-autodep.pod @@ -26,7 +26,18 @@ aa-autodep - guess basic AppArmor profile requirements =head1 SYNOPSIS -BexecutableE> [IexecutableE> ...]> +BexecutableE> [IexecutableE> ...] [I<-d /path/to/profiles>] [I<-f>]> + +=head1 OPTIONS + +B<-d --dir /path/to/profiles> + + Specifies where to look for the AppArmor security profile set. + Defaults to /etc/apparmor.d. + +B<-f --force> + + Overwrites any existing AppArmor profile for the executable with the generated minimal AppArmor profile. =head1 DESCRIPTION @@ -37,6 +48,9 @@ a base profile containing a base include directive which includes basic profile entries needed by most programs. The profile is generated by recursively calling ldd(1) on the executables listed on the command line. +The I<--force> option will overwrite any existing profile for the executable with +the newly generated minimal AppArmor profile. + =head1 BUGS This program does not perform full static analysis of executables, so diff --git a/utils/aa-cleanprof b/utils/aa-cleanprof new file mode 100755 index 000000000..ed23d3b39 --- /dev/null +++ b/utils/aa-cleanprof @@ -0,0 +1,31 @@ +#! /usr/bin/env python +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import argparse + +import apparmor.tools + +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() + +parser = argparse.ArgumentParser(description=_('Cleanup the profiles for the given programs')) +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('-s', '--silent', action='store_true', help=_('Silently overwrite with a clean profile')) +args = parser.parse_args() + +clean = apparmor.tools.aa_tools('cleanprof', args) + +clean.act() diff --git a/utils/aa-cleanprof.pod b/utils/aa-cleanprof.pod new file mode 100644 index 000000000..1651b5a55 --- /dev/null +++ b/utils/aa-cleanprof.pod @@ -0,0 +1,39 @@ +=pod + +=head1 NAME + +aa-cleanprof - clean an existing AppArmor security profile. + +=head1 SYNOPSIS + +BexecutableE> [IexecutableE> ...] [I<-d /path/to/profiles>] [I<-s>]> + +=head1 OPTIONS + +B<-d --dir /path/to/profiles> + + Specifies where to look for the AppArmor security profile set. + Defaults to /etc/apparmor.d. + +B<-s --silent> + + Silently overwrites the profile without user prompt. + +=head1 DESCRIPTION + +B is used to perform a cleanup on one or more profiles. +The tool removes any existing superfluous rules (rules that are covered +under an include or another rule), reorders the rules to group similar rules +together and removes all comments from the file. + +=head1 BUGS + +If you find any bugs, please report them at +L. + +=head1 SEE ALSO + +apparmor(7), apparmor.d(5), aa-enforce(1), aa-complain(1), aa-disable(1), +aa_change_hat(2), and L. + +=cut diff --git a/utils/aa-complain b/utils/aa-complain index 5e497e05f..b6caa572e 100755 --- a/utils/aa-complain +++ b/utils/aa-complain @@ -1,6 +1,6 @@ -#!/usr/bin/perl +#! /usr/bin/env python # ---------------------------------------------------------------------- -# Copyright (c) 2005 Novell, Inc. All Rights Reserved. +# 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 @@ -11,121 +11,21 @@ # 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. -# -# To contact Novell about this file by physical or electronic mail, -# you may find current contact information at www.novell.com. # ---------------------------------------------------------------------- +import argparse -use strict; -use FindBin; -use Getopt::Long; +import apparmor.tools -use Immunix::AppArmor; +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() -use Data::Dumper; - -use Locale::gettext; -use POSIX; - -# initialize the local poo -setlocale(LC_MESSAGES, ""); -textdomain("apparmor-utils"); - -$UI_Mode = "text"; - -# options variables -my $help = ''; - -GetOptions( - 'dir|d=s' => \$profiledir, - 'help|h' => \$help, -); - -# tell 'em how to use it... -&usage && exit if $help; - -# let's convert it to full path... -$profiledir = get_full_path($profiledir); - -unless (-d $profiledir) { - UI_Important("Can't find AppArmor profiles in $profiledir."); - exit 1; -} - -# what are we profiling? -my @profiling = @ARGV; - -unless (@profiling) { - @profiling = (UI_GetString(gettext("Please enter the program to switch to complain mode: "), "")); -} - -for my $profiling (@profiling) { - - next unless $profiling; - - my $fqdbin; - if (-e $profiling) { - $fqdbin = get_full_path($profiling); - chomp($fqdbin); - } else { - if ($profiling !~ /\//) { - opendir(DIR,$profiledir); - my @tmp_fqdbin = grep ( /$profiling/, readdir(DIR)); - closedir(DIR); - if (scalar @tmp_fqdbin eq 1) { - $fqdbin = "$profiledir/$tmp_fqdbin[0]"; - } else { - my $which = which($profiling); - if ($which) { - $fqdbin = get_full_path($which); - } - } - } - } - - if (-e $fqdbin) { - - my $filename; - if ($fqdbin =~ /^$profiledir\//) { - $filename = $fqdbin; - } else { - $filename = getprofilefilename($fqdbin); - } - - # argh, skip directories - next unless -f $filename; - - # skip rpm backup files - next if isSkippableFile($filename); - - printf(gettext('Setting %s to complain mode.'), $fqdbin); - print "\n"; - setprofileflags($filename, "complain"); - - my $cmd_info = qx(cat $filename | $parser -I$profiledir -r 2>&1 1>/dev/null); - if ($? != 0) { - UI_Info($cmd_info); - exit $?; - } - -# if check_for_subdomain(); - } else { - if ($profiling =~ /^[^\/]+$/) { - UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); - exit 1; - } else { - UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); - exit 1; - } - } -} - -exit 0; - -sub usage { - UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ program to switch to complain mode ]"), $0)); - exit 0; -} +parser = argparse.ArgumentParser(description=_('Switch the given program to complain mode')) +parser.add_argument('-d', '--dir', type=str, help=_('path to profiles')) +parser.add_argument('-r', '--remove', action='store_true', help=_('remove complain mode')) +parser.add_argument('program', type=str, nargs='+', help=_('name of program')) +args = parser.parse_args() +complain = apparmor.tools.aa_tools('complain', args) +#print(args) +complain.act() diff --git a/utils/aa-complain.pod b/utils/aa-complain.pod index c36c09b3b..8607e85d0 100644 --- a/utils/aa-complain.pod +++ b/utils/aa-complain.pod @@ -22,17 +22,31 @@ =head1 NAME -aa-complain - set a AppArmor security profile to I mode. +aa-complain - set an AppArmor security profile to I mode. =head1 SYNOPSIS -BexecutableE> [IexecutableE> ...]> +BexecutableE> [IexecutableE> ...] [I<-d /path/to/profiles>] [I<-r>]> + +=head1 OPTIONS + +B<-d --dir /path/to/profiles> + + Specifies where to look for the AppArmor security profile set. + Defaults to /etc/apparmor.d. + +B<-r --remove> + + Removes the complain mode for the profile. =head1 DESCRIPTION -B is used to set the enforcement mode for one or more profiles to -complain. In this mode security policy is not enforced but rather access -violations are logged to the system log. +B is used to set the enforcement mode for one or more profiles to I mode. +In this mode security policy is not enforced but rather access violations +are logged to the system log. + +The I<--remove> option can be used to remove the complain mode for the profile, +setting it to enforce mode by default. =head1 BUGS diff --git a/utils/aa-disable b/utils/aa-disable index 2cc19a55a..d7200138d 100755 --- a/utils/aa-disable +++ b/utils/aa-disable @@ -1,7 +1,6 @@ -#!/usr/bin/perl +#! /usr/bin/env python # ---------------------------------------------------------------------- -# Copyright (c) 2005-2010 Novell, Inc. All Rights Reserved. -# Copyright (c) 2011 Canonical, Inc. All Rights Reserved. +# 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 @@ -12,141 +11,22 @@ # 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 Canonical, Inc. -# -# To contact Canonical about this file by physical or electronic mail, -# you may find current contact information at www.canonical.com. # ---------------------------------------------------------------------- +import argparse -use strict; -use FindBin; -use Getopt::Long; +import apparmor.tools -use Immunix::AppArmor; +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() -use Data::Dumper; +parser = argparse.ArgumentParser(description=_('Disable the profile for the given programs')) +parser.add_argument('-d', '--dir', type=str, help=_('path to profiles')) +parser.add_argument('-r', '--revert', action='store_true', help=_('enable the profile for the given programs')) +parser.add_argument('program', type=str, nargs='+', help=_('name of program')) +args = parser.parse_args() -use Locale::gettext; -use POSIX; -use File::Basename; +disable = apparmor.tools.aa_tools('disable', args) -# initialize the local poo -setlocale(LC_MESSAGES, ""); -textdomain("apparmor-utils"); - -$UI_Mode = "text"; - -# options variables -my $help = ''; - -GetOptions( - 'dir|d=s' => \$profiledir, - 'help|h' => \$help, -); - -# tell 'em how to use it... -&usage && exit if $help; - -# let's convert it to full path... -$profiledir = get_full_path($profiledir); - -unless (-d $profiledir) { - UI_Important("Can't find AppArmor profiles in $profiledir."); - exit 1; -} - -my $disabledir = "$profiledir/disable"; -unless (-d $disabledir) { - UI_Important("Can't find AppArmor disable directory '$disabledir'."); - exit 1; -} - -# what are we profiling? -my @profiling = @ARGV; - -unless (@profiling) { - @profiling = (UI_GetString(gettext("Please enter the program whose profile should be disabled: "), "")); -} - -for my $profiling (@profiling) { - - next unless $profiling; - - my $fqdbin; - if (-e $profiling) { - $fqdbin = get_full_path($profiling); - chomp($fqdbin); - } else { - if ($profiling !~ /\//) { - opendir(DIR,$profiledir); - my @tmp_fqdbin = grep ( /$profiling/, readdir(DIR)); - closedir(DIR); - if (scalar @tmp_fqdbin eq 1) { - $fqdbin = "$profiledir/$tmp_fqdbin[0]"; - } else { - my $which = which($profiling); - if ($which) { - $fqdbin = get_full_path($which); - } - } - } - } - - if (-e $fqdbin) { - - my $filename; - if ($fqdbin =~ /^$profiledir\//) { - $filename = $fqdbin; - } else { - $filename = getprofilefilename($fqdbin); - } - - # argh, skip directories - next unless -f $filename; - - # skip package manager backup files - next if isSkippableFile($filename); - - my ($bname, $dname, $suffix) = File::Basename::fileparse($filename); - if ($bname eq "") { - UI_Info(sprintf(gettext('Could not find basename for %s.'), $filename)); - exit 1; - } - - printf(gettext('Disabling %s.'), $fqdbin); - print "\n"; - - my $link = "$disabledir/$bname"; - if (! -e $link) { - if (symlink($filename, $link) != 1) { - UI_Info(sprintf(gettext('Could not create %s symlink.'), $link)); - exit 1; - } - } - - my $cmd_info = qx(cat $filename | $parser -I$profiledir -R 2>&1 1>/dev/null); - if ($? != 0) { - UI_Info($cmd_info); - exit $?; - } - -# if check_for_subdomain(); - } else { - if ($profiling =~ /^[^\/]+$/) { - UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); - exit 1; - } else { - UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); - exit 1; - } - } -} - -exit 0; - -sub usage { - UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ program to have profile disabled ]"), $0)); - exit 0; -} +disable.act() diff --git a/utils/aa-disable.pod b/utils/aa-disable.pod index fc1a4ed89..3f665df50 100644 --- a/utils/aa-disable.pod +++ b/utils/aa-disable.pod @@ -26,15 +26,28 @@ aa-disable - disable an AppArmor security profile =head1 SYNOPSIS -BexecutableE> [IexecutableE> ...]> +BexecutableE> [IexecutableE> ...] [I<-d /path/to/profiles>] [I<-r>]> + +=head1 OPTIONS + +B<-d --dir /path/to/profiles> + + Specifies where to look for the AppArmor security profile set. + Defaults to /etc/apparmor.d. + +B<-r --revert> + + Enables the profile and loads it. =head1 DESCRIPTION -B is used to disable the enforcement mode for one or more -profiles. This command will unload the profile from the kernel and -prevent the profile from being loaded on AppArmor startup. The -I and I utilities may be used to to change this -behavior. +B is used to I one or more profiles. +This command will unload the profile from the kernel and prevent the +profile from being loaded on AppArmor startup. +The I and I utilities may be used to to change +this behavior. + +The I<--revert> option can be used to enable the profile. =head1 BUGS diff --git a/utils/aa-easyprof b/utils/aa-easyprof index a042c55ee..ac69ca706 100755 --- a/utils/aa-easyprof +++ b/utils/aa-easyprof @@ -1,7 +1,7 @@ #! /usr/bin/env python # ------------------------------------------------------------------ # -# Copyright (C) 2011-2012 Canonical Ltd. +# 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 @@ -22,6 +22,7 @@ if __name__ == "__main__": (opt, args) = apparmor.easyprof.parse_args() binary = None + manifest = None m = usage() if opt.show_policy_group and not opt.policy_groups: @@ -33,33 +34,65 @@ if __name__ == "__main__": if len(args) >= 1: binary = args[0] - try: - easyp = apparmor.easyprof.AppArmorEasyProfile(binary, opt) - except AppArmorException as e: - error(e.value) - except Exception: - raise + # parse_manifest() returns a list of tuples (binary, options). Create a + # list of these profile tuples to support multiple profiles in one manifest + profiles = [] + if opt.manifest: + try: + # should hide this in a common function + if sys.version_info[0] >= 3: + f = open(opt.manifest, "r", encoding="utf-8") + else: + f = open(opt.manifest, "r") + manifest = f.read() + except EnvironmentError as e: + error("Could not read '%s': %s (%d)\n" % (opt.manifest, + os.strerror(e.errno), + e.errno)) + profiles = apparmor.easyprof.parse_manifest(manifest, opt) + else: # fake up a tuple list when processing command line args + profiles.append( (binary, opt) ) - if opt.list_templates: - apparmor.easyprof.print_basefilenames(easyp.get_templates()) - sys.exit(0) - elif opt.template and opt.show_template: - files = [os.path.join(easyp.dirs['templates'], opt.template)] - apparmor.easyprof.print_files(files) - sys.exit(0) - elif opt.list_policy_groups: - apparmor.easyprof.print_basefilenames(easyp.get_policy_groups()) - sys.exit(0) - elif opt.policy_groups and opt.show_policy_group: - for g in opt.policy_groups.split(','): - files = [os.path.join(easyp.dirs['policygroups'], g)] + count = 0 + 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 + + if options.list_templates: + apparmor.easyprof.print_basefilenames(easyp.get_templates()) + sys.exit(0) + elif options.template and options.show_template: + files = [os.path.join(easyp.dirs['templates'], options.template)] apparmor.easyprof.print_files(files) - sys.exit(0) - elif binary == None: - error("Must specify full path to binary\n%s" % m) + sys.exit(0) + elif options.list_policy_groups: + apparmor.easyprof.print_basefilenames(easyp.get_policy_groups()) + sys.exit(0) + elif options.policy_groups and options.show_policy_group: + for g in options.policy_groups.split(','): + files = [os.path.join(easyp.dirs['policygroups'], g)] + apparmor.easyprof.print_files(files) + sys.exit(0) + elif binary == None and not options.profile_name and \ + not options.manifest: + error("Must specify binary and/or profile name\n%s" % m) - # if we made it here, generate a profile - params = apparmor.easyprof.gen_policy_params(binary, opt) - p = easyp.gen_policy(**params) - sys.stdout.write('%s\n' % p) + params = apparmor.easyprof.gen_policy_params(binary, options) + if options.manifest and options.verify_manifest and \ + not apparmor.easyprof.verify_manifest(params): + error("Manifest file requires review") + if options.output_format == "json": + 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) diff --git a/utils/aa-easyprof.pod b/utils/aa-easyprof.pod index e47b65237..486edead2 100644 --- a/utils/aa-easyprof.pod +++ b/utils/aa-easyprof.pod @@ -78,8 +78,15 @@ Like --read-path but also allow owner writes in additions to reads. =item -n NAME, --name=NAME Specify NAME of policy. If not specified, NAME is set to the name of the -binary. The NAME of the policy is often used as part of the path in the -various templates. +binary. The NAME of the policy is typically only used for profile meta +data and does not specify the AppArmor profile name. + +=item --profile-name=PROFILENAME + +Specify the AppArmor profile name. When set, uses 'profile PROFILENAME' in the +profile. When set and specifying a binary, uses 'profile PROFILENAME BINARY' +in the profile. If not set, the binary will be used as the profile name and +profile attachment. =item --template-var="@{VAR}=VALUE" @@ -110,6 +117,32 @@ Display policy groups specified with --policy. Use PATH instead of system policy-groups directory. +=item --policy-version=VERSION + +Must be used with --policy-vendor and is used to specify the version of policy +groups and templates. When specified, B looks for the subdirectory +VENDOR/VERSION within the policy-groups and templates directory. The specified +version must be a positive decimal number compatible with the JSON Number type. +Eg, when using: + +=over + + $ aa-easyprof --templates-dir=/usr/share/apparmor/easyprof/templates \ + --policy-groups-dir=/usr/share/apparmor/easyprof/policygroups \ + --policy-vendor="foo" \ + --policy-version=1.0 + +=back + +Then /usr/share/apparmor/easyprof/templates/foo/1.0 will be searched for +templates and /usr/share/apparmor/easyprof/policygroups/foo/1.0 for policy +groups. + +=item --policy-vendor=VENDOR + +Must be used with --policy-version and is used to specify the vendor for policy +groups and templates. See --policy-version for more information. + =item --author Specify author of the policy. @@ -122,6 +155,104 @@ Specify copyright of the policy. Specify comment for the policy. +=item -m MANIFEST, --manifest=MANIFEST + +B also supports using a JSON manifest file for specifying options +related to policy. Unlike command line arguments, the JSON file may specify +multiple profiles. The structure of the JSON is: + + { + "security": { + "profiles": { + "": { + ... attributes specific to this profile ... + }, + "": { + ... + } + } + } + } + +Each profile JSON object (ie, everything under a profile name) may specify any +fields related to policy. The "security" JSON container object is optional and +may be omitted. An example manifest file demonstrating all fields is: + + { + "security": { + "profiles": { + "com.example.foo": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "networking", + "user-application" + ], + "policy_vendor": "somevendor", + "policy_version": 1.0, + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + } + } + } + } + +A manifest file does not have to include all the fields. Eg, a manifest file +for an Ubuntu SDK application might be: + + { + "security": { + "profiles": { + "com.ubuntu.developer.myusername.MyCoolApp": { + "policy_groups": [ + "networking", + "online-accounts" + ], + "policy_vendor": "ubuntu", + "policy_version": 1.0, + "template": "ubuntu-sdk", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } + } + +=item --verify-manifest + +When used with --manifest, warn about potentially unsafe definitions in the +manifest file. + +=item --output-format=FORMAT + +Specify either B (default if unspecified) for AppArmor policy output or +B for JSON manifest format. + +=item --output-directory=DIR + +Specify output directory for profile. If unspecified, policy is sent to stdout. + =back =head1 EXAMPLE @@ -130,7 +261,41 @@ Example usage for a program named 'foo' which is installed in /opt/foo: =over -$ aa-easyprof --template=user-application --template-var="@{APPNAME}=foo" --policy-groups=opt-application,user-application /opt/foo/bin/FooApp + $ aa-easyprof --template=user-application --template-var="@{APPNAME}=foo" \ + --policy-groups=opt-application,user-application \ + /opt/foo/bin/FooApp + +=back + +When using a manifest file: + +=over + + $ aa-easyprof --manifest=manifest.json + +=back + +To output a manifest file based on aa-easyprof arguments: + +=over + + $ aa-easyprof --output-format=json \ + --author="Your Name" \ + --comment="Unstructured single-line comment" \ + --copyright="Unstructured single-line copyright statement" \ + --name="My Foo App" \ + --profile-name="com.example.foo" \ + --template="user-application" \ + --policy-groups="user-application,networking" \ + --abstractions="audio,gnome" \ + --read-path="/tmp/foo_r" \ + --read-path="/tmp/bar_r/" \ + --write-path="/tmp/foo_w" \ + --write-path=/tmp/bar_w/ \ + --template-var="@{APPNAME}=foo" \ + --template-var="@{VAR1}=bar" \ + --template-var="@{VAR2}=baz" \ + "/opt/foo/**" =back diff --git a/utils/aa-enforce b/utils/aa-enforce index 06415ba5b..a540d9b5b 100755 --- a/utils/aa-enforce +++ b/utils/aa-enforce @@ -1,7 +1,6 @@ -#!/usr/bin/perl +#! /usr/bin/env python # ---------------------------------------------------------------------- -# Copyright (c) 2005 Novell, Inc. All Rights Reserved. -# Copyright (c) 2011 Canonical, Ltd. +# 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 @@ -12,131 +11,23 @@ # 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. -# -# To contact Novell about this file by physical or electronic mail, -# you may find current contact information at www.novell.com. # ---------------------------------------------------------------------- +import argparse -use strict; -use FindBin; -use Getopt::Long; +import apparmor.tools -use Immunix::AppArmor; +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() -use Data::Dumper; +parser = argparse.ArgumentParser(description=_('Switch the given program to enforce mode')) +parser.add_argument('-d', '--dir', type=str, help=_('path to profiles')) +parser.add_argument('-r', '--remove', action='store_true', help=_('switch to complain mode')) +parser.add_argument('program', type=str, nargs='+', help=_('name of program')) +args = parser.parse_args() +# Flipping the remove flag since complain = !enforce +args.remove = not args.remove -use Locale::gettext; -use POSIX; - -# initialize the local poo -setlocale(LC_MESSAGES, ""); -textdomain("apparmor-utils"); - -$UI_Mode = "text"; - -# options variables -my $help = ''; - -GetOptions( - 'dir|d=s' => \$profiledir, - 'help|h' => \$help, -); - -# tell 'em how to use it... -&usage && exit if $help; - -# let's convert it to full path... -$profiledir = get_full_path($profiledir); - -unless (-d $profiledir) { - UI_Important("Can't find AppArmor profiles in $profiledir."); - exit 1; -} - -# what are we profiling? -my @profiling = @ARGV; - -unless (@profiling) { - @profiling = (UI_GetString(gettext("Please enter the program to switch to enforce mode: "), "")); -} - -for my $profiling (@profiling) { - - next unless $profiling; - - my $fqdbin; - if (-e $profiling) { - $fqdbin = get_full_path($profiling); - chomp($fqdbin); - } else { - if ($profiling !~ /\//) { - opendir(DIR,$profiledir); - my @tmp_fqdbin = grep ( /$profiling/, readdir(DIR)); - closedir(DIR); - if (scalar @tmp_fqdbin eq 1) { - $fqdbin = "$profiledir/$tmp_fqdbin[0]"; - } else { - my $which = which($profiling); - if ($which) { - $fqdbin = get_full_path($which); - } - } - } - } - - if (-e $fqdbin) { - my $filename; - if ($fqdbin =~ /^$profiledir\//) { - $filename = $fqdbin; - } else { - $filename = getprofilefilename($fqdbin); - } - - # argh, skip directories - next unless -f $filename; - - # skip rpm backup files - next if isSkippableFile($filename); - - printf(gettext('Setting %s to enforce mode.'), $fqdbin); - print "\n"; - setprofileflags($filename, ""); - - # remove symlink in $profiledir/force-complain as well - my $complainlink = $filename; - $complainlink =~ s/^$profiledir/$profiledir\/force-complain/; - -e $complainlink and unlink($complainlink); - - # remove symlink in $profiledir/disable as well - my $disablelink = $filename; - $disablelink =~ s/^$profiledir/$profiledir\/disable/; - -e $disablelink and unlink($disablelink); - - my $cmd_info = qx(cat $filename | $parser -I$profiledir -r 2>&1 1>/dev/null); - if ($? != 0) { - UI_Info($cmd_info); - exit $?; - } - - -# if check_for_subdomain(); - } else { - if ($profiling =~ /^[^\/]+$/) { - UI_Info(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' as a user with the correct PATH environment set up in order to find the fully-qualified path.'), $profiling, $profiling)); - exit 1; - } else { - UI_Info(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); - exit 1; - } - } -} - -exit 0; - -sub usage { - UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ program to switch to enforce mode ]"), $0)); - exit 0; -} +enforce = apparmor.tools.aa_tools('complain', args) +enforce.act() diff --git a/utils/aa-enforce.pod b/utils/aa-enforce.pod index 375183e3f..53aef579b 100644 --- a/utils/aa-enforce.pod +++ b/utils/aa-enforce.pod @@ -27,16 +27,30 @@ being disabled or I mode. =head1 SYNOPSIS -BexecutableE> [IexecutableE> ...]> +BexecutableE> [IexecutableE> ...] [I<-d /path/to/profiles>] [I<-r>]> + +=head1 OPTIONS + +B<-d --dir / path/to/profiles> + + Specifies where to look for the AppArmor security profile set. + Defaults to /etc/apparmor.d. + +B<-r --remove> + + Removes the enforce mode for the profile. =head1 DESCRIPTION -B is used to set the enforcement mode for one or more profiles -to I. This command is only relevant in conjunction with the -I utility which sets a profile to complain mode and the -I utility which unloads and disables a profile. The default -mode for a security policy is enforce and the I utility must -be run to change this behavior. +B is used to set one or more profiles to I mode. +This command is only relevant in conjunction with the I utility +which sets a profile to complain mode and the I utility which +unloads and disables a profile. +The default mode for a security policy is enforce and the I +utility must be run to change this behavior. + +The I<--remove> option can be used to remove the enforce mode for the profile, +setting it to complain mode. =head1 BUGS diff --git a/utils/aa-genprof b/utils/aa-genprof index 33046042b..385ce7b89 100755 --- a/utils/aa-genprof +++ b/utils/aa-genprof @@ -1,6 +1,6 @@ -#!/usr/bin/perl +#! /usr/bin/env python # ---------------------------------------------------------------------- -# Copyright (c) 2005 Novell, Inc. All Rights Reserved. +# 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 @@ -11,206 +11,154 @@ # 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. -# -# To contact Novell about this file by physical or electronic mail, -# you may find current contact information at www.novell.com. # ---------------------------------------------------------------------- +import argparse +import atexit +import os +import re +import subprocess +import sys -use strict; -use Getopt::Long; +import apparmor.aa as apparmor -use Immunix::AppArmor; +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() -use Data::Dumper; +def sysctl_read(path): + value = None + with open(path, 'r') as f_in: + value = int(f_in.readline()) + return value -use Locale::gettext; -use POSIX; +def sysctl_write(path, value): + if not value: + return + with open(path, 'w') as f_out: + f_out.write(str(value)) -sub sysctl_read($) { - my $path = shift; - my $value = undef; - if (open(SYSCTL, "<$path")) { - $value = int(); - } - close(SYSCTL); - return $value; -} +def last_audit_entry_time(): + out = subprocess.check_output(['tail', '-1', '/var/log/audit/audit.log'], shell=True) + logmark = None + if re.search('^*msg\=audit\((\d+\.\d+\:\d+).*\).*$', out): + logmark = re.search('^*msg\=audit\((\d+\.\d+\:\d+).*\).*$', out).groups()[0] + else: + logmark = '' + return logmark -sub sysctl_write($$) { - my $path = shift; - my $value = shift; - return if (!defined($value)); - if (open(SYSCTL, ">$path")) { - print SYSCTL $value; - close(SYSCTl); - } -} +def restore_ratelimit(): + sysctl_write(ratelimit_sysctl, ratelimit_saved) -# force $PATH to be sane -$ENV{PATH} = "/bin:/sbin:/usr/bin:/usr/sbin"; +parser = argparse.ArgumentParser(description=_('Generate profile for the given program')) +parser.add_argument('-d', '--dir', type=str, help=_('path to profiles')) +parser.add_argument('-f', '--file', type=str, help=_('path to logfile')) +parser.add_argument('program', type=str, help=_('name of program to profile')) +args = parser.parse_args() -# initialize the local poo -setlocale(LC_MESSAGES, ""); -textdomain("apparmor-utils"); - -# options variables -my $help = ''; - -GetOptions( - 'file|f=s' => \$filename, - 'dir|d=s' => \$profiledir, - 'help|h' => \$help, -); - -# tell 'em how to use it... -&usage && exit if $help; - -my $sd_mountpoint = check_for_subdomain(); -unless ($sd_mountpoint) { - fatal_error(gettext("AppArmor does not appear to be started. Please enable AppArmor and try again.")); -} - -# let's convert it to full path... -$profiledir = get_full_path($profiledir); - -unless (-d $profiledir) { - fatal_error "Can't find AppArmor profiles in $profiledir."; -} - -# what are we profiling? -my $profiling = shift; - -unless ($profiling) { - $profiling = UI_GetString(gettext("Please enter the program to profile: "), "") - || exit 0; -} - -my $fqdbin; -if (-e $profiling) { - $fqdbin = get_full_path($profiling); - chomp($fqdbin); -} else { - if ($profiling !~ /\//) { - my $which = which($profiling); - if ($which) { - $fqdbin = get_full_path($which); - } - } -} - -unless ($fqdbin && -e $fqdbin) { - if ($profiling =~ /^[^\/]+$/) { - fatal_error(sprintf(gettext('Can\'t find %s in the system path list. If the name of the application is correct, please run \'which %s\' in the other window in order to find the fully-qualified path.'), $profiling, $profiling)); - } else { - fatal_error(sprintf(gettext('%s does not exist, please double-check the path.'), $profiling)); - } -} +profiling = args.program +profiledir = args.dir +filename = args.file -# make sure that the app they're requesting to profile is not marked as -# not allowed to have it's own profile -check_qualifiers($fqdbin); +if filename: + if not os.path.isfile(filename): + raise apparmor.AppArmorException(_('The logfile %s does not exist. Please check the path') % filename) + else: + apparmor.filename = filename -# load all the include files -loadincludes(); +aa_mountpoint = apparmor.check_for_apparmor() +if not aa_mountpoint: + raise apparmor.AppArmorException(_('It seems AppArmor was not started. Please enable AppArmor and try again.')) -my $profilefilename = getprofilefilename($fqdbin); -if (-e $profilefilename) { - $helpers{$fqdbin} = getprofileflags($profilefilename) || "enforce"; -} else { - autodep($fqdbin); - $helpers{$fqdbin} = "enforce"; -} +if profiledir: + apparmor.profile_dir = apparmor.get_full_path(profiledir) + if not os.path.isdir(apparmor.profile_dir): + raise apparmor.AppArmorException(_("%s is not a directory.") %profiledir) -if ($helpers{$fqdbin} eq "enforce") { - complain($fqdbin); - reload($fqdbin); -} +program = None +#if os.path.exists(apparmor.which(profiling.strip())): +if os.path.exists(profiling): + program = apparmor.get_full_path(profiling) +else: + if '/' not in profiling: + which = apparmor.which(profiling) + if which: + program = apparmor.get_full_path(which) + +if not program or not os.path.exists(program): + if '/' not in profiling: + raise apparmor.AppArmorException(_("Can't find %s in the system path list. If the name of the application\nis correct, please run 'which %s' as a user with correct PATH\nenvironment set up in order to find the fully-qualified path and\nuse the full path as parameter.") %(profiling, profiling)) + else: + raise apparmor.AppArmorException(_('%s does not exists, please double-check the path.') %profiling) + +# Check if the program has been marked as not allowed to have a profile +apparmor.check_qualifiers(program) + +apparmor.loadincludes() + +profile_filename = apparmor.get_profile_filename(program) +if os.path.exists(profile_filename): + apparmor.helpers[program] = apparmor.get_profile_flags(profile_filename, program) +else: + apparmor.autodep(program) + apparmor.helpers[program] = 'enforce' + +if apparmor.helpers[program] == 'enforce': + apparmor.complain(program) + apparmor.reload(program) # When reading from syslog, it is possible to hit the default kernel # printk ratelimit. This will result in audit entries getting skipped, # making profile generation inaccurate. When using genprof, disable # the printk ratelimit, and restore it on exit. -my $ratelimit_sysctl = "/proc/sys/kernel/printk_ratelimit"; -my $ratelimit_saved = sysctl_read($ratelimit_sysctl); -END { sysctl_write($ratelimit_sysctl, $ratelimit_saved); } -sysctl_write($ratelimit_sysctl, 0); +ratelimit_sysctl = '/proc/sys/kernel/printk_ratelimit' +ratelimit_saved = sysctl_read(ratelimit_sysctl) +sysctl_write(ratelimit_sysctl, 0) -UI_Info(gettext("\nBefore you begin, you may wish to check if a\nprofile already exists for the application you\nwish to confine. See the following wiki page for\nmore information:\nhttp://wiki.apparmor.net/index.php/Profiles")); +atexit.register(restore_ratelimit) -UI_Important(gettext("Please start the application to be profiled in \nanother window and exercise its functionality now.\n\nOnce completed, select the \"Scan\" button below in \norder to scan the system logs for AppArmor events. \n\nFor each AppArmor event, you will be given the \nopportunity to choose whether the access should be \nallowed or denied.")); +apparmor.UI_Info(_('\nBefore you begin, you may wish to check if a\nprofile already exists for the application you\nwish to confine. See the following wiki page for\nmore information:')+'\nhttp://wiki.apparmor.net/index.php/Profiles') -my $syslog = 1; -my $logmark = ""; -my $done_profiling = 0; +apparmor.UI_Important(_('Please start the application to be profiled in\nanother window and exercise its functionality now.\n\nOnce completed, select the "Scan" option below in \norder to scan the system logs for AppArmor events. \n\nFor each AppArmor event, you will be given the \nopportunity to choose whether the access should be \nallowed or denied.')) -$syslog = 0 if (-e "/var/log/audit/audit.log"); +syslog = True +logmark = '' +done_profiling = False -while (not $done_profiling) { - if ($syslog) { - $logmark = `date | md5sum`; - chomp $logmark; - $logmark = $1 if $logmark =~ /^([0-9a-f]+)/; - system("$logger -p kern.warn 'GenProf: $logmark'"); - } else { - $logmark = last_audit_entry_time(); - } - eval { +if os.path.exists('/var/log/audit/audit.log'): + syslog = False - my $q = {}; - $q->{headers} = [ gettext("Profiling"), $fqdbin ]; - $q->{functions} = [ "CMD_SCAN", "CMD_FINISHED" ]; - $q->{default} = "CMD_SCAN"; +passno = 0 +while not done_profiling: + if syslog: + 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) - my ($ans, $arg) = UI_PromptUser($q); + else: + logmark = last_audit_entry_time() - if ($ans eq "CMD_SCAN") { + q=apparmor.hasher() + q['headers'] = [_('Profiling'), program] + q['functions'] = ['CMD_SCAN', 'CMD_FINISHED'] + q['default'] = 'CMD_SCAN' + ans, arg = apparmor.UI_PromptUser(q, 'noexit') - my $lp_ret = do_logprof_pass($logmark); + if ans == 'CMD_SCAN': + lp_ret = apparmor.do_logprof_pass(logmark, passno) + passno += 1 + if lp_ret == 'FINISHED': + done_profiling = True + else: + done_profiling = True - $done_profiling = 1 if $lp_ret eq "FINISHED"; +for p in sorted(apparmor.helpers.keys()): + if apparmor.helpers[p] == 'enforce': + apparmor.enforce(p) + apparmor.reload(p) - } else { - - $done_profiling = 1; - - } - }; - if ($@) { - if ($@ =~ /FINISHING/) { - $done_profiling = 1; - } else { - die $@; - } - } -} - -for my $p (sort keys %helpers) { - if ($helpers{$p} eq "enforce") { - enforce($p); - reload($p); - } -} - -UI_Info(gettext("Reloaded AppArmor profiles in enforce mode.")); -UI_Info(gettext("\nPlease consider contributing your new profile! See\nthe following wiki page for more information:\nhttp://wiki.apparmor.net/index.php/Profiles\n")); -UI_Info(sprintf(gettext('Finished generating profile for %s.'), $fqdbin)); -exit 0; - -sub usage { - UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ -f /path/to/logfile ] [ program to profile ]"), $0)); - exit 0; -} - -sub last_audit_entry_time { - local $_ = `tail -1 /var/log/audit/audit.log`; - my $logmark; - if (/^*msg\=audit\((\d+\.\d+\:\d+).*\).*$/) { - $logmark = $1; - } else { - $logmark = ""; - } - return $logmark; -} +apparmor.UI_Info(_('\nReloaded AppArmor profiles in enforce mode.')) +apparmor.UI_Info(_('\nPlease consider contributing your new profile!\nSee the following wiki page for more information:')+'\nhttp://wiki.apparmor.net/index.php/Profiles\n') +apparmor.UI_Info(_('Finished generating profile for %s.')%program) +sys.exit(0) diff --git a/utils/aa-genprof.pod b/utils/aa-genprof.pod index c59df591d..c259408a1 100644 --- a/utils/aa-genprof.pod +++ b/utils/aa-genprof.pod @@ -26,15 +26,23 @@ aa-genprof - profile generation utility for AppArmor =head1 SYNOPSIS -BexecutableE> [I<-d /path/to/profiles>]> +BexecutableE> [I<-d /path/to/profiles>] [I<-f /path/to/logfile>]> =head1 OPTIONS -B<-d --dir /path/to/profiles> +B<-d --dir /path/to/profiles> Specifies where to look for the AppArmor security profile set. Defaults to /etc/apparmor.d. +B<-f --file /path/to/logfile> + + Specifies the location of logfile. + Default locations are read from F. + Typical defaults are: + /var/log/audit/audit.log + /var/log/syslog + /var/log/messages =head1 DESCRIPTION @@ -64,7 +72,7 @@ using aa-logprof(1). After the user finishes selecting profile entries based on violations that were detected during the program execution, aa-genprof will reload the updated profiles in complain mode and again prompt the user for (S)can and -(D)one. This cycle can then be repeated as necessary until all application +(F)inish. This cycle can then be repeated as necessary until all application functionality has been exercised without generating access violations. When the user eventually hits (F)inish, aa-genprof will set the main profile, diff --git a/utils/aa-logprof b/utils/aa-logprof index b4c34b993..1d0546c4c 100755 --- a/utils/aa-logprof +++ b/utils/aa-logprof @@ -1,6 +1,6 @@ -#!/usr/bin/perl +#! /usr/bin/env python # ---------------------------------------------------------------------- -# Copyright (c) 2005 Novell, Inc. All Rights Reserved. +# 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 @@ -11,62 +11,43 @@ # 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. -# -# To contact Novell about this file by physical or electronic mail, -# you may find current contact information at www.novell.com. # ---------------------------------------------------------------------- +import argparse +import os -use strict; -use Data::Dumper; -use Getopt::Long; -use Locale::gettext; -use POSIX; +import apparmor.aa as apparmor -use Immunix::AppArmor; +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() -# force $PATH to be sane -$ENV{PATH} = "/bin:/sbin:/usr/bin:/usr/sbin"; +parser = argparse.ArgumentParser(description=_('Process log entries to generate profiles')) +parser.add_argument('-d', '--dir', type=str, help=_('path to profiles')) +parser.add_argument('-f', '--file', type=str, help=_('path to logfile')) +parser.add_argument('-m', '--mark', type=str, help=_('mark in the log to start processing after')) +args = parser.parse_args() -# initialize the local poo -setlocale(LC_MESSAGES, ""); -textdomain("apparmor-utils"); +profiledir = args.dir +filename = args.file +logmark = args.mark or '' -setup_yast(); -# options variables -my $help = ''; -my $logmark; +if filename: + if not os.path.isfile(filename): + raise apparmor.AppArmorException(_('The logfile %s does not exist. Please check the path') % filename) + else: + apparmor.filename = filename -GetOptions( - 'file|f=s' => \$filename, - 'dir|d=s' => \$profiledir, - 'logmark|m=s' => \$logmark, - 'help|h' => \$help, -); +aa_mountpoint = apparmor.check_for_apparmor() +if not aa_mountpoint: + raise apparmor.AppArmorException(_('It seems AppArmor was not started. Please enable AppArmor and try again.')) -# tell 'em how to use it... -&usage && exit if $help; +if profiledir: + apparmor.profile_dir = apparmor.get_full_path(profiledir) + if not os.path.isdir(apparmor.profile_dir): + raise apparmor.AppArmorException("%s is not a directory."%profiledir) -# let's convert it to full path... -$profiledir = get_full_path($profiledir); +apparmor.loadincludes() -unless (-d $profiledir) { - fatal_error "Can't find AppArmor profiles in $profiledir."; -} - -# load all the include files -loadincludes(); - -do_logprof_pass($logmark); - -shutdown_yast(); - -exit 0; - -sub usage { - UI_Info(sprintf(gettext("usage: \%s [ -d /path/to/profiles ] [ -f /path/to/logfile ] [ -m \"mark in log to start processing after\""), $0)); - exit 0; -} +apparmor.do_logprof_pass(logmark) diff --git a/utils/aa-logprof.pod b/utils/aa-logprof.pod index 2eb115a89..6219c12fc 100644 --- a/utils/aa-logprof.pod +++ b/utils/aa-logprof.pod @@ -22,7 +22,7 @@ =head1 NAME -aa-logprof - utility program for managing AppArmor security profiles +aa-logprof - utility for updating AppArmor security profiles =head1 SYNOPSIS @@ -32,12 +32,17 @@ B] [I<-f /path/to/logfile>] [I<-m Emark B<-d --dir /path/to/profiles> - The path to where the AppArmor security profiles are stored + Specifies where to look for the AppArmor security profile set. + Defaults to /etc/apparmor.d. B<-f --file /path/to/logfile> - The path to the location of the logfile that contains AppArmor - security events. + Specifies the location of logfile that contains AppArmor security events. + Default locations are read from F. + Typical defaults are: + /var/log/audit/audit.log + /var/log/syslog + /var/log/messages B< -m --logmark "mark"> @@ -47,9 +52,8 @@ B< -m --logmark "mark"> =head1 DESCRIPTION -B is an interactive tool used to review AppArmor's -complain mode output and generate new entries for AppArmor security -profiles. +B is an interactive tool used to review AppArmor generated +messages and update AppArmor security profiles. Running aa-logprof will scan the log file and if there are new AppArmor events that are not covered by the existing profile set, the user will @@ -71,11 +75,17 @@ The user is then presented with info about the access including profile, path, old mode if there was a previous entry in the profile for this path, new mode, the suggestion list, and given these options: - (A)llow, (D)eny, (N)ew, (G)lob last piece, (Q)uit + (A)llow, (D)eny, (I)gnore, (N)ew, (G)lob last piece, (Q)uit If the AppArmor profile was in complain mode when the event was generated, the default for this option is (A)llow, otherwise, it's (D)eny. +The (D)eny option adds a "deny" rule to the AppArmor profile, which +silences logging. + +The (I)gnore option allows user to ignore the event, without making any +changes to the AppArmor profile. + The suggestion list is presented as a numbered list with includes at the top, the literal path in the middle, and the suggested globs at the bottom. If any globs are being suggested, the shortest glob @@ -109,9 +119,9 @@ Adding r access to /usr/share/themes/** would delete an entry for r access to /usr/share/themes/foo/*.gif if it exists in the profile. If (Q)uit is selected at this point, aa-logprof will ignore all new pending -capability and path accesses. +accesses. -After all of the path accesses have been handled, logrof will write all +After all of the accesses have been handled, logrof will write all updated profiles to the disk and reload them if AppArmor is running. =head2 New Process (Execution) Events diff --git a/utils/aa-mergeprof b/utils/aa-mergeprof new file mode 100755 index 000000000..ac149c2f3 --- /dev/null +++ b/utils/aa-mergeprof @@ -0,0 +1,678 @@ +#! /usr/bin/env python +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import argparse +import re + +import apparmor.aa +import apparmor.aamode +import apparmor.severity +import apparmor.cleanprofile as cleanprofile + +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() + +parser = argparse.ArgumentParser(description=_('Perform a 3way merge on the given profiles')) +parser.add_argument('mine', type=str, help=_('your profile')) +parser.add_argument('base', type=str, help=_('base profile')) +parser.add_argument('other', type=str, help=_('other profile')) +parser.add_argument('-d', '--dir', type=str, help=_('path to profiles')) +parser.add_argument('-a', '--auto', action='store_true', help=_('Automatically merge profiles, exits incase of *x conflicts')) +args = parser.parse_args() + +profiles = [args.mine, args.base, args.other] + + +def main(): + mergeprofiles = Merge(profiles) + #Get rid of common/superfluous stuff + mergeprofiles.clear_common() + + if not args.auto: + mergeprofiles.ask_the_questions('other') + + mergeprofiles.clear_common() + + mergeprofiles.ask_the_questions('base') + + q = apparmor.aa.hasher() + q['title'] = 'Changed Local Profiles' + q['headers'] = [] + q['explanation'] = _('The following local profiles were changed. Would you like to save them?') + q['functions'] = ['CMD_SAVE_CHANGES', 'CMD_VIEW_CHANGES', 'CMD_ABORT'] + q['default'] = 'CMD_VIEW_CHANGES' + q['options'] = [] + q['selected'] = 0 + ans = '' + arg = None + programs = list(mergeprofiles.user.aa.keys()) + program = programs[0] + while ans != 'CMD_SAVE_CHANGES': + ans, arg = apparmor.aa.UI_PromptUser(q) + if ans == 'CMD_SAVE_CHANGES': + apparmor.aa.write_profile_ui_feedback(program) + apparmor.aa.reload_base(program) + elif ans == 'CMD_VIEW_CHANGES': + for program in programs: + apparmor.aa.original_aa[program] = apparmor.aa.deepcopy(apparmor.aa.aa[program]) + #oldprofile = apparmor.serialize_profile(apparmor.original_aa[program], program, '') + newprofile = apparmor.aa.serialize_profile(mergeprofiles.user.aa[program], program, '') + apparmor.aa.display_changes_with_comments(mergeprofiles.user.filename, newprofile) + + +class Merge(object): + def __init__(self, profiles): + user, base, other = profiles + + #Read and parse base profile and save profile data, include data from it and reset them + apparmor.aa.read_profile(base, True) + self.base = cleanprofile.Prof(base) + + self.reset() + + #Read and parse other profile and save profile data, include data from it and reset them + apparmor.aa.read_profile(other, True) + self.other = cleanprofile.Prof(other) + + self.reset() + + #Read and parse user profile + apparmor.aa.read_profile(profiles[0], True) + self.user = cleanprofile.Prof(user) + + def reset(self): + apparmor.aa.aa = apparmor.aa.hasher() + apparmor.aa.filelist = apparmor.aa.hasher() + apparmor.aa.include = dict() + apparmor.aa.existing_profiles = apparmor.aa.hasher() + apparmor.aa.original_aa = apparmor.aa.hasher() + + def clear_common(self): + deleted = 0 + #Remove off the parts in other profile which are common/superfluous from user profile + user_other = cleanprofile.CleanProf(False, self.user, self.other) + deleted += user_other.compare_profiles() + + #Remove off the parts in base profile which are common/superfluous from user profile + user_base = cleanprofile.CleanProf(False, self.user, self.base) + deleted += user_base.compare_profiles() + + #Remove off the parts in other profile which are common/superfluous from base profile + # base_other = cleanprofile.CleanProf(False, self.base, self.other) # XXX base_other not used? + deleted += user_base.compare_profiles() + + def conflict_mode(self, profile, hat, allow, path, mode, new_mode, old_mode): + m = new_mode + o = old_mode + new_mode = apparmor.aa.flatten_mode(new_mode) + old_mode = apparmor.aa.flatten_mode(old_mode) + conflict_modes = set('uUpPcCiIxX') + conflict_x= (old_mode | new_mode) & conflict_modes + if conflict_x: + #We may have conflicting x modes + if conflict_x & set('x'): + conflict_x.remove('x') + if conflict_x & set('X'): + conflict_x.remove('X') + if len(conflict_x) > 1: + q = apparmor.aa.hasher() + q['headers'] = [_('Path'), path] + q['headers'] += [_('Select the appropriate mode'), ''] + options = [] + options.append('%s: %s' %(mode, apparmor.aa.mode_to_str_user(new_mode)))# - (old_mode & conflict_x)))) + options.append('%s: %s' %(mode, apparmor.aa.mode_to_str_user(old_mode)))#(old_mode | new_mode) - (new_mode & conflict_x)))) + q['options'] = options + q['functions'] = ['CMD_ALLOW', 'CMD_ABORT'] + done = False + while not done: + ans, selected = apparmor.aa.UI_PromptUser(q) + if ans == 'CMD_ALLOW': + if selected == 0: + self.user.aa[profile][hat][allow]['path'][path][mode] = m#apparmor.aa.owner_flatten_mode(new_mode)#(old_mode | new_mode) - (old_mode & conflict_x) + return m + elif selected == 1: + return o + pass#self.user.aa[profile][hat][allow][path][mode] = (old_mode | new_mode) - (new_mode & conflict_x) + else: + raise apparmor.aa.AppArmorException(_('Unknown selection')) + done = True + + def ask_the_questions(self, other): + if other == 'other': + other = self.other + else: + other = self.base + #print(other.aa) + + #Add the file-wide includes from the other profile to the user profile + done = False + options = list(map(lambda inc: '#include <%s>' %inc, sorted(other.filelist[other.filename]['include'].keys()))) + q = apparmor.aa.hasher() + q['options'] = options + default_option = 1 + q['selected'] = default_option - 1 + q['headers'] = [_('File includes'), _('Select the ones you wish to add')] + q['functions'] = ['CMD_ALLOW', 'CMD_IGNORE_ENTRY', 'CMD_ABORT', 'CMD_FINISHED'] + q['default'] = 'CMD_ALLOW' + while not done and options: + ans, selected = apparmor.aa.UI_PromptUser(q) + if ans == 'CMD_IGNORE_ENTRY': + done = True + elif ans == 'CMD_ALLOW': + selection = options[selected] + inc = apparmor.aa.re_match_include(selection) + self.user.filelist[self.user.filename]['include'][inc] = True + options.pop(selected) + apparmor.aa.UI_Info(_('Adding %s to the file.') % selection) + + sev_db = apparmor.aa.sev_db + if not sev_db: + sev_db = apparmor.severity.Severity(apparmor.aa.CONFDIR + '/severity.db', _('unknown')) + for profile in sorted(other.aa.keys()): + for hat in sorted(other.aa[profile].keys()): + #Add the includes from the other profile to the user profile + done = False + options = list(map(lambda inc: '#include <%s>' %inc, sorted(other.aa[profile][hat]['include'].keys()))) + q = apparmor.aa.hasher() + q['options'] = options + default_option = 1 + q['selected'] = default_option - 1 + q['headers'] = [_('File includes'), _('Select the ones you wish to add')] + q['functions'] = ['CMD_ALLOW', 'CMD_IGNORE_ENTRY', 'CMD_ABORT', 'CMD_FINISHED'] + q['default'] = 'CMD_ALLOW' + while not done and options: + ans, selected = apparmor.aa.UI_PromptUser(q) + if ans == 'CMD_IGNORE_ENTRY': + done = True + elif ans == 'CMD_ALLOW': + selection = options[selected] + inc = apparmor.aa.re_match_include(selection) + deleted = apparmor.aa.delete_duplicates(self.user.aa[profile][hat], inc) + self.user.aa[profile][hat]['include'][inc] = True + options.pop(selected) + apparmor.aa.UI_Info(_('Adding %s to the file.') % selection) + if deleted: + apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted) + + #Add the capabilities + for allow in ['allow', 'deny']: + if other.aa[profile][hat].get(allow, False): + continue + for capability in sorted(other.aa[profile][hat][allow]['capability'].keys()): + severity = sev_db.rank('CAP_%s' % capability) + default_option = 1 + options = [] + newincludes = apparmor.aa.match_cap_includes(self.user.aa[profile][hat], capability) + q = apparmor.aa.hasher() + if newincludes: + options += list(map(lambda inc: '#include <%s>' %inc, sorted(set(newincludes)))) + + if options: + options.append('capability %s' % capability) + q['options'] = [options] + q['selected'] = default_option - 1 + + q['headers'] = [_('Profile'), apparmor.aa.combine_name(profile, hat)] + q['headers'] += [_('Capability'), capability] + q['headers'] += [_('Severity'), severity] + + audit_toggle = 0 + + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_ABORT', 'CMD_FINISHED'] + + q['default'] = 'CMD_ALLOW' + + done = False + while not done: + ans, selected = apparmor.aa.UI_PromptUser(q) + # Ignore the log entry + if ans == 'CMD_IGNORE_ENTRY': + done = True + break + + if ans == 'CMD_ALLOW': + selection = '' + if options: + selection = options[selected] + match = apparmor.aa.re_match_include(selection) + if match: + deleted = False + inc = match + deleted = apparmor.aa.delete_duplicates(self.user.aa[profile][hat], inc) + self.user.aa[profile][hat]['include'][inc] = True + + apparmor.aa.UI_Info(_('Adding %s to profile.') % selection) + if deleted: + apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted) + + self.user.aa[profile][hat]['allow']['capability'][capability]['set'] = True + self.user.aa[profile][hat]['allow']['capability'][capability]['audit'] = other.aa[profile][hat]['allow']['capability'][capability]['audit'] + + apparmor.aa.changed[profile] = True + + apparmor.aa.UI_Info(_('Adding capability %s to profile.'), capability) + done = True + + elif ans == 'CMD_DENY': + self.user.aa[profile][hat]['deny']['capability'][capability]['set'] = True + apparmor.aa.changed[profile] = True + + apparmor.aa.UI_Info(_('Denying capability %s to profile.') % capability) + done = True + else: + done = False + + # Process all the path entries. + for allow in ['allow', 'deny']: + for path in sorted(other.aa[profile][hat][allow]['path'].keys()): + #print(path, other.aa[profile][hat][allow]['path'][path]) + mode = other.aa[profile][hat][allow]['path'][path]['mode'] + + if self.user.aa[profile][hat][allow]['path'].get(path, False): + mode = self.conflict_mode(profile, hat, allow, path, 'mode', other.aa[profile][hat][allow]['path'][path]['mode'], self.user.aa[profile][hat][allow]['path'][path]['mode']) + self.conflict_mode(profile, hat, allow, path, 'audit', other.aa[profile][hat][allow]['path'][path]['audit'], self.user.aa[profile][hat][allow]['path'][path]['audit']) + apparmor.aa.changed[profile] = True + continue + # Lookup modes from profile + allow_mode = set() + allow_audit = set() + deny_mode = set() + deny_audit = set() + + fmode, famode, fm = apparmor.aa.rematchfrag(self.user.aa[profile][hat], 'allow', path) + if fmode: + allow_mode |= fmode + if famode: + allow_audit |= famode + + cm, cam, m = apparmor.aa.rematchfrag(self.user.aa[profile][hat], 'deny', path) + if cm: + deny_mode |= cm + if cam: + deny_audit |= cam + + imode, iamode, im = apparmor.aa.match_prof_incs_to_path(self.user.aa[profile][hat], 'allow', path) + if imode: + allow_mode |= imode + if iamode: + allow_audit |= iamode + + cm, cam, m = apparmor.aa.match_prof_incs_to_path(self.user.aa[profile][hat], 'deny', path) + if cm: + deny_mode |= cm + if cam: + deny_audit |= cam + + if deny_mode & apparmor.aa.AA_MAY_EXEC: + deny_mode |= apparmor.aamode.ALL_AA_EXEC_TYPE + + # Mask off the denied modes + mode = mode - deny_mode + + # If we get an exec request from some kindof event that generates 'PERMITTING X' + # check if its already in allow_mode + # if not add ix permission + if mode & apparmor.aa.AA_MAY_EXEC: + # Remove all type access permission + mode = mode - apparmor.aamode.ALL_AA_EXEC_TYPE + if not allow_mode & apparmor.aa.AA_MAY_EXEC: + mode |= apparmor.aa.str_to_mode('ix') + + # m is not implied by ix + + ### If we get an mmap request, check if we already have it in allow_mode + ##if mode & AA_EXEC_MMAP: + ## # ix implies m, so we don't need to add m if ix is present + ## if contains(allow_mode, 'ix'): + ## mode = mode - AA_EXEC_MMAP + + if not mode: + continue + + matches = [] + + if fmode: + matches += fm + + if imode: + matches += im + + if not apparmor.aa.mode_contains(allow_mode, mode): + default_option = 1 + options = [] + newincludes = [] + include_valid = False + + for incname in apparmor.aa.include.keys(): + include_valid = False + # If already present skip + if self.user.aa[profile][hat][incname]: + continue + if incname.startswith(apparmor.aa.profile_dir): + incname = incname.replace(apparmor.aa.profile_dir+'/', '', 1) + + include_valid = apparmor.aa.valid_include('', incname) + + if not include_valid: + continue + + cm, am, m = apparmor.aa.match_include_to_path(incname, 'allow', path) + + if cm and apparmor.aa.mode_contains(cm, mode): + dm = apparmor.aa.match_include_to_path(incname, 'deny', path)[0] + # If the mode is denied + if not mode & dm: + if not list(filter(lambda s: '/**' == s, m)): + newincludes.append(incname) + # Add new includes to the options + if newincludes: + options += list(map(lambda s: '#include <%s>' % s, sorted(set(newincludes)))) + # We should have literal the path in options list too + options.append(path) + # Add any the globs matching path from logprof + globs = apparmor.aa.glob_common(path) + if globs: + matches += globs + # Add any user entered matching globs + for user_glob in apparmor.aa.user_globs: + if apparmor.aa.matchliteral(user_glob, path): + matches.append(user_glob) + + matches = list(set(matches)) + if path in matches: + matches.remove(path) + + options += apparmor.aa.order_globs(matches, path) + default_option = len(options) + + sev_db.unload_variables() + sev_db.load_variables(apparmor.aa.get_profile_filename(profile)) + severity = sev_db.rank(path, apparmor.aa.mode_to_str(mode)) + sev_db.unload_variables() + + audit_toggle = 0 + owner_toggle = 0 + if apparmor.aa.cfg['settings']['default_owner_prompt']: + owner_toggle = apparmor.aa.cfg['settings']['default_owner_prompt'] + done = False + while not done: + q = apparmor.aa.hasher() + q['headers'] = [_('Profile'), apparmor.aa.combine_name(profile, hat), + _('Path'), path] + + if allow_mode: + mode |= allow_mode + tail = '' + s = '' + prompt_mode = None + if owner_toggle == 0: + prompt_mode = apparmor.aa.flatten_mode(mode) + tail = ' ' + _('(owner permissions off)') + elif owner_toggle == 1: + prompt_mode = mode + elif owner_toggle == 2: + prompt_mode = allow_mode | apparmor.aa.owner_flatten_mode(mode - allow_mode) + tail = ' ' + _('(force new perms to owner)') + else: + prompt_mode = apparmor.aa.owner_flatten_mode(mode) + tail = ' ' + _('(force all rule perms to owner)') + + if audit_toggle == 1: + s = apparmor.aa.mode_to_str_user(allow_mode) + if allow_mode: + s += ', ' + s += 'audit ' + apparmor.aa.mode_to_str_user(prompt_mode - allow_mode) + tail + elif audit_toggle == 2: + s = 'audit ' + apparmor.aa.mode_to_str_user(prompt_mode) + tail + else: + s = apparmor.aa.mode_to_str_user(prompt_mode) + tail + + q['headers'] += [_('Old Mode'), apparmor.aa.mode_to_str_user(allow_mode), + _('New Mode'), s] + + else: + s = '' + tail = '' + prompt_mode = None + if audit_toggle: + s = 'audit' + if owner_toggle == 0: + prompt_mode = apparmor.aa.flatten_mode(mode) + tail = ' ' + _('(owner permissions off)') + elif owner_toggle == 1: + prompt_mode = mode + else: + prompt_mode = apparmor.aa.owner_flatten_mode(mode) + tail = ' ' + _('(force perms to owner)') + + s = apparmor.aa.mode_to_str_user(prompt_mode) + q['headers'] += [_('Mode'), s] + + q['headers'] += [_('Severity'), severity] + q['options'] = options + q['selected'] = default_option - 1 + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_GLOB', + 'CMD_GLOBEXT', 'CMD_NEW', 'CMD_ABORT', + 'CMD_FINISHED', 'CMD_OTHER'] + + q['default'] = 'CMD_ALLOW' + + + ans, selected = apparmor.aa.UI_PromptUser(q) + + if ans == 'CMD_IGNORE_ENTRY': + done = True + break + + if ans == 'CMD_OTHER': + audit_toggle, owner_toggle = apparmor.aa.UI_ask_mode_toggles(audit_toggle, owner_toggle, allow_mode) + elif ans == 'CMD_USER_TOGGLE': + owner_toggle += 1 + if not allow_mode and owner_toggle == 2: + owner_toggle += 1 + if owner_toggle > 3: + owner_toggle = 0 + elif ans == 'CMD_ALLOW': + path = options[selected] + done = True + match = apparmor.aa.re_match_include(path) + if match: + inc = match + deleted = 0 + deleted = apparmor.aa.delete_duplicates(self.user.aa[profile][hat], inc) + self.user.aa[profile][hat]['include'][inc] = True + apparmor.aa.changed[profile] = True + apparmor.aa.UI_Info(_('Adding %s to profile.') % path) + if deleted: + apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted) + + else: + if self.user.aa[profile][hat]['allow']['path'][path].get('mode', False): + mode |= self.user.aa[profile][hat]['allow']['path'][path]['mode'] + deleted = [] + for entry in self.user.aa[profile][hat]['allow']['path'].keys(): + if path == entry: + continue + + if apparmor.aa.matchregexp(path, entry): + if apparmor.aa.mode_contains(mode, self.user.aa[profile][hat]['allow']['path'][entry]['mode']): + deleted.append(entry) + for entry in deleted: + self.user.aa[profile][hat]['allow']['path'].pop(entry) + deleted = len(deleted) + + if owner_toggle == 0: + mode = apparmor.aa.flatten_mode(mode) + #elif owner_toggle == 1: + # mode = mode + elif owner_toggle == 2: + mode = allow_mode | apparmor.aa.owner_flatten_mode(mode - allow_mode) + elif owner_toggle == 3: + mode = apparmor.aa.owner_flatten_mode(mode) + + if not self.user.aa[profile][hat]['allow'].get(path, False): + self.user.aa[profile][hat]['allow']['path'][path]['mode'] = self.user.aa[profile][hat]['allow']['path'][path].get('mode', set()) | mode + + + tmpmode = set() + if audit_toggle == 1: + tmpmode = mode- allow_mode + elif audit_toggle == 2: + tmpmode = mode + + self.user.aa[profile][hat]['allow']['path'][path]['audit'] = self.user.aa[profile][hat]['allow']['path'][path].get('audit', set()) | tmpmode + + apparmor.aa.changed[profile] = True + + apparmor.aa.UI_Info(_('Adding %s %s to profile') % (path, apparmor.aa.mode_to_str_user(mode))) + if deleted: + apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted) + + elif ans == 'CMD_DENY': + path = options[selected].strip() + # Add new entry? + self.user.aa[profile][hat]['deny']['path'][path]['mode'] = self.user.aa[profile][hat]['deny']['path'][path].get('mode', set()) | (mode - allow_mode) + + self.user.aa[profile][hat]['deny']['path'][path]['audit'] = self.user.aa[profile][hat]['deny']['path'][path].get('audit', set()) + + apparmor.aa.changed[profile] = True + + done = True + + elif ans == 'CMD_NEW': + arg = options[selected] + if not apparmor.aa.re_match_include(arg): + ans = apparmor.aa.UI_GetString(_('Enter new path: '), arg) +# if ans: +# if not matchliteral(ans, path): +# ynprompt = _('The specified path does not match this log entry:\n\n Log Entry: %s\n Entered Path: %s\nDo you really want to use this path?') % (path,ans) +# key = apparmor.aa.UI_YesNo(ynprompt, 'n') +# if key == 'n': +# continue + apparmor.aa.user_globs.append(ans) + options.append(ans) + default_option = len(options) + + elif ans == 'CMD_GLOB': + newpath = options[selected].strip() + if not apparmor.aa.re_match_include(newpath): + newpath = apparmor.aa.glob_path(newpath) + + if newpath not in options: + options.append(newpath) + default_option = len(options) + else: + default_option = options.index(newpath) + 1 + + elif ans == 'CMD_GLOBEXT': + newpath = options[selected].strip() + if not apparmor.aa.re_match_include(newpath): + newpath = apparmor.aa.glob_path_withext(newpath) + + if newpath not in options: + options.append(newpath) + default_option = len(options) + else: + default_option = options.index(newpath) + 1 + + elif re.search('\d', ans): + default_option = ans + + # + for allow in ['allow', 'deny']: + for family in sorted(other.aa[profile][hat][allow]['netdomain']['rule'].keys()): + # severity handling for net toggles goes here + + for sock_type in sorted(other.aa[profile][hat][allow]['netdomain']['rule'][family].keys()): + if apparmor.aa.profile_known_network(self.user.aa[profile][hat], family, sock_type): + continue + default_option = 1 + options = [] + newincludes = apparmor.aa.match_net_includes(self.user.aa[profile][hat], family, sock_type) + q = apparmor.aa.hasher() + if newincludes: + options += list(map(lambda s: '#include <%s>'%s, sorted(set(newincludes)))) + if True:#options: + options.append('network %s %s' % (family, sock_type)) + q['options'] = options + q['selected'] = default_option - 1 + + q['headers'] = [_('Profile'), apparmor.aa.combine_name(profile, hat)] + q['headers'] += [_('Network Family'), family] + q['headers'] += [_('Socket Type'), sock_type] + + audit_toggle = 0 + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_AUDIT_NEW', + 'CMD_ABORT', 'CMD_FINISHED'] + + q['default'] = 'CMD_ALLOW' + + done = False + while not done: + ans, selected = apparmor.aa.UI_PromptUser(q) + if ans == 'CMD_IGNORE_ENTRY': + done = True + break + + if ans.startswith('CMD_AUDIT'): + audit_toggle = not audit_toggle + audit = '' + if audit_toggle: + audit = 'audit' + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_AUDIT_OFF', + 'CMD_ABORT', 'CMD_FINISHED'] + else: + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_AUDIT_NEW', + 'CMD_ABORT', 'CMD_FINISHED'] + q['headers'] = [_('Profile'), apparmor.aa.combine_name(profile, hat)] + q['headers'] += [_('Network Family'), audit + family] + q['headers'] += [_('Socket Type'), sock_type] + + elif ans == 'CMD_ALLOW': + #print(options, selected) + selection = options[selected] + done = True + if apparmor.aa.re_match_include(selection): #re.search('#include\s+<.+>$', selection): + inc = apparmor.aa.re_match_include(selection) #re.search('#include\s+<(.+)>$', selection).groups()[0] + deleted = 0 + deleted = apparmor.aa.delete_duplicates(self.user.aa[profile][hat], inc) + + self.user.aa[profile][hat]['include'][inc] = True + + apparmor.aa.changed[profile] = True + + apparmor.aa.UI_Info(_('Adding %s to profile') % selection) + if deleted: + apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted) + + else: + self.user.aa[profile][hat]['allow']['netdomain']['audit'][family][sock_type] = audit_toggle + self.user.aa[profile][hat]['allow']['netdomain']['rule'][family][sock_type] = True + + apparmor.aa.changed[profile] = True + + apparmor.aa.UI_Info(_('Adding network access %s %s to profile.') % (family, sock_type)) + + elif ans == 'CMD_DENY': + done = True + self.user.aa[profile][hat]['deny']['netdomain']['rule'][family][sock_type] = True + apparmor.aa.changed[profile] = True + apparmor.aa.UI_Info(_('Denying network access %s %s to profile') % (family, sock_type)) + + else: + done = False + +if __name__ == '__main__': + main() diff --git a/utils/aa-mergeprof.pod b/utils/aa-mergeprof.pod new file mode 100644 index 000000000..272c04688 --- /dev/null +++ b/utils/aa-mergeprof.pod @@ -0,0 +1,33 @@ +=pod + +=head1 NAME + +aa-mergeprof - merge AppArmor security profiles. + +=head1 SYNOPSIS + +BmineE> IuserE> IotherE> [I<-d /path/to/profiles>]> + +=head1 OPTIONS + +B<-d --dir /path/to/profiles> + + Specifies where to look for the AppArmor security profile set. + Defaults to /etc/apparmor.d. + +=head1 DESCRIPTION + +B + +=head1 BUGS + +If you find any bugs, please report them at +L. + +=head1 SEE ALSO + +apparmor(7), apparmor.d(5), aa_change_hat(2), aa-genprof(1), +aa-logprof(1), aa-enforce(1), aa-audit(1), aa-complain(1), +aa-disable(1), and L. + +=cut diff --git a/utils/aa-status b/utils/aa-status index 49104fe27..cc0807ca0 100755 --- a/utils/aa-status +++ b/utils/aa-status @@ -1,4 +1,4 @@ -#!/usr/bin/python +#! /usr/bin/env python # ------------------------------------------------------------------ # # Copyright (C) 2005-2006 Novell/SUSE diff --git a/utils/aa-unconfined b/utils/aa-unconfined index a5dac3e25..7082242be 100755 --- a/utils/aa-unconfined +++ b/utils/aa-unconfined @@ -1,6 +1,6 @@ -#!/usr/bin/perl -w +#! /usr/bin/env python # ---------------------------------------------------------------------- -# Copyright (c) 2005 Novell, Inc. All Rights Reserved. +# 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 @@ -11,103 +11,82 @@ # 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. -# -# To contact Novell about this file by physical or electronic mail, -# you may find current contact information at www.novell.com. # ---------------------------------------------------------------------- -# -# unconfined - -# audit local system for processes listening on network connections -# that are not currently running with a profile. +import argparse +import os +import re +import sys -use strict; -use Getopt::Long; +import apparmor.aa as apparmor -use Immunix::AppArmor; -use Locale::gettext; -use POSIX; +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() -setlocale(LC_MESSAGES, ""); -textdomain("apparmor-utils"); +parser = argparse.ArgumentParser(description=_("Lists unconfined processes having tcp or udp ports")) +parser.add_argument("--paranoid", action="store_true", help=_("scan all processes from /proc")) +args = parser.parse_args() -# options variables -my $paranoid = ''; -my $help = ''; +paranoid = args.paranoid -GetOptions( - 'paranoid' => \$paranoid, - 'help|h' => \$help, -); +aa_mountpoint = apparmor.check_for_apparmor() +if not aa_mountpoint: + raise apparmor.AppArmorException(_("It seems AppArmor was not started. Please enable AppArmor and try again.")) -# tell 'em how to use it... -&usage && exit if $help; +pids = [] +if paranoid: + pids = list(filter(lambda x: re.search(r"^\d+$", x), apparmor.get_subdirectories("/proc"))) +else: + regex_tcp_udp = re.compile(r"^(tcp|udp)\s+\d+\s+\d+\s+\S+\:(\d+)\s+\S+\:(\*|\d+)\s+(LISTEN|\s+)\s+(\d+)\/(\S+)") + import subprocess + if sys.version_info < (3, 0): + output = subprocess.check_output("LANG=C netstat -nlp", shell=True).split("\n") + else: + #Python3 needs to translate a stream of bytes to string with specified encoding + output = str(subprocess.check_output("LANG=C netstat -nlp", shell=True), encoding='utf8').split("\n") -sub usage { - printf(gettext("Usage: %s [ --paranoid ]\n"), $0); - exit 0; -} + for line in output: + match = regex_tcp_udp.search(line) + if match: + pids.append(match.groups()[4]) +# We can safely remove duplicate pid's? +pids = list(map(int, set(pids))) -my $subdomainfs = check_for_subdomain(); +for pid in sorted(pids): + try: + prog = os.readlink("/proc/%s/exe"%pid) + except OSError: + continue + attr = None + if os.path.exists("/proc/%s/attr/current"%pid): + with apparmor.open_file_read("/proc/%s/attr/current"%pid) as current: + for line in current: + if line.startswith("/") or line.startswith("null"): + attr = line.strip() -die gettext("AppArmor does not appear to be started. Please enable AppArmor and try again.") . "\n" - unless $subdomainfs; + cmdline = apparmor.cmd(["cat", "/proc/%s/cmdline"%pid])[1] + pname = cmdline.split("\0")[0] + if '/' in pname and pname != prog: + pname = "(%s)"% pname + else: + pname = "" + regex_interpreter = re.compile(r"^(/usr)?/bin/(python|perl|bash|dash|sh)$") + if not attr: + if regex_interpreter.search(prog): + cmdline = re.sub(r"\x00", " ", cmdline) + cmdline = re.sub(r"\s+$", "", cmdline).strip() -my @pids; -if ($paranoid) { - opendir(PROC, "/proc") or die gettext("Can't read /proc\n"); - @pids = grep { /^\d+$/ } readdir(PROC); - closedir(PROC); -} else { - if (open(NETSTAT, "LANG=C /bin/netstat -nlp |")) { - while () { - chomp; - push @pids, $5 - if /^(tcp|udp)\s+\d+\s+\d+\s+\S+\:(\d+)\s+\S+\:(\*|\d+)\s+(LISTEN|\s+)\s+(\d+)\/(\S+)/; - } - close(NETSTAT); - } -} - -for my $pid (sort { $a <=> $b } @pids) { - my $prog = readlink "/proc/$pid/exe" or next; - my $attr; - if (open(CURRENT, "/proc/$pid/attr/current")) { - while () { - chomp; - $attr = $_ if (/^\// || /^null/); - } - close(CURRENT); - } - my $cmdline = `cat /proc/$pid/cmdline`; - my $pname = (split(/\0/, $cmdline))[0]; - if ($pname =~ /\// && !($pname eq $prog)) { - $pname = "($pname) "; - } else { - $pname = ""; - } - if (not $attr) { - if ($prog =~ m/^(\/usr\/bin\/python|\/usr\/bin\/perl|\/bin\/bash)$/) { - - #my $scriptname = (split(/\0/, `cat /proc/$pid/cmdline`))[1]; - $cmdline =~ s/\0/ /g; - $cmdline =~ s/\s+$//; - chomp $cmdline; - print "$pid $prog ($cmdline) " . gettext("not confined\n"); - } else { - print "$pid $prog $pname" . gettext("not confined\n"); - } - } else { - if ($prog =~ m/^(\/usr\/bin\/python|\/usr\/bin\/perl|\/bin\/bash)$/) { - - #my $scriptname = (split(/\0/, `cat /proc/$pid/cmdline`))[1]; - $cmdline =~ s/\0/ /g; - $cmdline =~ s/\s+$//; - chomp $cmdline; - print "$pid $prog ($cmdline) " . gettext("confined by") . " '$attr'\n"; - } else { - print "$pid $prog $pname" . gettext("confined by") . " '$attr'\n"; - } - } -} + apparmor.UI_Info(_("%s %s (%s) not confined\n")%(pid, prog, cmdline)) + else: + if pname and pname[-1] == ')': + pname += ' ' + apparmor.UI_Info(_("%s %s %snot confined\n")%(pid, prog, pname)) + else: + if regex_interpreter.search(prog): + cmdline = re.sub(r"\0", " ", cmdline) + cmdline = re.sub(r"\s+$", "", cmdline).strip() + apparmor.UI_Info(_("%s %s (%s) confined by '%s'\n")%(pid, prog, cmdline, attr)) + else: + if pname and pname[-1] == ')': + pname += ' ' + apparmor.UI_Info(_("%s %s %sconfined by '%s'\n")%(pid, prog, pname, attr)) diff --git a/utils/aa-unconfined.pod b/utils/aa-unconfined.pod index bd444c0a3..aaf8e4740 100644 --- a/utils/aa-unconfined.pod +++ b/utils/aa-unconfined.pod @@ -27,7 +27,14 @@ not have AppArmor profiles loaded =head1 SYNOPSIS -B +B]> + +=head1 OPTIONS + +B<--paranoid> + + Displays all processes from F filesystem with tcp or udp ports + that do not have AppArmor profiles loaded. =head1 DESCRIPTION @@ -39,7 +46,7 @@ network sockets and do not have AppArmor profiles loaded into the kernel. B must be run as root to retrieve the process executable link from the F filesystem. This program is susceptible to race conditions of several flavours: an unlinked executable will be mishandled; -an executable started before a AppArmor profile is loaded will not +an executable started before an AppArmor profile is loaded will not appear in the output, despite running without confinement; a process that dies between the netstat(8) and further checks will be mishandled. This program only lists processes using TCP and UDP. In short, this diff --git a/utils/apparmor/__init__.py b/utils/apparmor/__init__.py index 94f439406..352e8fc7d 100644 --- a/utils/apparmor/__init__.py +++ b/utils/apparmor/__init__.py @@ -7,3 +7,4 @@ # License published by the Free Software Foundation. # # ------------------------------------------------------------------ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/utils/apparmor/aa.py b/utils/apparmor/aa.py new file mode 100644 index 000000000..9f37239dc --- /dev/null +++ b/utils/apparmor/aa.py @@ -0,0 +1,4278 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +# No old version logs, only 2.6 + supported +from __future__ import with_statement +import inspect +import os +import re +import shutil +import subprocess +import sys +import time +import traceback +import atexit +import tempfile + +import apparmor.config +import apparmor.logparser +import apparmor.severity + +from copy import deepcopy + +from apparmor.common import (AppArmorException, open_file_read, valid_path, hasher, + open_file_write, convert_regexp, DebugLogger) + +import apparmor.ui as aaui + +from apparmor.aamode import (str_to_mode, mode_to_str, contains, split_mode, + mode_to_str_user, mode_contains, AA_OTHER, + flatten_mode, owner_flatten_mode) + +from apparmor.yasti import SendDataToYast, GetDataFromYast, shutdown_yast + +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() + +# Setup logging incase of debugging is enabled +debug_logger = DebugLogger('aa') + +CONFDIR = '/etc/apparmor' +running_under_genprof = False +unimplemented_warning = False + +# The database for severity +sev_db = None +# The file to read log messages from +### Was our +filename = None + +cfg = None +repo_cfg = None + +parser = None +ldd = None +logger = None +profile_dir = None +extra_profile_dir = None +### end our +# To keep track of previously included profile fragments +include = dict() + +existing_profiles = dict() + +seen_events = 0 # was our +# To store the globs entered by users so they can be provided again +user_globs = [] + +## Variables used under logprof +### Were our +t = hasher() # dict() +transitions = hasher() +aa = hasher() # Profiles originally in sd, replace by aa +original_aa = hasher() +extras = hasher() # Inactive profiles from extras +### end our +log = [] +pid = dict() + +seen = hasher() # dir() +profile_changes = hasher() +prelog = hasher() +log_dict = hasher() # dict() +changed = dict() +created = [] +skip = hasher() +helpers = dict() # Preserve this between passes # was our +### logprof ends + +filelist = hasher() # File level variables and stuff in config files + +def on_exit(): + """Shutdowns the logger and records exit if debugging enabled""" + debug_logger.debug('Exiting..') + debug_logger.shutdown() + +# Register the on_exit method with atexit +atexit.register(on_exit) + +def check_for_LD_XXX(file): + """Returns True if specified program contains references to LD_PRELOAD or + LD_LIBRARY_PATH to give the Px/Ux code better suggestions""" + found = False + if not os.path.isfile(file): + return False + size = os.stat(file).st_size + # Limit to checking files under 100k for the sake of speed + if size > 100000: + return False + with open_file_read(file, encoding='ascii') as f_in: + for line in f_in: + if 'LD_PRELOAD' in line or 'LD_LIBRARY_PATH' in line: + found = True + return found + +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 + debug_logger.error(message) + caller = inspect.stack()[1][3] + + # If caller is SendDataToYast or GetDatFromYast simply exit + if caller == 'SendDataToYast' or caller == 'GetDatFromYast': + sys.exit(1) + + # Else tell user what happened + aaui.UI_Important(message) + shutdown_yast() + sys.exit(1) + +def check_for_apparmor(): + """Finds and returns the mointpoint for apparmor None otherwise""" + filesystem = '/proc/filesystems' + mounts = '/proc/mounts' + support_securityfs = False + aa_mountpoint = None + regex_securityfs = re.compile('^\S+\s+(\S+)\s+securityfs\s') + if valid_path(filesystem): + with open_file_read(filesystem) as f_in: + for line in f_in: + if 'securityfs' in line: + support_securityfs = True + if valid_path(mounts): + with open_file_read(mounts) as f_in: + for line in f_in: + if support_securityfs: + match = regex_securityfs.search(line) + if match: + mountpoint = match.groups()[0] + '/apparmor' + if valid_path(mountpoint): + aa_mountpoint = mountpoint + # Check if apparmor is actually mounted there + if not valid_path(aa_mountpoint + '/profiles'): + aa_mountpoint = None + return aa_mountpoint + +def which(file): + """Returns the executable fullpath for the file, None otherwise""" + if sys.version_info >= (3, 3): + return shutil.which(file) + env_dirs = os.getenv('PATH').split(':') + for env_dir in env_dirs: + env_path = env_dir + '/' + file + # Test if the path is executable or not + if os.access(env_path, os.X_OK): + return env_path + return None + +def get_full_path(original_path): + """Return the full path after resolving any symlinks""" + path = original_path + link_count = 0 + if not path.startswith('/'): + path = os.getcwd() + '/' + path + while os.path.islink(path): + link_count += 1 + if link_count > 64: + fatal_error(_("Followed too many links while resolving %s") % (original_path)) + direc, file = os.path.split(path) + link = os.readlink(path) + # If the link an absolute path + if link.startswith('/'): + path = link + else: + # Link is relative path + path = direc + '/' + link + return os.path.realpath(path) + +def find_executable(bin_path): + """Returns the full executable path for the given executable, None otherwise""" + full_bin = None + if os.path.exists(bin_path): + full_bin = get_full_path(bin_path) + else: + if '/' not in bin_path: + env_bin = which(bin_path) + if env_bin: + full_bin = get_full_path(env_bin) + if full_bin and os.path.exists(full_bin): + return full_bin + return None + +def get_profile_filename(profile): + """Returns the full profile name""" + if existing_profiles.get(profile, False): + return existing_profiles[profile] + elif profile.startswith('/'): + # Remove leading / + profile = profile[1:] + else: + profile = "profile_" + profile + profile = profile.replace('/', '.') + full_profilename = profile_dir + '/' + profile + return full_profilename + +def name_to_prof_filename(prof_filename): + """Returns the profile""" + if prof_filename.startswith(profile_dir): + profile = prof_filename.split(profile_dir, 1)[1] + return (prof_filename, profile) + else: + bin_path = find_executable(prof_filename) + if bin_path: + prof_filename = get_profile_filename(bin_path) + if os.path.isfile(prof_filename): + return (prof_filename, bin_path) + else: + return None, None + +def complain(path): + """Sets the profile to complain mode if it exists""" + prof_filename, name = name_to_prof_filename(path) + if not prof_filename: + fatal_error(_("Can't find %s") % path) + set_complain(prof_filename, name) + +def enforce(path): + """Sets the profile to enforce mode if it exists""" + prof_filename, name = name_to_prof_filename(path) + if not prof_filename: + fatal_error(_("Can't find %s") % path) + set_enforce(prof_filename, name) + +def set_complain(filename, program): + """Sets the profile to complain mode""" + aaui.UI_Info(_('Setting %s to complain mode.') % program) + create_symlink('force-complain', filename) + change_profile_flags(filename, program, 'complain', True) + +def set_enforce(filename, program): + """Sets the profile to enforce mode""" + aaui.UI_Info(_('Setting %s to enforce mode.') % program) + delete_symlink('force-complain', filename) + delete_symlink('disable', filename) + change_profile_flags(filename, program, 'complain', False) + +def delete_symlink(subdir, filename): + path = filename + link = re.sub('^%s' % profile_dir, '%s/%s' % (profile_dir, subdir), path) + if link != path and os.path.islink(link): + os.remove(link) + +def create_symlink(subdir, filename): + path = filename + bname = os.path.basename(filename) + if not bname: + raise AppArmorException(_('Unable to find basename for %s.') % filename) + #print(filename) + link = re.sub('^%s' % profile_dir, '%s/%s' % (profile_dir, subdir), path) + #print(link) + #link = link + '/%s'%bname + #print(link) + symlink_dir = os.path.dirname(link) + if not os.path.exists(symlink_dir): + # If the symlink directory does not exist create it + os.makedirs(symlink_dir) + + if not os.path.exists(link): + try: + os.symlink(filename, link) + except: + raise AppArmorException(_('Could not create %s symlink to %s.') % (link, filename)) + +def head(file): + """Returns the first/head line of the file""" + first = '' + if os.path.isfile(file): + with open_file_read(file) as f_in: + try: + first = f_in.readline().rstrip() + except UnicodeDecodeError: + pass + return first + else: + raise AppArmorException(_('Unable to read first line from %s: File Not Found') % file) + +def get_output(params): + """Returns the return code output by running the program with the args given in the list""" + program = params[0] + # args = params[1:] + ret = -1 + output = [] + # program is executable + if os.access(program, os.X_OK): + try: + # Get the output of the program + output = subprocess.check_output(params) + except OSError as e: + raise AppArmorException(_("Unable to fork: %s\n\t%s") % (program, str(e))) + # If exit-codes besides 0 + except subprocess.CalledProcessError as e: + output = e.output + output = output.decode('utf-8').split('\n') + ret = e.returncode + else: + ret = 0 + output = output.decode('utf-8').split('\n') + # Remove the extra empty string caused due to \n if present + if len(output) > 1: + output.pop() + return (ret, output) + +def get_reqs(file): + """Returns a list of paths from ldd output""" + pattern1 = re.compile('^\s*\S+ => (\/\S+)') + pattern2 = re.compile('^\s*(\/\S+)') + reqs = [] + ret, ldd_out = get_output([ldd, file]) + if ret == 0: + for line in ldd_out: + if 'not a dynamic executable' in line: + break + if 'cannot read header' in line: + break + if 'statically linked' in line: + break + match = pattern1.search(line) + if match: + reqs.append(match.groups()[0]) + else: + match = pattern2.search(line) + if match: + reqs.append(match.groups()[0]) + return reqs + +def handle_binfmt(profile, path): + """Modifies the profile to add the requirements""" + reqs_processed = dict() + reqs = get_reqs(path) + while reqs: + library = reqs.pop() + if not reqs_processed.get(library, False): + if get_reqs(library): + reqs += get_reqs(library) + reqs_processed[library] = True + combined_mode = match_prof_incs_to_path(profile, 'allow', library) + if combined_mode: + continue + library = glob_common(library) + if not library: + continue + 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_inactive_profile(local_profile): + if extras.get(local_profile, False): + return {local_profile: extras[local_profile]} + return dict() + +def create_new_profile(localfile): + local_profile = hasher() + 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) + + 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()) + + local_profile[localfile]['allow']['path'][interpreter_path]['mode'] = local_profile[localfile]['allow']['path'][interpreter_path].get('mode', str_to_mode('ix')) | str_to_mode('ix') + + 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 + handle_binfmt(local_profile[localfile], interpreter_path) + else: + + local_profile[localfile]['allow']['path'][localfile]['mode'] = local_profile[localfile]['allow']['path'][localfile].get('mode', str_to_mode('mr')) | str_to_mode('mr') + + local_profile[localfile]['allow']['path'][localfile]['audit'] = local_profile[localfile]['allow']['path'][localfile].get('audit', set()) + + handle_binfmt(local_profile[localfile], localfile) + # Add required hats to the profile if they match the localfile + for hatglob in cfg['required_hats'].keys(): + if re.search(hatglob, localfile): + for hat in sorted(cfg['required_hats'][hatglob].split()): + local_profile[hat]['flags'] = 'complain' + + created.append(localfile) + changed[localfile] = True + + debug_logger.debug("Profile for %s:\n\t%s" % (localfile, local_profile.__str__())) + return {localfile: local_profile} + +def delete_profile(local_prof): + """Deletes the specified file from the disk and remove it from our list""" + profile_file = get_profile_filename(local_prof) + if os.path.isfile(profile_file): + os.remove(profile_file) + if aa.get(local_prof, False): + aa.pop(local_prof) + + #prof_unload(local_prof) + +def confirm_and_abort(): + ans = aaui.UI_YesNo(_('Are you sure you want to abandon this set of profile changes and exit?'), 'n') + if ans == 'y': + aaui.UI_Info(_('Abandoning all changes.')) + shutdown_yast() + for prof in created: + delete_profile(prof) + sys.exit(0) + +def get_profile(prof_name): + profile_data = None + distro = cfg['repository']['distro'] + repo_url = cfg['repository']['url'] + # local_profiles = [] + profile_hash = hasher() + if repo_is_enabled(): + aaui.UI_BusyStart(_('Connecting to repository...')) + status_ok, ret = fetch_profiles_by_name(repo_url, distro, prof_name) + aaui.UI_BusyStop() + if status_ok: + profile_hash = ret + else: + aaui.UI_Important(_('WARNING: Error fetching profiles from the repository')) + inactive_profile = get_inactive_profile(prof_name) + if inactive_profile: + uname = 'Inactive local profile for %s' % prof_name + inactive_profile[prof_name][prof_name]['flags'] = 'complain' + inactive_profile[prof_name][prof_name].pop('filename') + profile_hash[uname]['username'] = uname + profile_hash[uname]['profile_type'] = 'INACTIVE_LOCAL' + profile_hash[uname]['profile'] = serialize_profile(inactive_profile[prof_name], prof_name) + profile_hash[uname]['profile_data'] = inactive_profile + # If no profiles in repo and no inactive profiles + if not profile_hash.keys(): + return None + options = [] + tmp_list = [] + preferred_present = False + preferred_user = cfg['repository'].get('preferred_user', 'NOVELL') + + for p in profile_hash.keys(): + if profile_hash[p]['username'] == preferred_user: + preferred_present = True + else: + tmp_list.append(profile_hash[p]['username']) + + if preferred_present: + options.append(preferred_user) + options += tmp_list + + q = dict() + q['headers'] = ['Profile', prof_name] + q['functions'] = ['CMD_VIEW_PROFILE', 'CMD_USE_PROFILE', 'CMD_CREATE_PROFILE', + 'CMD_ABORT', 'CMD_FINISHED'] + q['default'] = "CMD_VIEW_PROFILE" + q['options'] = options + q['selected'] = 0 + + ans = '' + while 'CMD_USE_PROFILE' not in ans and 'CMD_CREATE_PROFILE' not in ans: + ans, arg = aaui.UI_PromptUser(q) + p = profile_hash[options[arg]] + q['selected'] = options.index(options[arg]) + if ans == 'CMD_VIEW_PROFILE': + if aaui.UI_mode == 'yast': + SendDataToYast({'type': 'dialogue-view-profile', + 'user': options[arg], + 'profile': p['profile'], + 'profile_type': p['profile_type'] + }) + ypath, yarg = GetDataFromYast() + #else: + # pager = get_pager() + # proc = subprocess.Popen(pager, stdin=subprocess.PIPE) + # proc.communicate('Profile submitted by %s:\n\n%s\n\n' % + # (options[arg], p['profile'])) + # proc.kill() + elif ans == 'CMD_USE_PROFILE': + if p['profile_type'] == 'INACTIVE_LOCAL': + profile_data = p['profile_data'] + created.append(prof_name) + else: + profile_data = parse_repo_profile(prof_name, repo_url, p) + return profile_data + +def activate_repo_profiles(url, profiles, complain): + read_profiles() + try: + for p in profiles: + pname = p[0] + profile_data = parse_repo_profile(pname, url, p[1]) + attach_profile_data(aa, profile_data) + write_profile(pname) + if complain: + fname = get_profile_filename(pname) + set_profile_flags(profile_dir + fname, 'complain') + aaui.UI_Info(_('Setting %s to complain mode.') % pname) + except Exception as e: + sys.stderr.write(_("Error activating profiles: %s") % e) + +def autodep(bin_name, pname=''): + bin_full = None + global repo_cfg + if not bin_name and pname.startswith('/'): + bin_name = pname + if not repo_cfg and not cfg['repository'].get('url', False): + repo_conf = apparmor.config.Config('shell', CONFDIR) + repo_cfg = repo_conf.read_config('repository.conf') + if not repo_cfg.get('repository', False) or repo_cfg['repository']['enabled'] == 'later': + UI_ask_to_enable_repo() + if bin_name: + bin_full = find_executable(bin_name) + #if not bin_full: + # bin_full = bin_name + #if not bin_full.startswith('/'): + #return None + # Return if exectuable path not found + if not bin_full: + return None + else: + bin_full = pname # for named profiles + + pname = bin_full + read_inactive_profiles() + profile_data = get_profile(pname) + # Create a new profile if no existing profile + if not profile_data: + profile_data = create_new_profile(pname) + file = get_profile_filename(pname) + attach_profile_data(aa, profile_data) + attach_profile_data(original_aa, profile_data) + if os.path.isfile(profile_dir + '/tunables/global'): + if not filelist.get(file, False): + filelist[file] = hasher() + filelist[file]['include']['tunables/global'] = True + write_profile_ui_feedback(pname) + +def get_profile_flags(filename, program): + # To-Do + # XXX If more than one profile in a file then second one is being ignored XXX + # Do we return flags for both or + flags = '' + with open_file_read(filename) as f_in: + for line in f_in: + if RE_PROFILE_START.search(line): + matches = RE_PROFILE_START.search(line).groups() + profile = matches[1] or matches[3] + flags = matches[6] + if profile == program: + return flags + + raise AppArmorException(_('%s contains no profile') % filename) + +def change_profile_flags(filename, program, flag, set_flag): + old_flags = get_profile_flags(filename, program) + print(old_flags) + newflags = [] + if old_flags: + # Flags maybe white-space and/or , separated + old_flags = old_flags.split(',') + + if not isinstance(old_flags, str): + for i in old_flags: + newflags += i.split() + else: + newflags = old_flags.split() + #newflags = [lambda x:x.strip(), oldflags] + + if set_flag: + if flag not in newflags: + newflags.append(flag) + else: + if flag in newflags: + newflags.remove(flag) + + newflags = ','.join(newflags) + + set_profile_flags(filename, program, newflags) + +def set_profile_flags(prof_filename, program, newflags): + """Reads the old profile file and updates the flags accordingly""" + regex_bin_flag = re.compile('^(\s*)(("??/.+?"??)|(profile\s+("??.+?"??)))\s+((flags=)?\((.*)\)\s+)?\{\s*(#.*)?$') + regex_hat_flag = re.compile('^([a-z]*)\s+([A-Z]*)\s*(#.*)?$') + if os.path.isfile(prof_filename): + with open_file_read(prof_filename) as f_in: + temp_file = tempfile.NamedTemporaryFile('w', prefix=prof_filename, suffix='~', delete=False, dir=profile_dir) + shutil.copymode(prof_filename, temp_file.name) + with open_file_write(temp_file.name) as f_out: + for line in f_in: + comment = '' + if '#' in line: + comment = '#' + line.split('#', 1)[1].rstrip() + match = regex_bin_flag.search(line) + if not line.strip() or line.strip().startswith('#'): + pass + elif match: + matches = match.groups() + space = matches[0] + binary = matches[1] + flag = matches[6] or 'flags=' + flags = matches[7] + if binary == program: + if newflags: + line = '%s%s %s(%s) {%s\n' % (space, binary, flag, newflags, comment) + else: + line = '%s%s {%s\n' % (space, binary, comment) + else: + match = regex_hat_flag.search(line) + if match: + hat, flags = match.groups()[:2] + if newflags: + line = '%s flags=(%s) {%s\n' % (hat, newflags, comment) + else: + line = '%s {%s\n' % (hat, comment) + f_out.write(line) + os.rename(temp_file.name, prof_filename) + +def profile_exists(program): + """Returns True if profile exists, False otherwise""" + # Check cache of profiles + + if existing_profiles.get(program, False): + return True + # Check the disk for profile + prof_path = get_profile_filename(program) + #print(prof_path) + if os.path.isfile(prof_path): + # Add to cache of profile + existing_profiles[program] = prof_path + return True + return False + +def sync_profile(): + user, passw = get_repo_user_pass() + if not user or not passw: + return None + repo_profiles = [] + changed_profiles = [] + new_profiles = [] + serialize_opts = hasher() + status_ok, ret = fetch_profiles_by_user(cfg['repository']['url'], + cfg['repository']['distro'], user) + if not status_ok: + if not ret: + ret = 'UNKNOWN ERROR' + aaui.UI_Important(_('WARNING: Error synchronizing profiles with the repository:\n%s\n') % ret) + else: + users_repo_profiles = ret + serialize_opts['NO_FLAGS'] = True + for prof in sorted(aa.keys()): + if is_repo_profile([aa[prof][prof]]): + repo_profiles.append(prof) + if prof in created: + p_local = serialize_profile(aa[prof], prof, serialize_opts) + if not users_repo_profiles.get(prof, False): + new_profiles.append(prof) + new_profiles.append(p_local) + new_profiles.append('') + else: + p_repo = users_repo_profiles[prof]['profile'] + if p_local != p_repo: + changed_profiles.append(prof) + changed_profiles.append(p_local) + changed_profiles.append(p_repo) + if repo_profiles: + for prof in repo_profiles: + p_local = serialize_profile(aa[prof], prof, serialize_opts) + if not users_repo_profiles.get(prof, False): + new_profiles.append(prof) + new_profiles.append(p_local) + new_profiles.append('') + else: + p_repo = '' + if aa[prof][prof]['repo']['user'] == user: + p_repo = users_repo_profiles[prof]['profile'] + else: + status_ok, ret = fetch_profile_by_id(cfg['repository']['url'], + aa[prof][prof]['repo']['id']) + if status_ok: + p_repo = ret['profile'] + else: + if not ret: + ret = 'UNKNOWN ERROR' + aaui.UI_Important(_('WARNING: Error synchronizing profiles with the repository\n%s') % ret) + continue + if p_repo != p_local: + changed_profiles.append(prof) + changed_profiles.append(p_local) + changed_profiles.append(p_repo) + if changed_profiles: + submit_changed_profiles(changed_profiles) + if new_profiles: + submit_created_profiles(new_profiles) + +def fetch_profile_by_id(url, id): + #To-Do + return None, None + +def fetch_profiles_by_name(url, distro, user): + #to-Do + return None, None + +def fetch_profiles_by_user(url, distro, user): + #to-Do + return None, None + +def submit_created_profiles(new_profiles): + #url = cfg['repository']['url'] + if new_profiles: + if aaui.UI_mode == 'yast': + title = 'New Profiles' + message = 'Please select the newly created profiles that you would like to store in the repository' + yast_select_and_upload_profiles(title, message, new_profiles) + else: + title = 'Submit newly created profiles to the repository' + message = 'Would you like to upload newly created profiles?' + console_select_and_upload_profiles(title, message, new_profiles) + +def submit_changed_profiles(changed_profiles): + #url = cfg['repository']['url'] + if changed_profiles: + if aaui.UI_mode == 'yast': + title = 'Changed Profiles' + message = 'Please select which of the changed profiles would you like to upload to the repository' + yast_select_and_upload_profiles(title, message, changed_profiles) + else: + title = 'Submit changed profiles to the repository' + message = 'The following profiles from the repository were changed.\nWould you like to upload your changes?' + console_select_and_upload_profiles(title, message, changed_profiles) + +def yast_select_and_upload_profiles(title, message, profiles_up): + url = cfg['repository']['url'] + profile_changes = hasher() + profs = profiles_up[:] + for p in profs: + profile_changes[p[0]] = get_profile_diff(p[2], p[1]) + SendDataToYast({'type': 'dialog-select-profiles', + 'title': title, + 'explanation': message, + 'default_select': 'false', + 'disable_ask_upload': 'true', + 'profiles': profile_changes + }) + ypath, yarg = GetDataFromYast() + selected_profiles = [] + changelog = None + changelogs = None + single_changelog = False + if yarg['STATUS'] == 'cancel': + return + else: + selected_profiles = yarg['PROFILES'] + changelogs = yarg['CHANGELOG'] + if changelogs.get('SINGLE_CHANGELOG', False): + changelog = changelogs['SINGLE_CHANGELOG'] + single_changelog = True + user, passw = get_repo_user_pass() + for p in selected_profiles: + profile_string = serialize_profile(aa[p], p) + if not single_changelog: + changelog = changelogs[p] + status_ok, ret = upload_profile(url, user, passw, cfg['repository']['distro'], + p, profile_string, changelog) + if status_ok: + newprofile = ret + newid = newprofile['id'] + set_repo_info(aa[p][p], url, user, newid) + write_profile_ui_feedback(p) + else: + if not ret: + ret = 'UNKNOWN ERROR' + aaui.UI_Important(_('WARNING: An error occurred while uploading the profile %s\n%s') % (p, ret)) + aaui.UI_Info(_('Uploaded changes to repository.')) + if yarg.get('NEVER_ASK_AGAIN'): + unselected_profiles = [] + for p in profs: + if p[0] not in selected_profiles: + unselected_profiles.append(p[0]) + set_profiles_local_only(unselected_profiles) + +def upload_profile(url, user, passw, distro, p, profile_string, changelog): + # To-Do + return None, None + +def console_select_and_upload_profiles(title, message, profiles_up): + url = cfg['repository']['url'] + profs = profiles_up[:] + q = hasher() + q['title'] = title + q['headers'] = ['Repository', url] + q['explanation'] = message + q['functions'] = ['CMD_UPLOAD_CHANGES', 'CMD_VIEW_CHANGES', 'CMD_ASK_LATER', + 'CMD_ASK_NEVER', 'CMD_ABORT'] + q['default'] = 'CMD_VIEW_CHANGES' + q['options'] = [i[0] for i in profs] + q['selected'] = 0 + ans = '' + while 'CMD_UPLOAD_CHANGES' not in ans and 'CMD_ASK_NEVER' not in ans and 'CMD_ASK_LATER' not in ans: + ans, arg = aaui.UI_PromptUser(q) + if ans == 'CMD_VIEW_CHANGES': + display_changes(profs[arg][2], profs[arg][1]) + if ans == 'CMD_NEVER_ASK': + set_profiles_local_only([i[0] for i in profs]) + elif ans == 'CMD_UPLOAD_CHANGES': + changelog = aaui.UI_GetString(_('Changelog Entry: '), '') + user, passw = get_repo_user_pass() + if user and passw: + for p_data in profs: + prof = p_data[0] + prof_string = p_data[1] + status_ok, ret = upload_profile(url, user, passw, + cfg['repository']['distro'], + prof, prof_string, changelog) + if status_ok: + newprof = ret + newid = newprof['id'] + set_repo_info(aa[prof][prof], url, user, newid) + write_profile_ui_feedback(prof) + aaui.UI_Info('Uploaded %s to repository' % prof) + else: + if not ret: + ret = 'UNKNOWN ERROR' + aaui.UI_Important(_('WARNING: An error occurred while uploading the profile %s\n%s') % (prof, ret)) + else: + aaui.UI_Important(_('Repository Error\nRegistration or Signin was unsuccessful. User login\ninformation is required to upload profiles to the repository.\nThese changes could not be sent.')) + +def set_profiles_local_only(profs): + for p in profs: + aa[profs][profs]['repo']['neversubmit'] = True + write_profile_ui_feedback(profs) + + +def build_x_functions(default, options, exec_toggle): + ret_list = [] + fallback_toggle = False + if exec_toggle: + if 'i' in options: + ret_list.append('CMD_ix') + if 'p' in options: + ret_list.append('CMD_pix') + fallback_toggle = True + if 'c' in options: + ret_list.append('CMD_cix') + fallback_toggle = True + if 'n' in options: + ret_list.append('CMD_nix') + fallback_toggle = True + if fallback_toggle: + ret_list.append('CMD_EXEC_IX_OFF') + if 'u' in options: + ret_list.append('CMD_ux') + + else: + if 'i' in options: + ret_list.append('CMD_ix') + if 'c' in options: + ret_list.append('CMD_cx') + fallback_toggle = True + if 'p' in options: + ret_list.append('CMD_px') + fallback_toggle = True + if 'n' in options: + ret_list.append('CMD_nx') + fallback_toggle = True + if 'u' in options: + ret_list.append('CMD_ux') + + if fallback_toggle: + ret_list.append('CMD_EXEC_IX_ON') + + ret_list += ['CMD_DENY', 'CMD_ABORT', 'CMD_FINISHED'] + return ret_list + +def handle_children(profile, hat, root): + entries = root[:] + pid = None + p = None + h = None + prog = None + aamode = None + mode = None + detail = None + to_name = None + uhat = None + capability = None + family = None + sock_type = None + protocol = None + global seen_events + regex_nullcomplain = re.compile('^null(-complain)*-profile$') + + for entry in entries: + if type(entry[0]) != str: + handle_children(profile, hat, entry) + else: + typ = entry.pop(0) + if typ == 'fork': + # If type is fork then we (should) have pid, profile and hat + pid, p, h = entry[:3] + if not regex_nullcomplain.search(p) and not regex_nullcomplain.search(h): + profile = p + hat = h + if hat: + profile_changes[pid] = profile + '//' + hat + else: + profile_changes[pid] = profile + elif typ == 'unknown_hat': + # If hat is not known then we (should) have pid, profile, hat, mode and unknown hat in entry + pid, p, h, aamode, uhat = entry[:5] + if not regex_nullcomplain.search(p): + profile = p + if aa[profile].get(uhat, False): + hat = uhat + continue + new_p = update_repo_profile(aa[profile][profile]) + if new_p and UI_SelectUpdatedRepoProfile(profile, new_p) and aa[profile].get(uhat, False): + hat = uhat + continue + + default_hat = None + for hatglob in cfg.options('defaulthat'): + if re.search(hatglob, profile): + default_hat = cfg['defaulthat'][hatglob] + + context = profile + context = context + ' -> ^%s' % uhat + ans = transitions.get(context, 'XXXINVALIDXXX') + + while ans not in ['CMD_ADDHAT', 'CMD_USEDEFAULT', 'CMD_DENY']: + q = hasher() + q['headers'] = [] + q['headers'] += [_('Profile'), profile] + + if default_hat: + q['headers'] += [_('Default Hat'), default_hat] + + q['headers'] += [_('Requested Hat'), uhat] + + q['functions'] = [] + q['functions'].append('CMD_ADDHAT') + if default_hat: + q['functions'].append('CMD_USEDEFAULT') + q['functions'] += ['CMD_DENY', 'CMD_ABORT', 'CMD_FINISHED'] + + q['default'] = 'CMD_DENY' + if aamode == 'PERMITTING': + q['default'] = 'CMD_ADDHAT' + + seen_events += 1 + + ans = aaui.UI_PromptUser(q) + + transitions[context] = ans + + if ans == 'CMD_ADDHAT': + hat = uhat + aa[profile][hat]['flags'] = aa[profile][profile]['flags'] + elif ans == 'CMD_USEDEFAULT': + hat = default_hat + elif ans == 'CMD_DENY': + # As unknown hat is denied no entry for it should be made + return None + + elif typ == 'capability': + # If capability then we (should) have pid, profile, hat, program, mode, capability + pid, p, h, prog, aamode, capability = entry[:6] + 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]['capability'][capability] = 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] + if not mode: + mode = set() + if not regex_nullcomplain.search(p) and not regex_nullcomplain.search(h): + profile = p + hat = h + if not profile or not hat or not detail: + continue + + domainchange = 'nochange' + if typ == 'exec': + domainchange = 'change' + + # Escape special characters + detail = detail.replace('[', '\[') + detail = detail.replace(']', '\]') + 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 + do_execute = False + exec_target = detail + + if mode & str_to_mode('x'): + if os.path.isdir(exec_target): + mode = mode - apparmor.aamode.ALL_AA_EXEC_TYPE + mode = mode | str_to_mode('ix') + 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: + path = detail + + if prelog[aamode][profile][hat]['path'].get(path, False): + mode |= prelog[aamode][profile][hat]['path'][path] + prelog[aamode][profile][hat]['path'][path] = mode + + if do_execute: + if profile_known_exec(aa[profile][hat], 'exec', exec_target): + continue + + p = update_repo_profile(aa[profile][profile]) + if to_name: + if UI_SelectUpdatedRepoProfile(profile, p) and profile_known_exec(aa[profile][hat], 'exec', to_name): + continue + else: + if UI_SelectUpdatedRepoProfile(profile, p) and profile_known_exec(aa[profile][hat], 'exec', exec_target): + continue + + context_new = profile + if profile != hat: + context_new = context_new + '^%s' % hat + context_new = context_new + ' -> %s' % exec_target + + # ans_new = transitions.get(context_new, '') # XXX ans meant here? + combinedmode = set() + combinedaudit = set() + ## Check return Value Consistency + # Check if path matches any existing regexps in profile + cm, am, m = rematchfrag(aa[profile][hat], 'allow', exec_target) + if cm: + combinedmode |= cm + if am: + combinedaudit |= am + + if combinedmode & str_to_mode('x'): + nt_name = None + for entr in m: + if aa[profile][hat]['allow']['path'].get(entr, False): + nt_name = aa[profile][hat] + break + if to_name and to_name != nt_name: + pass + elif nt_name: + to_name = nt_name + ## Check return value consistency + # Check if the includes from profile match + cm, am, m = match_prof_incs_to_path(aa[profile][hat], 'allow', exec_target) + if cm: + combinedmode |= cm + if am: + combinedaudit |= am + if combinedmode & str_to_mode('x'): + nt_name = None + for entr in m: + if aa[profile][hat]['allow']['path'][entry]['to']: + nt_name = aa[profile][hat]['allow']['path'][entry]['to'] + break + if to_name and to_name != nt_name: + pass + elif nt_name: + to_name = nt_name + + # nx is not used in profiles but in log files. + # Log parsing methods will convert it to its profile form + # nx is internally cx/px/cix/pix + to_name + exec_mode = False + if contains(combinedmode, 'pix'): + if to_name: + ans = 'CMD_nix' + else: + ans = 'CMD_pix' + exec_mode = str_to_mode('pixr') + elif contains(combinedmode, 'cix'): + if to_name: + ans = 'CMD_nix' + else: + ans = 'CMD_cix' + exec_mode = str_to_mode('cixr') + elif contains(combinedmode, 'Pix'): + if to_name: + ans = 'CMD_nix_safe' + else: + ans = 'CMD_pix_safe' + exec_mode = str_to_mode('Pixr') + elif contains(combinedmode, 'Cix'): + if to_name: + ans = 'CMD_nix_safe' + else: + ans = 'CMD_cix_safe' + exec_mode = str_to_mode('Cixr') + elif contains(combinedmode, 'ix'): + ans = 'CMD_ix' + exec_mode = str_to_mode('ixr') + elif contains(combinedmode, 'px'): + if to_name: + ans = 'CMD_nx' + else: + ans = 'CMD_px' + exec_mode = str_to_mode('px') + elif contains(combinedmode, 'cx'): + if to_name: + ans = 'CMD_nx' + else: + ans = 'CMD_cx' + exec_mode = str_to_mode('cx') + elif contains(combinedmode, 'ux'): + ans = 'CMD_ux' + exec_mode = str_to_mode('ux') + elif contains(combinedmode, 'Px'): + if to_name: + ans = 'CMD_nx_safe' + else: + ans = 'CMD_px_safe' + exec_mode = str_to_mode('Px') + elif contains(combinedmode, 'Cx'): + if to_name: + ans = 'CMD_nx_safe' + else: + ans = 'CMD_cx_safe' + exec_mode = str_to_mode('Cx') + elif contains(combinedmode, 'Ux'): + ans = 'CMD_ux_safe' + exec_mode = str_to_mode('Ux') + else: + options = cfg['qualifiers'].get(exec_target, 'ipcnu') + if to_name: + fatal_error(_('%s has transition name but not transition mode') % entry) + + ### If profiled program executes itself only 'ix' option + ##if exec_target == profile: + ##options = 'i' + + # Don't allow hats to cx? + options.replace('c', '') + # Add deny to options + options += 'd' + # Define the default option + default = None + if 'p' in options and os.path.exists(get_profile_filename(exec_target)): + default = 'CMD_px' + sys.stdout.write(_('Target profile exists: %s\n') % get_profile_filename(exec_target)) + elif 'i' in options: + default = 'CMD_ix' + elif 'c' in options: + default = 'CMD_cx' + elif 'n' in options: + default = 'CMD_nx' + else: + default = 'DENY' + + # + parent_uses_ld_xxx = check_for_LD_XXX(profile) + + sev_db.unload_variables() + sev_db.load_variables(profile) + severity = sev_db.rank(exec_target, 'x') + + # Prompt portion starts + q = hasher() + q['headers'] = [] + q['headers'] += [_('Profile'), combine_name(profile, hat)] + if prog and prog != 'HINT': + q['headers'] += [_('Program'), prog] + + # to_name should not exist here since, transitioning is already handeled + q['headers'] += [_('Execute'), exec_target] + q['headers'] += [_('Severity'), severity] + + q['functions'] = [] + # prompt = '\n%s\n' % context_new # XXX + exec_toggle = False + q['functions'] += build_x_functions(default, options, exec_toggle) + + # options = '|'.join(options) + seen_events += 1 + regex_options = re.compile('^CMD_(ix|px|cx|nx|pix|cix|nix|px_safe|cx_safe|nx_safe|pix_safe|cix_safe|nix_safe|ux|ux_safe|EXEC_TOGGLE|DENY)$') + + ans = '' + while not regex_options.search(ans): + ans = aaui.UI_PromptUser(q)[0].strip() + if ans.startswith('CMD_EXEC_IX_'): + exec_toggle = not exec_toggle + q['functions'] = [] + q['functions'] += build_x_functions(default, options, exec_toggle) + ans = '' + continue + if ans == 'CMD_nx' or ans == 'CMD_nix': + arg = exec_target + ynans = 'n' + if profile == hat: + ynans = aaui.UI_YesNo(_('Are you specifying a transition to a local profile?'), 'n') + if ynans == 'y': + if ans == 'CMD_nx': + ans = 'CMD_cx' + else: + ans = 'CMD_cix' + else: + if ans == 'CMD_nx': + ans = 'CMD_px' + else: + ans = 'CMD_pix' + + to_name = aaui.UI_GetString(_('Enter profile name to transition to: '), arg) + + regex_optmode = re.compile('CMD_(px|cx|nx|pix|cix|nix)') + if ans == 'CMD_ix': + exec_mode = str_to_mode('ix') + elif regex_optmode.search(ans): + match = regex_optmode.search(ans).groups()[0] + exec_mode = str_to_mode(match) + px_default = 'n' + px_msg = _("Should AppArmor sanitise the environment when\nswitching profiles?\n\nSanitising environment is more secure,\nbut some applications depend on the presence\nof LD_PRELOAD or LD_LIBRARY_PATH.") + if parent_uses_ld_xxx: + px_msg = _("Should AppArmor sanitise the environment when\nswitching profiles?\n\nSanitising environment is more secure,\nbut this application appears to be using LD_PRELOAD\nor LD_LIBRARY_PATH and sanitising the environment\ncould cause functionality problems.") + + ynans = aaui.UI_YesNo(px_msg, px_default) + if ynans == 'y': + # Disable the unsafe mode + exec_mode = exec_mode - (apparmor.aamode.AA_EXEC_UNSAFE | AA_OTHER(apparmor.aamode.AA_EXEC_UNSAFE)) + elif ans == 'CMD_ux': + exec_mode = str_to_mode('ux') + ynans = aaui.UI_YesNo(_("Launching processes in an unconfined state is a very\ndangerous operation and can cause serious security holes.\n\nAre you absolutely certain you wish to remove all\nAppArmor protection when executing %s ?") % exec_target, 'n') + if ynans == 'y': + ynans = aaui.UI_YesNo(_("Should AppArmor sanitise the environment when\nrunning this program unconfined?\n\nNot sanitising the environment when unconfining\na program opens up significant security holes\nand should be avoided if at all possible."), 'y') + if ynans == 'y': + # Disable the unsafe mode + exec_mode = exec_mode - (apparmor.aamode.AA_EXEC_UNSAFE | AA_OTHER(apparmor.aamode.AA_EXEC_UNSAFE)) + else: + ans = 'INVALID' + transitions[context_new] = ans + + regex_options = re.compile('CMD_(ix|px|cx|nx|pix|cix|nix)') + if regex_options.search(ans): + # For inherit we need r + if exec_mode & str_to_mode('i'): + exec_mode |= str_to_mode('r') + else: + if ans == 'CMD_DENY': + aa[profile][hat]['deny']['path'][exec_target]['mode'] = aa[profile][hat]['deny']['path'][exec_target].get('mode', str_to_mode('x')) | str_to_mode('x') + aa[profile][hat]['deny']['path'][exec_target]['audit'] = aa[profile][hat]['deny']['path'][exec_target].get('audit', set()) + changed[profile] = True + # Skip remaining events if they ask to deny exec + if domainchange == 'change': + return None + + if ans != 'CMD_DENY': + prelog['PERMITTING'][profile][hat]['path'][exec_target] = prelog['PERMITTING'][profile][hat]['path'].get(exec_target, exec_mode) | exec_mode + + log_dict['PERMITTING'][profile] = hasher() + + aa[profile][hat]['allow']['path'][exec_target]['mode'] = aa[profile][hat]['allow']['path'][exec_target].get('mode', exec_mode) + + aa[profile][hat]['allow']['path'][exec_target]['audit'] = aa[profile][hat]['allow']['path'][exec_target].get('audit', set()) + + if to_name: + aa[profile][hat]['allow']['path'][exec_target]['to'] = to_name + + 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) + + aa[profile][hat]['path'][interpreter_path]['mode'] = aa[profile][hat]['path'][interpreter_path].get('mode', str_to_mode('ix')) | str_to_mode('ix') + + aa[profile][hat]['path'][interpreter_path]['audit'] = aa[profile][hat]['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 + + # Update tracking info based on kind of change + + if ans == 'CMD_ix': + if hat: + profile_changes[pid] = '%s//%s' % (profile, hat) + else: + profile_changes[pid] = '%s//' % profile + elif re.search('^CMD_(px|nx|pix|nix)', ans): + if to_name: + exec_target = to_name + if aamode == 'PERMITTING': + if domainchange == 'change': + profile = exec_target + hat = exec_target + profile_changes[pid] = '%s' % profile + + # Check profile exists for px + if not os.path.exists(get_profile_filename(exec_target)): + ynans = 'y' + if exec_mode & str_to_mode('i'): + ynans = aaui.UI_YesNo(_('A profile for %s does not exist.\nDo you want to create one?') % exec_target, 'n') + if ynans == 'y': + helpers[exec_target] = 'enforce' + if to_name: + autodep('', exec_target) + else: + autodep(exec_target, '') + reload_base(exec_target) + elif ans.startswith('CMD_cx') or ans.startswith('CMD_cix'): + if to_name: + exec_target = to_name + if aamode == 'PERMITTING': + if domainchange == 'change': + profile_changes[pid] = '%s//%s' % (profile, exec_target) + + if not aa[profile].get(exec_target, False): + ynans = 'y' + if exec_mode & str_to_mode('i'): + 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 + aa[profile][hat]['declared'] = False + aa[profile][hat]['profile'] = True + + if profile != hat: + aa[profile][hat]['flags'] = aa[profile][profile]['flags'] + + stub_profile = create_new_profile(hat) + + aa[profile][hat]['flags'] = 'complain' + + aa[profile][hat]['allow']['path'] = hasher() + if stub_profile[hat][hat]['allow'].get('path', False): + aa[profile][hat]['allow']['path'] = stub_profile[hat][hat]['allow']['path'] + + aa[profile][hat]['include'] = hasher() + if stub_profile[hat][hat].get('include', False): + aa[profile][hat]['include'] = stub_profile[hat][hat]['include'] + + aa[profile][hat]['allow']['netdomain'] = hasher() + + file_name = aa[profile][profile]['filename'] + filelist[file_name]['profiles'][profile][hat] = True + + elif ans.startswith('CMD_ux'): + profile_changes[pid] = 'unconfined' + if domainchange == 'change': + return None + + elif typ == 'netdomain': + # If netdomain we (should) have pid, profile, hat, program, mode, network family, socket type and protocol + pid, p, h, prog, aamode, family, sock_type, protocol = entry[:8] + + if not regex_nullcomplain.search(p) and not regex_nullcomplain.search(h): + profile = p + hat = h + if not hat or not profile: + continue + if family and sock_type: + prelog[aamode][profile][hat]['netdomain'][family][sock_type] = True + + return None + +PROFILE_MODE_RE = re.compile('r|w|l|m|k|a|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix') +PROFILE_MODE_NT_RE = re.compile('r|w|l|m|k|a|x|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix') +PROFILE_MODE_DENY_RE = re.compile('r|w|l|m|k|a|x') + +##### Repo related functions + +def UI_SelectUpdatedRepoProfile(profile, p): + # To-Do + return False + +def UI_repo_signup(): + # To-Do + return None, None + +def UI_ask_to_enable_repo(): + # To-Do + pass + +def UI_ask_to_upload_profiles(): + # To-Do + pass + +def UI_ask_mode_toggles(audit_toggle, owner_toggle, oldmode): + # To-Do + return (audit_toggle, owner_toggle) + +def parse_repo_profile(fqdbin, repo_url, profile): + # To-Do + pass + +def set_repo_info(profile_data, repo_url, username, iden): + # To-Do + pass + +def is_repo_profile(profile_data): + # To-Do + pass + +def get_repo_user_pass(): + # To-Do + pass +def get_preferred_user(repo_url): + # To-Do + pass +def repo_is_enabled(): + # To-Do + return False + +def update_repo_profile(profile): + # To-Do + return None + +def order_globs(globs, path): + """Returns the globs in sorted order, more specific behind""" + # To-Do + # ATM its lexicographic, should be done to allow better matches later + return sorted(globs) + +def ask_the_questions(): + found = 0 + global seen_events + for aamode in sorted(log_dict.keys()): + # Describe the type of changes + if aamode == 'PERMITTING': + aaui.UI_Info(_('Complain-mode changes:')) + elif aamode == 'REJECTING': + aaui.UI_Info(_('Enforce-mode changes:')) + else: + # This is so wrong! + fatal_error(_('Invalid mode found: %s') % aamode) + + for profile in sorted(log_dict[aamode].keys()): + # Update the repo profiles + p = update_repo_profile(aa[profile][profile]) + if p: + UI_SelectUpdatedRepoProfile(profile, p) + + found += 1 + # Sorted list of hats with the profile name coming first + hats = list(filter(lambda key: key != profile, sorted(log_dict[aamode][profile].keys()))) + if log_dict[aamode][profile].get(profile, False): + hats = [profile] + hats + + for hat in hats: + for capability in sorted(log_dict[aamode][profile][hat]['capability'].keys()): + # skip if capability already in profile + if profile_known_capability(aa[profile][hat], capability): + continue + # Load variables? Don't think so. + severity = sev_db.rank('CAP_%s' % capability) + default_option = 1 + options = [] + newincludes = match_cap_includes(aa[profile][hat], capability) + q = hasher() + + if newincludes: + options += list(map(lambda inc: '#include <%s>' % inc, sorted(set(newincludes)))) + + if options: + options.append('capability %s' % capability) + q['options'] = [options] + q['selected'] = default_option - 1 + + q['headers'] = [_('Profile'), combine_name(profile, hat)] + q['headers'] += [_('Capability'), capability] + q['headers'] += [_('Severity'), severity] + + audit_toggle = 0 + + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_AUDIT_NEW', + 'CMD_ABORT', 'CMD_FINISHED'] + + # In complain mode: events default to allow + # In enforce mode: events default to deny + q['default'] = 'CMD_DENY' + if aamode == 'PERMITTING': + q['default'] = 'CMD_ALLOW' + + seen_events += 1 + + done = False + while not done: + ans, selected = aaui.UI_PromptUser(q) + # Ignore the log entry + if ans == 'CMD_IGNORE_ENTRY': + done = True + break + + if ans == 'CMD_AUDIT': + audit_toggle = not audit_toggle + audit = '' + if audit_toggle: + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_AUDIT_OFF', + 'CMD_ABORT', 'CMD_FINISHED'] + audit = 'audit' + else: + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_AUDIT_NEW', + 'CMD_ABORT', 'CMD_FINISHED', ] + + q['headers'] = [_('Profile'), combine_name(profile, hat), + _('Capability'), audit + capability, + _('Severity'), severity] + + if ans == 'CMD_ALLOW': + selection = '' + if options: + selection = options[selected] + match = re_match_include(selection) + if match: + deleted = False + inc = match # .groups()[0] + deleted = delete_duplicates(aa[profile][hat], inc) + aa[profile][hat]['include'][inc] = True + + aaui.UI_Info(_('Adding %s to profile.') % selection) + if deleted: + aaui.UI_Info(_('Deleted %s previous matching profile entries.') % deleted) + + aa[profile][hat]['allow']['capability'][capability]['set'] = True + aa[profile][hat]['allow']['capability'][capability]['audit'] = audit_toggle + + changed[profile] = True + + aaui.UI_Info(_('Adding capability %s to profile.') % capability) + done = True + + elif ans == 'CMD_DENY': + aa[profile][hat]['deny']['capability'][capability]['set'] = True + changed[profile] = True + + aaui.UI_Info(_('Denying capability %s to profile.') % capability) + done = True + else: + done = False + + # Process all the path entries. + for path in sorted(log_dict[aamode][profile][hat]['path'].keys()): + mode = log_dict[aamode][profile][hat]['path'][path] + # Lookup modes from profile + allow_mode = set() + allow_audit = set() + deny_mode = set() + deny_audit = set() + + fmode, famode, fm = rematchfrag(aa[profile][hat], 'allow', path) + if fmode: + allow_mode |= fmode + if famode: + allow_audit |= famode + + cm, cam, m = rematchfrag(aa[profile][hat], 'deny', path) + if cm: + deny_mode |= cm + if cam: + deny_audit |= cam + + imode, iamode, im = match_prof_incs_to_path(aa[profile][hat], 'allow', path) + if imode: + allow_mode |= imode + if iamode: + allow_audit |= iamode + + cm, cam, m = match_prof_incs_to_path(aa[profile][hat], 'deny', path) + if cm: + deny_mode |= cm + if cam: + deny_audit |= cam + + if deny_mode & apparmor.aamode.AA_MAY_EXEC: + deny_mode |= apparmor.aamode.ALL_AA_EXEC_TYPE + + # Mask off the denied modes + mode = mode - deny_mode + + # If we get an exec request from some kindof event that generates 'PERMITTING X' + # check if its already in allow_mode + # if not add ix permission + if mode & apparmor.aamode.AA_MAY_EXEC: + # Remove all type access permission + mode = mode - apparmor.aamode.ALL_AA_EXEC_TYPE + if not allow_mode & apparmor.aamode.AA_MAY_EXEC: + mode |= str_to_mode('ix') + + # m is not implied by ix + + ### If we get an mmap request, check if we already have it in allow_mode + ##if mode & AA_EXEC_MMAP: + ## # ix implies m, so we don't need to add m if ix is present + ## if contains(allow_mode, 'ix'): + ## mode = mode - AA_EXEC_MMAP + + if not mode: + continue + + matches = [] + + if fmode: + matches += fm + + if imode: + matches += im + + if not mode_contains(allow_mode, mode): + default_option = 1 + options = [] + newincludes = [] + include_valid = False + + for incname in include.keys(): + include_valid = False + # If already present skip + if aa[profile][hat][incname]: + continue + if incname.startswith(profile_dir): + incname = incname.replace(profile_dir + '/', '', 1) + + include_valid = valid_include('', incname) + + if not include_valid: + continue + + cm, am, m = match_include_to_path(incname, 'allow', path) + + if cm and mode_contains(cm, mode): + dm = match_include_to_path(incname, 'deny', path)[0] + # If the mode is denied + if not mode & dm: + if not list(filter(lambda s: '/**' == s, m)): + newincludes.append(incname) + # Add new includes to the options + if newincludes: + options += list(map(lambda s: '#include <%s>' % s, sorted(set(newincludes)))) + # We should have literal the path in options list too + options.append(path) + # Add any the globs matching path from logprof + globs = glob_common(path) + if globs: + matches += globs + # Add any user entered matching globs + for user_glob in user_globs: + if matchliteral(user_glob, path): + matches.append(user_glob) + + matches = list(set(matches)) + if path in matches: + matches.remove(path) + + options += order_globs(matches, path) + default_option = len(options) + + sev_db.unload_variables() + sev_db.load_variables(get_profile_filename(profile)) + severity = sev_db.rank(path, mode_to_str(mode)) + sev_db.unload_variables() + + audit_toggle = 0 + owner_toggle = 0 + if cfg['settings']['default_owner_prompt']: + owner_toggle = cfg['settings']['default_owner_prompt'] + done = False + while not done: + q = hasher() + q['headers'] = [_('Profile'), combine_name(profile, hat), + _('Path'), path] + + if allow_mode: + mode |= allow_mode + tail = '' + s = '' + prompt_mode = None + if owner_toggle == 0: + prompt_mode = flatten_mode(mode) + tail = ' ' + _('(owner permissions off)') + elif owner_toggle == 1: + prompt_mode = mode + elif owner_toggle == 2: + prompt_mode = allow_mode | owner_flatten_mode(mode - allow_mode) + tail = ' ' + _('(force new perms to owner)') + else: + prompt_mode = owner_flatten_mode(mode) + tail = ' ' + _('(force all rule perms to owner)') + + if audit_toggle == 1: + s = mode_to_str_user(allow_mode) + if allow_mode: + s += ', ' + s += 'audit ' + mode_to_str_user(prompt_mode - allow_mode) + tail + elif audit_toggle == 2: + s = 'audit ' + mode_to_str_user(prompt_mode) + tail + else: + s = mode_to_str_user(prompt_mode) + tail + + q['headers'] += [_('Old Mode'), mode_to_str_user(allow_mode), + _('New Mode'), s] + + else: + s = '' + tail = '' + prompt_mode = None + if audit_toggle: + s = 'audit' + if owner_toggle == 0: + prompt_mode = flatten_mode(mode) + tail = ' ' + _('(owner permissions off)') + elif owner_toggle == 1: + prompt_mode = mode + else: + prompt_mode = owner_flatten_mode(mode) + tail = ' ' + _('(force perms to owner)') + + s = mode_to_str_user(prompt_mode) + q['headers'] += [_('Mode'), s] + + q['headers'] += [_('Severity'), severity] + q['options'] = options + q['selected'] = default_option - 1 + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_GLOB', + 'CMD_GLOBEXT', 'CMD_NEW', 'CMD_ABORT', + 'CMD_FINISHED', 'CMD_OTHER'] + q['default'] = 'CMD_DENY' + if aamode == 'PERMITTING': + q['default'] = 'CMD_ALLOW' + + seen_events += 1 + + ans, selected = aaui.UI_PromptUser(q) + + if ans == 'CMD_IGNORE_ENTRY': + done = True + break + + if ans == 'CMD_OTHER': + audit_toggle, owner_toggle = UI_ask_mode_toggles(audit_toggle, owner_toggle, allow_mode) + elif ans == 'CMD_USER_TOGGLE': + owner_toggle += 1 + if not allow_mode and owner_toggle == 2: + owner_toggle += 1 + if owner_toggle > 3: + owner_toggle = 0 + elif ans == 'CMD_ALLOW': + path = options[selected] + done = True + match = re_match_include(path) # .search('^#include\s+<(.+)>$', path) + if match: + inc = match # .groups()[0] + deleted = 0 + deleted = delete_duplicates(aa[profile][hat], inc) + aa[profile][hat]['include'][inc] = True + changed[profile] = True + aaui.UI_Info(_('Adding %s to profile.') % path) + if deleted: + aaui.UI_Info(_('Deleted %s previous matching profile entries.') % deleted) + + else: + if aa[profile][hat]['allow']['path'][path].get('mode', False): + mode |= aa[profile][hat]['allow']['path'][path]['mode'] + deleted = [] + for entry in aa[profile][hat]['allow']['path'].keys(): + if path == entry: + continue + + if matchregexp(path, entry): + if mode_contains(mode, aa[profile][hat]['allow']['path'][entry]['mode']): + deleted.append(entry) + for entry in deleted: + aa[profile][hat]['allow']['path'].pop(entry) + deleted = len(deleted) + + if owner_toggle == 0: + mode = flatten_mode(mode) + #elif owner_toggle == 1: + # mode = mode + elif owner_toggle == 2: + mode = allow_mode | owner_flatten_mode(mode - allow_mode) + elif owner_toggle == 3: + mode = owner_flatten_mode(mode) + + aa[profile][hat]['allow']['path'][path]['mode'] = aa[profile][hat]['allow']['path'][path].get('mode', set()) | mode + + tmpmode = set() + if audit_toggle == 1: + tmpmode = mode - allow_mode + elif audit_toggle == 2: + tmpmode = mode + + aa[profile][hat]['allow']['path'][path]['audit'] = aa[profile][hat]['allow']['path'][path].get('audit', set()) | tmpmode + + changed[profile] = True + + aaui.UI_Info(_('Adding %s %s to profile') % (path, mode_to_str_user(mode))) + if deleted: + aaui.UI_Info(_('Deleted %s previous matching profile entries.') % deleted) + + elif ans == 'CMD_DENY': + path = options[selected].strip() + # Add new entry? + aa[profile][hat]['deny']['path'][path]['mode'] = aa[profile][hat]['deny']['path'][path].get('mode', set()) | (mode - allow_mode) + + aa[profile][hat]['deny']['path'][path]['audit'] = aa[profile][hat]['deny']['path'][path].get('audit', set()) + + changed[profile] = True + + done = True + + elif ans == 'CMD_NEW': + arg = options[selected] + if not re_match_include(arg): + ans = aaui.UI_GetString(_('Enter new path: '), arg) + if ans: + if not matchliteral(ans, path): + ynprompt = _('The specified path does not match this log entry:\n\n Log Entry: %s\n Entered Path: %s\nDo you really want to use this path?') % (path, ans) + key = aaui.UI_YesNo(ynprompt, 'n') + if key == 'n': + continue + + user_globs.append(ans) + options.append(ans) + default_option = len(options) + + elif ans == 'CMD_GLOB': + newpath = options[selected].strip() + if not re_match_include(newpath): + newpath = glob_path(newpath) + + if newpath not in options: + options.append(newpath) + default_option = len(options) + else: + default_option = options.index(newpath) + 1 + + elif ans == 'CMD_GLOBEXT': + newpath = options[selected].strip() + if not re_match_include(newpath): + newpath = glob_path_withext(newpath) + + if newpath not in options: + options.append(newpath) + default_option = len(options) + else: + default_option = options.index(newpath) + 1 + + elif re.search('\d', ans): + default_option = ans + + # + for family in sorted(log_dict[aamode][profile][hat]['netdomain'].keys()): + # severity handling for net toggles goes here + for sock_type in sorted(log_dict[profile][profile][hat]['netdomain'][family].keys()): + if profile_known_network(aa[profile][hat], family, sock_type): + continue + default_option = 1 + options = [] + newincludes = match_net_includes(aa[profile][hat], family, sock_type) + q = hasher() + if newincludes: + options += list(map(lambda s: '#include <%s>' % s, sorted(set(newincludes)))) + if options: + options.append('network %s %s' % (family, sock_type)) + q['options'] = options + q['selected'] = default_option - 1 + + q['headers'] = [_('Profile'), combine_name(profile, hat)] + q['headers'] += [_('Network Family'), family] + q['headers'] += [_('Socket Type'), sock_type] + + audit_toggle = 0 + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_AUDIT_NEW', + 'CMD_ABORT', 'CMD_FINISHED'] + q['default'] = 'CMD_DENY' + + if aamode == 'PERMITTING': + q['default'] = 'CMD_ALLOW' + + seen_events += 1 + + done = False + while not done: + ans, selected = aaui.UI_PromptUser(q) + if ans == 'CMD_IGNORE_ENTRY': + done = True + break + + if ans.startswith('CMD_AUDIT'): + audit_toggle = not audit_toggle + audit = '' + if audit_toggle: + audit = 'audit' + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_AUDIT_OFF', + 'CMD_ABORT', 'CMD_FINISHED'] + else: + q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_AUDIT_NEW', + 'CMD_ABORT', 'CMD_FINISHED'] + q['headers'] = [_('Profile'), combine_name(profile, hat)] + q['headers'] += [_('Network Family'), audit + family] + q['headers'] += [_('Socket Type'), sock_type] + + elif ans == 'CMD_ALLOW': + selection = options[selected] + done = True + if re_match_include(selection): # re.search('#include\s+<.+>$', selection): + inc = re_match_include(selection) # re.search('#include\s+<(.+)>$', selection).groups()[0] + deleted = 0 + deleted = delete_duplicates(aa[profile][hat], inc) + + aa[profile][hat]['include'][inc] = True + + changed[profile] = True + + aaui.UI_Info(_('Adding %s to profile') % selection) + if deleted: + aaui.UI_Info(_('Deleted %s previous matching profile entries.') % deleted) + + else: + aa[profile][hat]['allow']['netdomain']['audit'][family][sock_type] = audit_toggle + aa[profile][hat]['allow']['netdomain']['rule'][family][sock_type] = True + + changed[profile] = True + + aaui.UI_Info(_('Adding network access %s %s to profile.') % (family, sock_type)) + + elif ans == 'CMD_DENY': + done = True + aa[profile][hat]['deny']['netdomain']['rule'][family][sock_type] = True + changed[profile] = True + aaui.UI_Info(_('Denying network access %s %s to profile') % (family, sock_type)) + + else: + done = False + +def glob_path(newpath): + """Glob the given file path""" + if newpath[-1] == '/': + if newpath[-4:] == '/**/' or newpath[-3:] == '/*/': + # /foo/**/ and /foo/*/ => /**/ + newpath = re.sub('/[^/]+/\*{1,2}/$', '/**/', newpath) # re.sub('/[^/]+/\*{1,2}$/', '/\*\*/', newpath) + elif re.search('/[^/]+\*\*[^/]*/$', newpath): + # /foo**/ and /foo**bar/ => /**/ + newpath = re.sub('/[^/]+\*\*[^/]*/$', '/**/', newpath) + elif re.search('/\*\*[^/]+/$', newpath): + # /**bar/ => /**/ + newpath = re.sub('/\*\*[^/]+/$', '/**/', newpath) + else: + newpath = re.sub('/[^/]+/$', '/*/', newpath) + else: + if newpath[-3:] == '/**' or newpath[-2:] == '/*': + # /foo/** and /foo/* => /** + newpath = re.sub('/[^/]+/\*{1,2}$', '/**', newpath) + elif re.search('/[^/]*\*\*[^/]+$', newpath): + # /**foo and /foor**bar => /** + newpath = re.sub('/[^/]*\*\*[^/]+$', '/**', newpath) + elif re.search('/[^/]+\*\*$', newpath): + # /foo** => /** + newpath = re.sub('/[^/]+\*\*$', '/**', newpath) + else: + newpath = re.sub('/[^/]+$', '/*', newpath) + return newpath + +def glob_path_withext(newpath): + """Glob given file path with extension""" + # match /**.ext and /*.ext + match = re.search('/\*{1,2}(\.[^/]+)$', newpath) + if match: + # /foo/**.ext and /foo/*.ext => /**.ext + newpath = re.sub('/[^/]+/\*{1,2}\.[^/]+$', '/**' + match.groups()[0], newpath) + elif re.search('/[^/]+\*\*[^/]*\.[^/]+$', newpath): + # /foo**.ext and /foo**bar.ext => /**.ext + match = re.search('/[^/]+\*\*[^/]*(\.[^/]+)$', newpath) + newpath = re.sub('/[^/]+\*\*[^/]*\.[^/]+$', '/**' + match.groups()[0], newpath) + elif re.search('/\*\*[^/]+\.[^/]+$', newpath): + # /**foo.ext => /**.ext + match = re.search('/\*\*[^/]+(\.[^/]+)$', newpath) + newpath = re.sub('/\*\*[^/]+\.[^/]+$', '/**' + match.groups()[0], newpath) + else: + match = re.search('(\.[^/]+)$', newpath) + if match: + newpath = re.sub('/[^/]+(\.[^/]+)$', '/*' + match.groups()[0], newpath) + return newpath + +def delete_net_duplicates(netrules, incnetrules): + deleted = 0 + hasher_obj = hasher() + copy_netrules = deepcopy(netrules) + if incnetrules and netrules: + incnetglob = False + # Delete matching rules from abstractions + if incnetrules.get('all', False): + incnetglob = True + for fam in copy_netrules['rule'].keys(): + if incnetglob or (type(incnetrules['rule'][fam]) != type(hasher_obj) and incnetrules['rule'][fam]): + if type(netrules['rule'][fam]) == type(hasher_obj): + deleted += len(netrules['rule'][fam].keys()) + else: + deleted += 1 + netrules['rule'].pop(fam) + elif type(netrules['rule'][fam]) != type(hasher_obj) and netrules['rule'][fam]: + continue + else: + for socket_type in copy_netrules['rule'][fam].keys(): + if incnetrules['rule'][fam].get(socket_type, False): + netrules['rule'][fam].pop(socket_type) + deleted += 1 + return deleted + +def delete_cap_duplicates(profilecaps, inccaps): + deleted = [] + if profilecaps and inccaps: + for capname in profilecaps.keys(): + if inccaps[capname].get('set', False) == 1: + deleted.append(capname) + for capname in deleted: + profilecaps.pop(capname) + + return len(deleted) + +def delete_path_duplicates(profile, incname, allow): + deleted = [] + for entry in profile[allow]['path'].keys(): + if entry == '#include <%s>' % incname: + continue + cm, am, m = match_include_to_path(incname, allow, entry) + if cm and mode_contains(cm, profile[allow]['path'][entry]['mode']) and mode_contains(am, profile[allow]['path'][entry]['audit']): + deleted.append(entry) + + for entry in deleted: + profile[allow]['path'].pop(entry) + return len(deleted) + +def delete_duplicates(profile, incname): + deleted = 0 + # Allow rules covered by denied rules shouldn't be deleted + # only a subset allow rules may actually be denied + + if include.get(incname, False): + deleted += delete_net_duplicates(profile['allow']['netdomain'], include[incname][incname]['allow']['netdomain']) + + deleted += delete_net_duplicates(profile['deny']['netdomain'], include[incname][incname]['deny']['netdomain']) + + deleted += delete_cap_duplicates(profile['allow']['capability'], include[incname][incname]['allow']['capability']) + + deleted += delete_cap_duplicates(profile['deny']['capability'], include[incname][incname]['deny']['capability']) + + deleted += delete_path_duplicates(profile, incname, 'allow') + deleted += delete_path_duplicates(profile, incname, 'deny') + + elif filelist.get(incname, False): + deleted += delete_net_duplicates(profile['allow']['netdomain'], filelist[incname][incname]['allow']['netdomain']) + + deleted += delete_net_duplicates(profile['deny']['netdomain'], filelist[incname][incname]['deny']['netdomain']) + + deleted += delete_cap_duplicates(profile['allow']['capability'], filelist[incname][incname]['allow']['capability']) + + deleted += delete_cap_duplicates(profile['deny']['capability'], filelist[incname][incname]['deny']['capability']) + + deleted += delete_path_duplicates(profile, incname, 'allow') + deleted += delete_path_duplicates(profile, incname, 'deny') + + return deleted + +def match_net_include(incname, family, type): + includelist = [incname] + checked = [] + name = None + if includelist: + name = includelist.pop(0) + while name: + checked.append(name) + if netrules_access_check(include[name][name]['allow']['netdomain'], family, type): + return True + + if include[name][name]['include'].keys() and name not in checked: + includelist += include[name][name]['include'].keys() + + if len(includelist): + name = includelist.pop(0) + else: + name = False + + return False + +def match_cap_includes(profile, cap): + newincludes = [] + for incname in include.keys(): + if valid_include(profile, incname) and include[incname][incname]['allow']['capability'][cap].get('set', False) == 1: + newincludes.append(incname) + + return newincludes + +def re_match_include(path): + """Matches the path for include and returns the include path""" + regex_include = re.compile('^\s*#?include\s*<(.*)>\s*(#.*)?$') + match = regex_include.search(path) + if match: + return match.groups()[0] + else: + return None + +def valid_include(profile, incname): + if profile and profile['include'].get(incname, False): + return False + + if cfg['settings']['custom_includes']: + for incm in cfg['settings']['custom_includes'].split(): + if incm == incname: + return True + + if incname.startswith('abstractions/') and os.path.isfile(profile_dir + '/' + incname): + return True + + return False + +def match_net_includes(profile, family, nettype): + newincludes = [] + for incname in include.keys(): + + if valid_include(profile, incname) and match_net_include(incname, family, type): + newincludes.append(incname) + + return newincludes + +def do_logprof_pass(logmark='', passno=0, pid=pid): + # set up variables for this pass +# t = hasher() +# transitions = hasher() +# seen = hasher() # XXX global? + global log + global existing_profiles + log = [] + global sev_db +# aa = hasher() +# profile_changes = hasher() +# prelog = hasher() + log = [] +# log_dict = hasher() +# changed = dict() +# skip = hasher() # XXX global? +# filelist = hasher() + + aaui.UI_Info(_('Reading log entries from %s.') % filename) + + if not passno: + aaui.UI_Info(_('Updating AppArmor profiles in %s.') % profile_dir) + read_profiles() + + if not sev_db: + sev_db = apparmor.severity.Severity(CONFDIR + '/severity.db', _('unknown')) + #print(pid) + #print(existing_profiles) + ##if not repo_cf and cfg['repostory']['url']: + ## repo_cfg = read_config('repository.conf') + ## if not repo_cfg['repository'].get('enabled', False) or repo_cfg['repository]['enabled'] not in ['yes', 'no']: + ## UI_ask_to_enable_repo() + log_reader = apparmor.logparser.ReadLog(pid, filename, existing_profiles, profile_dir, log) + log = log_reader.read_log(logmark) + #read_log(logmark) + + for root in log: + handle_children('', '', root) + #for root in range(len(log)): + #log[root] = handle_children('', '', log[root]) + #print(log) + for pid in sorted(profile_changes.keys()): + set_process(pid, profile_changes[pid]) + + collapse_log() + + ask_the_questions() + + if aaui.UI_mode == 'yast': + # To-Do + pass + + finishing = False + # Check for finished + save_profiles() + + ##if not repo_cfg['repository'].get('upload', False) or repo['repository']['upload'] == 'later': + ## UI_ask_to_upload_profiles() + ##if repo_enabled(): + ## if repo_cgf['repository']['upload'] == 'yes': + ## sync_profiles() + ## created = [] + + # If user selects 'Finish' then we want to exit logprof + if finishing: + return 'FINISHED' + else: + return 'NORMAL' + + +def save_profiles(): + # Ensure the changed profiles are actual active profiles + for prof_name in changed.keys(): + if not is_active_profile(prof_name): + changed.pop(prof_name) + + changed_list = sorted(changed.keys()) + + if changed_list: + + if aaui.UI_mode == 'yast': + # To-Do + # selected_profiles = [] # XXX selected_profiles_ref? + profile_changes = dict() + for prof in changed_list: + oldprofile = serialize_profile(original_aa[prof], prof) + newprofile = serialize_profile(aa[prof], prof) + profile_changes[prof] = get_profile_diff(oldprofile, newprofile) + explanation = _('Select which profile changes you would like to save to the\nlocal profile set.') + title = _('Local profile changes') + SendDataToYast({'type': 'dialog-select-profiles', + 'title': title, + 'explanation': explanation, + 'dialog_select': 'true', + 'get_changelog': 'false', + 'profiles': profile_changes + }) + ypath, yarg = GetDataFromYast() + if yarg['STATUS'] == 'cancel': + return None + else: + selected_profiles_ref = yarg['PROFILES'] + for profile_name in selected_profiles_ref: + write_profile_ui_feedback(profile_name) + reload_base(profile_name) + + else: + q = hasher() + q['title'] = 'Changed Local Profiles' + q['headers'] = [] + q['explanation'] = _('The following local profiles were changed. Would you like to save them?') + q['functions'] = ['CMD_SAVE_CHANGES', 'CMD_SAVE_SELECTED', 'CMD_VIEW_CHANGES', 'CMD_VIEW_CHANGES_CLEAN', 'CMD_ABORT'] + q['default'] = 'CMD_VIEW_CHANGES' + q['options'] = changed + q['selected'] = 0 + ans = '' + arg = None + while ans != 'CMD_SAVE_CHANGES': + if not changed: + return + ans, arg = aaui.UI_PromptUser(q) + if ans == 'CMD_SAVE_SELECTED': + profile_name = list(changed.keys())[arg] + write_profile_ui_feedback(profile_name) + reload_base(profile_name) + #changed.pop(profile_name) + #q['options'] = changed + + elif ans == 'CMD_VIEW_CHANGES': + which = list(changed.keys())[arg] + oldprofile = None + if aa[which][which].get('filename', False): + oldprofile = aa[which][which]['filename'] + else: + oldprofile = get_profile_filename(which) + newprofile = serialize_profile_from_old_profile(aa[which], which, '') + + display_changes_with_comments(oldprofile, newprofile) + + elif ans == 'CMD_VIEW_CHANGES_CLEAN': + which = list(changed.keys())[arg] + oldprofile = serialize_profile(original_aa[which], which, '') + newprofile = serialize_profile(aa[which], which, '') + + display_changes(oldprofile, newprofile) + + for profile_name in changed_list: + write_profile_ui_feedback(profile_name) + reload_base(profile_name) + +def get_pager(): + pass + +def generate_diff(oldprofile, newprofile): + oldtemp = tempfile.NamedTemporaryFile('w') + + oldtemp.write(oldprofile) + oldtemp.flush() + + newtemp = tempfile.NamedTemporaryFile('w') + newtemp.write(newprofile) + newtemp.flush() + + difftemp = tempfile.NamedTemporaryFile('w', delete=False) + + subprocess.call('diff -u -p %s %s > %s' % (oldtemp.name, newtemp.name, difftemp.name), shell=True) + + oldtemp.close() + newtemp.close() + return difftemp + +def get_profile_diff(oldprofile, newprofile): + difftemp = generate_diff(oldprofile, newprofile) + diff = [] + with open_file_read(difftemp.name) as f_in: + for line in f_in: + if not (line.startswith('---') and line .startswith('+++') and line.startswith('@@')): + diff.append(line) + + difftemp.delete = True + difftemp.close() + return ''.join(diff) + +def display_changes(oldprofile, newprofile): + if aaui.UI_mode == 'yast': + aaui.UI_LongMessage(_('Profile Changes'), get_profile_diff(oldprofile, newprofile)) + else: + difftemp = generate_diff(oldprofile, newprofile) + subprocess.call('less %s' % difftemp.name, shell=True) + difftemp.delete = True + difftemp.close() + +def display_changes_with_comments(oldprofile, newprofile): + """Compare the new profile with the existing profile inclusive of all the comments""" + if not os.path.exists(oldprofile): + raise AppArmorException(_("Can't find existing profile %s to compare changes.") % oldprofile) + if aaui.UI_mode == 'yast': + #To-Do + pass + else: + newtemp = tempfile.NamedTemporaryFile('w') + newtemp.write(newprofile) + newtemp.flush() + + difftemp = tempfile.NamedTemporaryFile('w') + + subprocess.call('diff -u -p %s %s > %s' % (oldprofile, newtemp.name, difftemp.name), shell=True) + + newtemp.close() + subprocess.call('less %s' % difftemp.name, shell=True) + difftemp.close() + +def set_process(pid, profile): + # If process not running don't do anything + if not os.path.exists('/proc/%s/attr/current' % pid): + return None + + process = None + try: + process = open_file_read('/proc/%s/attr/current' % pid) + except IOError: + return None + current = process.readline().strip() + process.close() + + if not re.search('^null(-complain)*-profile$', current): + return None + + stats = None + try: + stats = open_file_read('/proc/%s/stat' % pid) + except IOError: + return None + stat = stats.readline().strip() + stats.close() + + match = re.search('^\d+ \((\S+)\) ', stat) + if not match: + return None + + try: + process = open_file_write('/proc/%s/attr/current' % pid) + except IOError: + return None + process.write('setprofile %s' % profile) + process.close() + +def collapse_log(): + for aamode in prelog.keys(): + for profile in prelog[aamode].keys(): + for hat in prelog[aamode][profile].keys(): + + for path in prelog[aamode][profile][hat]['path'].keys(): + mode = prelog[aamode][profile][hat]['path'][path] + + combinedmode = set() + # Is path in original profile? + if aa[profile][hat]['allow']['path'].get(path, False): + combinedmode |= aa[profile][hat]['allow']['path'][path]['mode'] + + # Match path to regexps in profile + combinedmode |= rematchfrag(aa[profile][hat], 'allow', path)[0] + + # Match path from includes + + combinedmode |= match_prof_incs_to_path(aa[profile][hat], 'allow', path)[0] + + if not combinedmode or not mode_contains(combinedmode, mode): + if log_dict[aamode][profile][hat]['path'].get(path, False): + mode |= log_dict[aamode][profile][hat]['path'][path] + + log_dict[aamode][profile][hat]['path'][path] = mode + + for capability in prelog[aamode][profile][hat]['capability'].keys(): + # If capability not already in profile + if not aa[profile][hat]['allow']['capability'][capability].get('set', False): + log_dict[aamode][profile][hat]['capability'][capability] = True + + nd = prelog[aamode][profile][hat]['netdomain'] + for family in nd.keys(): + for sock_type in nd[family].keys(): + if not profile_known_network(aa[profile][hat], family, sock_type): + log_dict[aamode][profile][hat]['netdomain'][family][sock_type] = True + + +def validate_profile_mode(mode, allow, nt_name=None): + if allow == 'deny': + pattern = '^(%s)+$' % PROFILE_MODE_DENY_RE.pattern + if re.search(pattern, mode): + return True + else: + return False + + elif nt_name: + pattern = '^(%s)+$' % PROFILE_MODE_NT_RE.pattern + if re.search(pattern, mode): + return True + else: + return False + + else: + pattern = '^(%s)+$' % PROFILE_MODE_RE.pattern + if re.search(pattern, mode): + return True + else: + return False + +# rpm backup files, dotfiles, emacs backup files should not be processed +# The skippable files type needs be synced with apparmor initscript +def is_skippable_file(path): + """Returns True if filename matches something to be skipped""" + if (re.search('(^|/)\.[^/]*$', path) or re.search('\.rpm(save|new)$', path) + or re.search('\.dpkg-(old|new)$', path) or re.search('\.swp$', path) + or path[-1] == '~' or path == 'README'): + return True + +def is_skippable_dir(path): + if re.search('(disable|cache|force-complain|lxc)', path): + return True + return False + +def check_include_syntax(errors): + # To-Do + pass + +def check_profile_syntax(errors): + # To-Do + pass + +def read_profiles(): + try: + os.listdir(profile_dir) + except: + fatal_error(_("Can't read AppArmor profiles in %s") % profile_dir) + + for file in os.listdir(profile_dir): + if os.path.isfile(profile_dir + '/' + file): + if is_skippable_file(file): + continue + else: + read_profile(profile_dir + '/' + file, True) + +def read_inactive_profiles(): + if not os.path.exists(extra_profile_dir): + return None + try: + os.listdir(profile_dir) + except: + fatal_error(_("Can't read AppArmor profiles in %s") % extra_profile_dir) + + for file in os.listdir(profile_dir): + if os.path.isfile(extra_profile_dir + '/' + file): + if is_skippable_file(file): + continue + else: + read_profile(extra_profile_dir + '/' + file, False) + +def read_profile(file, active_profile): + data = None + try: + with open_file_read(file) as f_in: + data = f_in.readlines() + except IOError: + debug_logger.debug("read_profile: can't read %s - skipping" % file) + return None + + profile_data = parse_profile_data(data, file, 0) + + if profile_data and active_profile: + attach_profile_data(aa, profile_data) + attach_profile_data(original_aa, profile_data) + elif profile_data: + attach_profile_data(extras, profile_data) + + +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(): + profiles[p] = deepcopy(profile_data[p]) + +## Profile parsing regex +RE_PROFILE_START = re.compile('^\s*(("??/.+?"??)|(profile\s+("??.+?"??)))\s+((flags=)?\((.+)\)\s+)?\{\s*(#.*)?$') +RE_PROFILE_END = re.compile('^\s*\}\s*(#.*)?$') +RE_PROFILE_CAP = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?capability\s+(\S+)\s*,\s*(#.*)?$') +RE_PROFILE_LINK = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?link\s+(((subset)|(<=))\s+)?([\"\@\/].*?"??)\s+->\s*([\"\@\/].*?"??)\s*,\s*(#.*)?$') +RE_PROFILE_CHANGE_PROFILE = re.compile('^\s*change_profile\s+->\s*("??.+?"??),(#.*)?$') +RE_PROFILE_ALIAS = re.compile('^\s*alias\s+("??.+?"??)\s+->\s*("??.+?"??)\s*,(#.*)?$') +RE_PROFILE_RLIMIT = re.compile('^\s*set\s+rlimit\s+(.+)\s+(<=)?\s*(.+)\s*,(#.*)?$') +RE_PROFILE_BOOLEAN = re.compile('^\s*(\$\{?\w*\}?)\s*=\s*(true|false)\s*,?\s*(#.*)?$', flags=re.IGNORECASE) +RE_PROFILE_VARIABLE = re.compile('^\s*(@\{?\w+\}?)\s*(\+?=)\s*(@*.+?)\s*,?\s*(#.*)?$') +RE_PROFILE_CONDITIONAL = re.compile('^\s*if\s+(not\s+)?(\$\{?\w*\}?)\s*\{\s*(#.*)?$') +RE_PROFILE_CONDITIONAL_VARIABLE = re.compile('^\s*if\s+(not\s+)?defined\s+(@\{?\w+\}?)\s*\{\s*(#.*)?$') +RE_PROFILE_CONDITIONAL_BOOLEAN = re.compile('^\s*if\s+(not\s+)?defined\s+(\$\{?\w+\}?)\s*\{\s*(#.*)?$') +RE_PROFILE_PATH_ENTRY = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?(owner\s+)?([\"@/].*?)\s+(\S+)(\s+->\s*(.*?))?\s*,\s*(#.*)?$') +RE_PROFILE_NETWORK = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?network(.*)\s*(#.*)?$') +RE_PROFILE_CHANGE_HAT = re.compile('^\s*\^(\"??.+?\"??)\s*,\s*(#.*)?$') +RE_PROFILE_HAT_DEF = re.compile('^\s*\^(\"??.+?\"??)\s+((flags=)?\((.+)\)\s+)*\{\s*(#.*)?$') +RE_NETWORK_FAMILY_TYPE = re.compile('\s+(\S+)\s+(\S+)\s*,$') +RE_NETWORK_FAMILY = re.compile('\s+(\S+)\s*,$') + +def parse_profile_data(data, file, do_include): + profile_data = hasher() + profile = None + hat = None + in_contained_hat = None + repo_data = None + parsed_profiles = [] + initial_comment = '' + + if do_include: + profile = file + hat = file + for lineno, line in enumerate(data): + line = line.strip() + if not line: + continue + # Starting line of a profile + if RE_PROFILE_START.search(line): + matches = RE_PROFILE_START.search(line).groups() + + if profile: + #print(profile, hat) + if profile != hat or not matches[3]: + raise AppArmorException(_('%s profile in %s contains syntax errors in line: %s.') % (profile, file, lineno + 1)) + # Keep track of the start of a profile + if profile and profile == hat and matches[3]: + # local profile + hat = matches[3] + in_contained_hat = True + profile_data[profile][hat]['profile'] = True + else: + if matches[1]: + profile = matches[1] + else: + profile = matches[3] + #print(profile) + if len(profile.split('//')) >= 2: + profile, hat = profile.split('//')[:2] + else: + hat = None + in_contained_hat = False + if hat: + profile_data[profile][hat]['external'] = True + else: + hat = profile + # Profile stored + existing_profiles[profile] = file + + flags = matches[6] + + profile = strip_quotes(profile) + if hat: + hat = strip_quotes(hat) + # save profile name and filename + profile_data[profile][hat]['name'] = profile + profile_data[profile][hat]['filename'] = file + filelist[file]['profiles'][profile][hat] = True + + profile_data[profile][hat]['flags'] = flags + + profile_data[profile][hat]['allow']['netdomain'] = hasher() + profile_data[profile][hat]['allow']['path'] = hasher() + # Save the initial comment + if initial_comment: + profile_data[profile][hat]['initial_comment'] = initial_comment + + initial_comment = '' + + if repo_data: + profile_data[profile][profile]['repo']['url'] = repo_data['url'] + profile_data[profile][profile]['repo']['user'] = repo_data['user'] + + elif RE_PROFILE_END.search(line): + # If profile ends and we're not in one + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected End of Profile reached in file: %s line: %s') % (file, lineno + 1)) + + if in_contained_hat: + hat = profile + in_contained_hat = False + else: + parsed_profiles.append(profile) + profile = None + + initial_comment = '' + + elif RE_PROFILE_CAP.search(line): + matches = RE_PROFILE_CAP.search(line).groups() + + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected capability entry found in file: %s line: %s') % (file, lineno + 1)) + + audit = False + if matches[0]: + audit = True + + allow = 'allow' + if matches[1] and matches[1].strip() == 'deny': + allow = 'deny' + + capability = matches[2] + + profile_data[profile][hat][allow]['capability'][capability]['set'] = True + profile_data[profile][hat][allow]['capability'][capability]['audit'] = audit + + elif RE_PROFILE_LINK.search(line): + matches = RE_PROFILE_LINK.search(line).groups() + + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected link entry found in file: %s line: %s') % (file, lineno + 1)) + + audit = False + if matches[0]: + audit = True + + allow = 'allow' + if matches[1] and matches[1].strip() == 'deny': + allow = 'deny' + + subset = matches[3] + link = strip_quotes(matches[6]) + value = strip_quotes(matches[7]) + profile_data[profile][hat][allow]['link'][link]['to'] = value + profile_data[profile][hat][allow]['link'][link]['mode'] = profile_data[profile][hat][allow]['link'][link].get('mode', set()) | apparmor.aamode.AA_MAY_LINK + + if subset: + profile_data[profile][hat][allow]['link'][link]['mode'] |= apparmor.aamode.AA_LINK_SUBSET + + if audit: + profile_data[profile][hat][allow]['link'][link]['audit'] = profile_data[profile][hat][allow]['link'][link].get('audit', set()) | apparmor.aamode.AA_LINK_SUBSET + else: + profile_data[profile][hat][allow]['link'][link]['audit'] = set() + + elif RE_PROFILE_CHANGE_PROFILE.search(line): + matches = RE_PROFILE_CHANGE_PROFILE.search(line).groups() + + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected change profile entry found in file: %s line: %s') % (file, lineno + 1)) + + cp = strip_quotes(matches[0]) + profile_data[profile][hat]['changes_profile'][cp] = True + + elif RE_PROFILE_ALIAS.search(line): + matches = RE_PROFILE_ALIAS.search(line).groups() + + from_name = strip_quotes(matches[0]) + to_name = strip_quotes(matches[1]) + + if profile: + profile_data[profile][hat]['alias'][from_name] = to_name + else: + if not filelist.get(file, False): + filelist[file] = hasher() + filelist[file]['alias'][from_name] = to_name + + elif RE_PROFILE_RLIMIT.search(line): + matches = RE_PROFILE_RLIMIT.search(line).groups() + + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected rlimit entry found in file: %s line: %s') % (file, lineno + 1)) + + from_name = matches[0] + to_name = matches[2] + + profile_data[profile][hat]['rlimit'][from_name] = to_name + + elif RE_PROFILE_BOOLEAN.search(line): + matches = RE_PROFILE_BOOLEAN.search(line) + + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected boolean definition found in file: %s line: %s') % (file, lineno + 1)) + + bool_var = matches[0] + value = matches[1] + + profile_data[profile][hat]['lvar'][bool_var] = value + + elif RE_PROFILE_VARIABLE.search(line): + # variable additions += and = + matches = RE_PROFILE_VARIABLE.search(line).groups() + + list_var = strip_quotes(matches[0]) + var_operation = matches[1] + value = strip_quotes(matches[2]) + + if profile: + if not profile_data[profile][hat].get('lvar', False): + profile_data[profile][hat]['lvar'][list_var] = [] + store_list_var(profile_data[profile]['lvar'], list_var, value, var_operation) + else: + if not filelist[file].get('lvar', False): + filelist[file]['lvar'][list_var] = [] + store_list_var(filelist[file]['lvar'], list_var, value, var_operation) + + elif RE_PROFILE_CONDITIONAL.search(line): + # Conditional Boolean + pass + + elif RE_PROFILE_CONDITIONAL_VARIABLE.search(line): + # Conditional Variable defines + pass + + elif RE_PROFILE_CONDITIONAL_BOOLEAN.search(line): + # Conditional Boolean defined + pass + + elif RE_PROFILE_PATH_ENTRY.search(line): + matches = RE_PROFILE_PATH_ENTRY.search(line).groups() + + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected path entry found in file: %s line: %s') % (file, lineno + 1)) + + audit = False + if matches[0]: + audit = True + + allow = 'allow' + if matches[1] and matches[1].strip() == 'deny': + allow = 'deny' + + user = False + if matches[2]: + user = True + + path = matches[3].strip() + mode = matches[4] + nt_name = matches[6] + if nt_name: + nt_name = nt_name.strip() + + p_re = convert_regexp(path) + try: + re.compile(p_re) + except: + raise AppArmorException(_('Syntax Error: Invalid Regex %s in file: %s line: %s') % (path, file, lineno + 1)) + + if not validate_profile_mode(mode, allow, nt_name): + raise AppArmorException(_('Invalid mode %s in file: %s line: %s') % (mode, file, lineno + 1)) + + tmpmode = set() + if user: + tmpmode = str_to_mode('%s::' % mode) + else: + tmpmode = str_to_mode(mode) + + profile_data[profile][hat][allow]['path'][path]['mode'] = profile_data[profile][hat][allow]['path'][path].get('mode', set()) | tmpmode + + if nt_name: + profile_data[profile][hat][allow]['path'][path]['to'] = nt_name + + if audit: + profile_data[profile][hat][allow]['path'][path]['audit'] = profile_data[profile][hat][allow]['path'][path].get('audit', set()) | tmpmode + else: + profile_data[profile][hat][allow]['path'][path]['audit'] = set() + + elif re_match_include(line): + # Include files + include_name = re_match_include(line) + if include_name.startswith('local/'): + profile_data[profile][hat]['localinclude'][include_name] = True + + if profile: + profile_data[profile][hat]['include'][include_name] = True + else: + if not filelist.get(file): + filelist[file] = hasher() + filelist[file]['include'][include_name] = True + # If include is a directory + if os.path.isdir(profile_dir + '/' + include_name): + for path in os.listdir(profile_dir + '/' + include_name): + path = path.strip() + if is_skippable_file(path): + continue + if os.path.isfile(profile_dir + '/' + include_name + '/' + path): + file_name = include_name + '/' + path + file_name = file_name.replace(profile_dir + '/', '') + if not include.get(file_name, False): + load_include(file_name) + else: + if not include.get(include_name, False): + load_include(include_name) + + elif RE_PROFILE_NETWORK.search(line): + matches = RE_PROFILE_NETWORK.search(line).groups() + + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected network entry found in file: %s line: %s') % (file, lineno + 1)) + + audit = False + if matches[0]: + audit = True + allow = 'allow' + if matches[1] and matches[1].strip() == 'deny': + allow = 'deny' + network = matches[2] + + if RE_NETWORK_FAMILY_TYPE.search(network): + nmatch = RE_NETWORK_FAMILY_TYPE.search(network).groups() + fam, typ = nmatch[:2] + ##Simply ignore any type subrules if family has True (seperately for allow and deny) + ##This will lead to those type specific rules being lost when written + #if type(profile_data[profile][hat][allow]['netdomain']['rule'].get(fam, False)) == dict: + profile_data[profile][hat][allow]['netdomain']['rule'][fam][typ] = 1 + profile_data[profile][hat][allow]['netdomain']['audit'][fam][typ] = audit + elif RE_NETWORK_FAMILY.search(network): + fam = RE_NETWORK_FAMILY.search(network).groups()[0] + profile_data[profile][hat][allow]['netdomain']['rule'][fam] = True + profile_data[profile][hat][allow]['netdomain']['audit'][fam] = audit + else: + profile_data[profile][hat][allow]['netdomain']['rule']['all'] = True + profile_data[profile][hat][allow]['netdomain']['audit']['all'] = audit # True + + elif RE_PROFILE_CHANGE_HAT.search(line): + matches = RE_PROFILE_CHANGE_HAT.search(line).groups() + + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected change hat declaration found in file: %s line: %s') % (file, lineno + 1)) + + hat = matches[0] + hat = strip_quotes(hat) + + if not profile_data[profile][hat].get('declared', False): + profile_data[profile][hat]['declared'] = True + + elif RE_PROFILE_HAT_DEF.search(line): + # An embedded hat syntax definition starts + matches = RE_PROFILE_HAT_DEF.search(line).groups() + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected hat definition found in file: %s line: %s') % (file, lineno + 1)) + + in_contained_hat = True + hat = matches[0] + hat = strip_quotes(hat) + flags = matches[3] + + profile_data[profile][hat]['flags'] = flags + profile_data[profile][hat]['declared'] = False + #profile_data[profile][hat]['allow']['path'] = hasher() + #profile_data[profile][hat]['allow']['netdomain'] = hasher() + + if initial_comment: + profile_data[profile][hat]['initial_comment'] = initial_comment + initial_comment = '' + if filelist[file]['profiles'][profile].get(hat, False): + raise AppArmorException(_('Error: Multiple definitions for hat %s in profile %s.') % (hat, profile)) + filelist[file]['profiles'][profile][hat] = True + + elif line[0] == '#': + # Handle initial comments + if not profile: + if line.startswith('# vim:syntax') or line.startswith('# Last Modified:'): + continue + line = line.split() + if len(line) > 1 and line[1] == 'REPOSITORY:': + if len(line) == 3: + repo_data = {'neversubmit': True} + elif len(line) == 5: + repo_data = {'url': line[2], + 'user': line[3], + 'id': line[4]} + else: + initial_comment = ' '.join(line) + '\n' + + else: + raise AppArmorException(_('Syntax Error: Unknown line found in file: %s line: %s') % (file, lineno + 1)) + + # Below is not required I'd say + if not do_include: + for hatglob in cfg['required_hats'].keys(): + for parsed_prof in sorted(parsed_profiles): + 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] = hasher() + + # End of file reached but we're stuck in a profile + if profile and not do_include: + raise AppArmorException(_("Syntax Error: Missing '}' . Reached end of file %s while inside profile %s") % (file, profile)) + + return profile_data + +def separate_vars(vs): + """Returns a list of all the values for a variable""" + data = [] + + #data = [i.strip('"') for i in vs.split()] + RE_VARS = re.compile('\s*((\".+?\")|([^\"]\S+))\s*(.*)$') + while RE_VARS.search(vs): + matches = RE_VARS.search(vs).groups() + data.append(strip_quotes(matches[0])) + vs = matches[3] + + return data + +def is_active_profile(pname): + if aa.get(pname, False): + return True + else: + return False + +def store_list_var(var, list_var, value, var_operation): + """Store(add new variable or add values to variable) the variables encountered in the given list_var""" + vlist = separate_vars(value) + if var_operation == '=': + if not var.get(list_var, False): + var[list_var] = set(vlist) + else: + #print('Ignored: New definition for variable for:',list_var,'=', value, 'operation was:',var_operation,'old value=', var[list_var]) + raise AppArmorException(_('An existing variable redefined: %s') % list_var) + elif var_operation == '+=': + if var.get(list_var, False): + var[list_var] = set(var[list_var] + vlist) + else: + raise AppArmorException(_('Values added to a non-existing variable: %s') % list_var) + else: + raise AppArmorException(_('Unknown variable operation: %s') % var_operation) + + +def strip_quotes(data): + if data[0] + data[-1] == '""': + return data[1:-1] + else: + return data + +def quote_if_needed(data): + # quote data if it contains whitespace + if ' ' in data: + data = '"' + data + '"' + return data + +def escape(escape): + escape = strip_quotes(escape) + escape = re.sub('((?') + +def write_change_profile(prof_data, depth): + return write_single(prof_data, depth, '', 'change_profile', 'change_profile -> ', ',') + +def write_alias(prof_data, depth): + return write_pair(prof_data, depth, '', 'alias', 'alias ', ' -> ', ',', quote_if_needed) + +def write_rlimits(prof_data, depth): + return write_pair(prof_data, depth, '', 'rlimit', 'set rlimit ', ' <= ', ',', quote_if_needed) + +def var_transform(ref): + data = [] + for value in ref: + data.append(quote_if_needed(value)) + return ' '.join(data) + +def write_list_vars(prof_data, depth): + return write_pair(prof_data, depth, '', 'lvar', '', ' = ', '', var_transform) + +def write_cap_rules(prof_data, depth, allow): + pre = ' ' * depth + data = [] + allowstr = set_allow_str(allow) + + if prof_data[allow].get('capability', False): + for cap in sorted(prof_data[allow]['capability'].keys()): + audit = '' + if prof_data[allow]['capability'][cap].get('audit', False): + audit = 'audit ' + if prof_data[allow]['capability'][cap].get('set', False): + data.append('%s%s%scapability %s,' % (pre, audit, allowstr, cap)) + data.append('') + + return data + +def write_capabilities(prof_data, depth): + #data = write_single(prof_data, depth, '', 'set_capability', 'set capability ', ',') + data = write_cap_rules(prof_data, depth, 'deny') + data += write_cap_rules(prof_data, depth, 'allow') + return data + +def write_net_rules(prof_data, depth, allow): + pre = ' ' * depth + data = [] + allowstr = set_allow_str(allow) + audit = '' + if prof_data[allow].get('netdomain', False): + if prof_data[allow]['netdomain'].get('rule', False) == 'all': + if prof_data[allow]['netdomain']['audit'].get('all', False): + audit = 'audit ' + data.append('%s%snetwork,' % (pre, audit)) + else: + for fam in sorted(prof_data[allow]['netdomain']['rule'].keys()): + if prof_data[allow]['netdomain']['rule'][fam] is True: + if prof_data[allow]['netdomain']['audit'][fam]: + audit = 'audit' + data.append('%s%s%snetwork %s' % (pre, audit, allowstr, fam)) + else: + for typ in sorted(prof_data[allow]['netdomain']['rule'][fam].keys()): + if prof_data[allow]['netdomain']['audit'][fam].get(typ, False): + audit = 'audit' + data.append('%s%s%snetwork %s %s,' % (pre, audit, allowstr, fam, typ)) + if prof_data[allow].get('netdomain', False): + data.append('') + + return data + +def write_netdomain(prof_data, depth): + data = write_net_rules(prof_data, depth, 'deny') + data += write_net_rules(prof_data, depth, 'allow') + return data + +def write_link_rules(prof_data, depth, allow): + pre = ' ' * depth + data = [] + allowstr = set_allow_str(allow) + + if prof_data[allow].get('link', False): + for path in sorted(prof_data[allow]['link'].keys()): + to_name = prof_data[allow]['link'][path]['to'] + subset = '' + if prof_data[allow]['link'][path]['mode'] & apparmor.aamode.AA_LINK_SUBSET: + subset = 'subset' + audit = '' + if prof_data[allow]['link'][path].get('audit', False): + audit = 'audit ' + path = quote_if_needed(path) + to_name = quote_if_needed(to_name) + data.append('%s%s%slink %s%s -> %s,' % (pre, audit, allowstr, subset, path, to_name)) + data.append('') + + return data + +def write_links(prof_data, depth): + data = write_link_rules(prof_data, depth, 'deny') + data += write_link_rules(prof_data, depth, 'allow') + + return data + +def write_path_rules(prof_data, depth, allow): + pre = ' ' * depth + data = [] + allowstr = set_allow_str(allow) + + if prof_data[allow].get('path', False): + for path in sorted(prof_data[allow]['path'].keys()): + mode = prof_data[allow]['path'][path]['mode'] + audit = prof_data[allow]['path'][path]['audit'] + tail = '' + if prof_data[allow]['path'][path].get('to', False): + tail = ' -> %s' % prof_data[allow]['path'][path]['to'] + user, other = split_mode(mode) + user_audit, other_audit = split_mode(audit) + + while user or other: + ownerstr = '' + tmpmode = 0 + tmpaudit = False + if user - other: + # if no other mode set + ownerstr = 'owner ' + tmpmode = user - other + tmpaudit = user_audit + user = user - tmpmode + else: + if user_audit - other_audit & user: + ownerstr = 'owner ' + tmpaudit = user_audit - other_audit & user + tmpmode = user & tmpaudit + user = user - tmpmode + else: + ownerstr = '' + tmpmode = user | other + tmpaudit = user_audit | other_audit + user = user - tmpmode + other = other - tmpmode + + if tmpmode & tmpaudit: + modestr = mode_to_str(tmpmode & tmpaudit) + path = quote_if_needed(path) + data.append('%saudit %s%s%s %s%s,' % (pre, allowstr, ownerstr, path, modestr, tail)) + tmpmode = tmpmode - tmpaudit + + if tmpmode: + modestr = mode_to_str(tmpmode) + path = quote_if_needed(path) + data.append('%s%s%s%s %s%s,' % (pre, allowstr, ownerstr, path, modestr, tail)) + + data.append('') + return data + +def write_paths(prof_data, depth): + data = write_path_rules(prof_data, depth, 'deny') + data += write_path_rules(prof_data, depth, 'allow') + + return data + +def write_rules(prof_data, depth): + data = write_alias(prof_data, depth) + data += write_list_vars(prof_data, depth) + data += write_includes(prof_data, depth) + data += write_rlimits(prof_data, depth) + data += write_capabilities(prof_data, depth) + data += write_netdomain(prof_data, depth) + data += write_links(prof_data, depth) + data += write_paths(prof_data, depth) + data += write_change_profile(prof_data, depth) + + return data + +def write_piece(profile_data, depth, name, nhat, write_flags): + pre = ' ' * depth + data = [] + wname = None + inhat = False + if name == nhat: + wname = name + else: + wname = name + '//' + nhat + name = nhat + inhat = True + data += write_header(profile_data[name], depth, wname, False, write_flags) + data += write_rules(profile_data[name], depth + 1) + + pre2 = ' ' * (depth + 1) + # External hat declarations + for hat in list(filter(lambda x: x != name, sorted(profile_data.keys()))): + if profile_data[hat].get('declared', False): + data.append('%s^%s,' % (pre2, hat)) + + if not inhat: + # Embedded hats + for hat in list(filter(lambda x: x != name, sorted(profile_data.keys()))): + if not profile_data[hat]['external'] and not profile_data[hat]['declared']: + data.append('') + if profile_data[hat]['profile']: + data += list(map(str, write_header(profile_data[hat], depth + 1, hat, True, write_flags))) + else: + data += list(map(str, write_header(profile_data[hat], depth + 1, '^' + hat, True, write_flags))) + + data += list(map(str, write_rules(profile_data[hat], depth + 2))) + + data.append('%s}' % pre2) + + data.append('%s}' % pre) + + # External hats + for hat in list(filter(lambda x: x != name, sorted(profile_data.keys()))): + if name == nhat and profile_data[hat].get('external', False): + data.append('') + data += list(map(lambda x: ' %s' % x, write_piece(profile_data, depth - 1, name, nhat, write_flags))) + data.append(' }') + + return data + +def serialize_profile(profile_data, name, options): + string = '' + include_metadata = False + include_flags = True + data = [] + + if options: # and type(options) == dict: + if options.get('METADATA', False): + include_metadata = True + if options.get('NO_FLAGS', False): + include_flags = False + + if include_metadata: + string = '# Last Modified: %s\n' % time.asctime() + + if (profile_data[name].get('repo', False) and + profile_data[name]['repo']['url'] and + profile_data[name]['repo']['user'] and + profile_data[name]['repo']['id']): + repo = profile_data[name]['repo'] + string += '# REPOSITORY: %s %s %s\n' % (repo['url'], repo['user'], repo['id']) + elif profile_data[name]['repo']['neversubmit']: + string += '# REPOSITORY: NEVERSUBMIT\n' + +# if profile_data[name].get('initial_comment', False): +# comment = profile_data[name]['initial_comment'] +# comment.replace('\\n', '\n') +# string += comment + '\n' + + prof_filename = get_profile_filename(name) + if filelist.get(prof_filename, False): + data += write_alias(filelist[prof_filename], 0) + data += write_list_vars(filelist[prof_filename], 0) + data += write_includes(filelist[prof_filename], 0) + + #Here should be all the profiles from the files added write after global/common stuff + for prof in sorted(filelist[prof_filename]['profiles'].keys()): + if prof != name: + if original_aa[prof][prof].get('initial_comment', False): + comment = profile_data[name]['initial_comment'] + comment.replace('\\n', '\n') + data += [comment + '\n'] + data += write_piece(original_aa[prof], 0, prof, prof, include_flags) + else: + if profile_data[name].get('initial_comment', False): + comment = profile_data[name]['initial_comment'] + comment.replace('\\n', '\n') + data += [comment + '\n'] + data += write_piece(profile_data, 0, name, name, include_flags) + + string += '\n'.join(data) + + return string + '\n' + +def serialize_profile_from_old_profile(profile_data, name, options): + data = [] + string = '' + include_metadata = False + include_flags = True + prof_filename = get_profile_filename(name) + + write_filelist = deepcopy(filelist[prof_filename]) + write_prof_data = deepcopy(profile_data) + + if options: # and type(options) == dict: + if options.get('METADATA', False): + include_metadata = True + if options.get('NO_FLAGS', False): + include_flags = False + + if include_metadata: + string = '# Last Modified: %s\n' % time.asctime() + + if (profile_data[name].get('repo', False) and + profile_data[name]['repo']['url'] and + profile_data[name]['repo']['user'] and + profile_data[name]['repo']['id']): + repo = profile_data[name]['repo'] + string += '# REPOSITORY: %s %s %s\n' % (repo['url'], repo['user'], repo['id']) + elif profile_data[name]['repo']['neversubmit']: + string += '# REPOSITORY: NEVERSUBMIT\n' + + if not os.path.isfile(prof_filename): + raise AppArmorException(_("Can't find existing profile to modify")) + + # profiles_list = filelist[prof_filename].keys() # XXX + + with open_file_read(prof_filename) as f_in: + profile = None + hat = None + write_methods = {'alias': write_alias, + 'lvar': write_list_vars, + 'include': write_includes, + 'rlimit': write_rlimits, + 'capability': write_capabilities, + 'netdomain': write_netdomain, + 'link': write_links, + 'path': write_paths, + 'change_profile': write_change_profile, + } + # prof_correct = True # XXX correct? + segments = {'alias': False, + 'lvar': False, + 'include': False, + 'rlimit': False, + 'capability': False, + 'netdomain': False, + 'link': False, + 'path': False, + 'change_profile': False, + 'include_local_started': False, + } + #data.append('reading prof') + for line in f_in: + correct = True + line = line.rstrip('\n') + #data.append(' ')#data.append('read: '+line) + if RE_PROFILE_START.search(line): + matches = RE_PROFILE_START.search(line).groups() + if profile and profile == hat and matches[3]: + hat = matches[3] + in_contained_hat = True + if write_prof_data[profile][hat]['profile']: + pass + else: + if matches[1]: + profile = matches[1] + else: + profile = matches[3] + if len(profile.split('//')) >= 2: + profile, hat = profile.split('//')[:2] + else: + hat = None + in_contained_hat = False + if hat and not write_prof_data[profile][hat]['external']: + correct = False + else: + hat = profile + + flags = matches[6] + profile = strip_quotes(profile) + if hat: + hat = strip_quotes(hat) + + if not write_prof_data[hat]['name'] == profile: + correct = False + + if not write_filelist['profiles'][profile][hat] is True: + correct = False + + if not write_prof_data[hat]['flags'] == flags: + correct = False + + #Write the profile start + if correct: + if write_filelist: + data += write_alias(write_filelist, 0) + data += write_list_vars(write_filelist, 0) + data += write_includes(write_filelist, 0) + data.append(line) + else: + if write_prof_data[hat]['name'] == profile: + depth = len(line) - len(line.lstrip()) + data += write_header(write_prof_data[name], int(depth / 2), name, False, include_flags) + + elif RE_PROFILE_END.search(line): + # DUMP REMAINDER OF PROFILE + if profile: + depth = len(line) - len(line.lstrip()) + if True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + + data += write_alias(write_prof_data[name], depth) + data += write_list_vars(write_prof_data[name], depth) + data += write_includes(write_prof_data[name], depth) + data += write_rlimits(write_prof_data, depth) + data += write_capabilities(write_prof_data[name], depth) + data += write_netdomain(write_prof_data[name], depth) + data += write_links(write_prof_data[name], depth) + data += write_paths(write_prof_data[name], depth) + data += write_change_profile(write_prof_data[name], depth) + + write_prof_data.pop(name) + + #Append local includes + data.append(line) + + if not in_contained_hat: + # Embedded hats + depth = int((len(line) - len(line.lstrip())) / 2) + pre2 = ' ' * (depth + 1) + for hat in list(filter(lambda x: x != name, sorted(profile_data.keys()))): + if not profile_data[hat]['external'] and not profile_data[hat]['declared']: + data.append('') + if profile_data[hat]['profile']: + data += list(map(str, write_header(profile_data[hat], depth + 1, hat, True, include_flags))) + else: + data += list(map(str, write_header(profile_data[hat], depth + 1, '^' + hat, True, include_flags))) + + data += list(map(str, write_rules(profile_data[hat], depth + 2))) + + data.append('%s}' % pre2) + + # External hats + for hat in list(filter(lambda x: x != name, sorted(profile_data.keys()))): + if profile_data[hat].get('external', False): + data.append('') + data += list(map(lambda x: ' %s' % x, write_piece(profile_data, depth - 1, name, name, include_flags))) + data.append(' }') + + if in_contained_hat: + #Hat processed, remove it + hat = profile + in_contained_hat = False + else: + profile = None + + elif RE_PROFILE_CAP.search(line): + matches = RE_PROFILE_CAP.search(line).groups() + audit = False + if matches[0]: + audit = matches[0] + + allow = 'allow' + if matches[1] and matches[1].strip() == 'deny': + allow = 'deny' + + capability = matches[2] + + if not write_prof_data[hat][allow]['capability'][capability].get('set', False): + correct = False + if not write_prof_data[hat][allow]['capability'][capability].get(audit, False) == audit: + correct = False + + if correct: + if not segments['capability'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['capability'] = True + write_prof_data[hat][allow]['capability'].pop(capability) + data.append(line) + + #write_prof_data[hat][allow]['capability'][capability].pop(audit) + + #Remove this line + else: + # To-Do + pass + elif RE_PROFILE_LINK.search(line): + matches = RE_PROFILE_LINK.search(line).groups() + audit = False + if matches[0]: + audit = True + allow = 'allow' + if matches[1] and matches[1].strip() == 'deny': + allow = 'deny' + + subset = matches[3] + link = strip_quotes(matches[6]) + value = strip_quotes(matches[7]) + if not write_prof_data[hat][allow]['link'][link]['to'] == value: + correct = False + if not write_prof_data[hat][allow]['link'][link]['mode'] & apparmor.aamode.AA_MAY_LINK: + correct = False + if subset and not write_prof_data[hat][allow]['link'][link]['mode'] & apparmor.aamode.AA_LINK_SUBSET: + correct = False + if audit and not write_prof_data[hat][allow]['link'][link]['audit'] & apparmor.aamode.AA_LINK_SUBSET: + correct = False + + if correct: + if not segments['link'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['link'] = True + write_prof_data[hat][allow]['link'].pop(link) + data.append(line) + else: + # To-Do + pass + + elif RE_PROFILE_CHANGE_PROFILE.search(line): + matches = RE_PROFILE_CHANGE_PROFILE.search(line).groups() + cp = strip_quotes(matches[0]) + + if not write_prof_data[hat]['changes_profile'][cp] is True: + correct = False + + if correct: + if not segments['change_profile'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['change_profile'] = True + write_prof_data[hat]['change_profile'].pop(cp) + data.append(line) + else: + #To-Do + pass + + elif RE_PROFILE_ALIAS.search(line): + matches = RE_PROFILE_ALIAS.search(line).groups() + + from_name = strip_quotes(matches[0]) + to_name = strip_quotes(matches[1]) + + if profile: + if not write_prof_data[hat]['alias'][from_name] == to_name: + correct = False + else: + if not write_filelist['alias'][from_name] == to_name: + correct = False + + if correct: + if not segments['alias'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['alias'] = True + if profile: + write_prof_data[hat]['alias'].pop(from_name) + else: + write_filelist['alias'].pop(from_name) + data.append(line) + else: + #To-Do + pass + + elif RE_PROFILE_RLIMIT.search(line): + matches = RE_PROFILE_RLIMIT.search(line).groups() + + from_name = matches[0] + to_name = matches[2] + + if not write_prof_data[hat]['rlimit'][from_name] == to_name: + correct = False + + if correct: + if not segments['rlimit'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['rlimit'] = True + write_prof_data[hat]['rlimit'].pop(from_name) + data.append(line) + else: + #To-Do + pass + + elif RE_PROFILE_BOOLEAN.search(line): + matches = RE_PROFILE_BOOLEAN.search(line).groups() + bool_var = matches[0] + value = matches[1] + + if not write_prof_data[hat]['lvar'][bool_var] == value: + correct = False + + if correct: + if not segments['lvar'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['lvar'] = True + write_prof_data[hat]['lvar'].pop(bool_var) + data.append(line) + else: + #To-Do + pass + elif RE_PROFILE_VARIABLE.search(line): + matches = RE_PROFILE_VARIABLE.search(line).groups() + list_var = strip_quotes(matches[0]) + var_operation = matches[1] + value = strip_quotes(matches[2]) + var_set = hasher() + if profile: + store_list_var(var_set, list_var, value, var_operation) + if not var_set[list_var] == write_prof_data['lvar'].get(list_var, False): + correct = False + else: + store_list_var(var_set, list_var, value, var_operation) + if not var_set[list_var] == write_filelist['lvar'].get(list_var, False): + correct = False + + if correct: + if not segments['lvar'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['lvar'] = True + if profile: + write_prof_data[hat]['lvar'].pop(list_var) + else: + write_filelist['lvar'].pop(list_var) + data.append(line) + else: + #To-Do + pass + + elif RE_PROFILE_PATH_ENTRY.search(line): + matches = RE_PROFILE_PATH_ENTRY.search(line).groups() + audit = False + if matches[0]: + audit = True + allow = 'allow' + if matches[1] and matches[1].split() == 'deny': + allow = 'deny' + + user = False + if matches[2]: + user = True + + path = matches[3].strip() + mode = matches[4] + nt_name = matches[6] + if nt_name: + nt_name = nt_name.strip() + + tmpmode = set() + if user: + tmpmode = str_to_mode('%s::' % mode) + else: + tmpmode = str_to_mode(mode) + + if not write_prof_data[hat][allow]['path'][path].get('mode', set()) & tmpmode: + correct = False + + if nt_name and not write_prof_data[hat][allow]['path'][path].get('to', False) == nt_name: + correct = False + + if audit and not write_prof_data[hat][allow]['path'][path].get('audit', set()) & tmpmode: + correct = False + + if correct: + if not segments['path'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['path'] = True + write_prof_data[hat][allow]['path'].pop(path) + data.append(line) + else: + #To-Do + pass + + elif re_match_include(line): + include_name = re_match_include(line) + if profile: + if write_prof_data[hat]['include'].get(include_name, False): + if not segments['include'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['include'] = True + write_prof_data[hat]['include'].pop(include_name) + data.append(line) + else: + if write_filelist['include'].get(include_name, False): + write_filelist['include'].pop(include_name) + data.append(line) + + elif RE_PROFILE_NETWORK.search(line): + matches = RE_PROFILE_NETWORK.search(line).groups() + audit = False + if matches[0]: + audit = True + allow = 'allow' + if matches[1] and matches[1].strip() == 'deny': + allow = 'deny' + network = matches[2] + if RE_NETWORK_FAMILY_TYPE.search(network): + nmatch = RE_NETWORK_FAMILY_TYPE.search(network).groups() + fam, typ = nmatch[:2] + if write_prof_data[hat][allow]['netdomain']['rule'][fam][typ] and write_prof_data[hat][allow]['netdomain']['audit'][fam][typ] == audit: + write_prof_data[hat][allow]['netdomain']['rule'][fam].pop(typ) + write_prof_data[hat][allow]['netdomain']['audit'][fam].pop(typ) + data.append(line) + else: + correct = False + + elif RE_NETWORK_FAMILY.search(network): + fam = RE_NETWORK_FAMILY.search(network).groups()[0] + if write_prof_data[hat][allow]['netdomain']['rule'][fam] and write_prof_data[hat][allow]['netdomain']['audit'][fam] == audit: + write_prof_data[hat][allow]['netdomain']['rule'].pop(fam) + write_prof_data[hat][allow]['netdomain']['audit'].pop(fam) + data.append(line) + else: + correct = False + else: + if write_prof_data[hat][allow]['netdomain']['rule']['all'] and write_prof_data[hat][allow]['netdomain']['audit']['all'] == audit: + write_prof_data[hat][allow]['netdomain']['rule'].pop('all') + write_prof_data[hat][allow]['netdomain']['audit'].pop('all') + data.append(line) + else: + correct = False + + if correct: + if not segments['netdomain'] and True in segments.values(): + for segs in list(filter(lambda x: segments[x], segments.keys())): + depth = len(line) - len(line.lstrip()) + data += write_methods[segs](write_prof_data[name], int(depth / 2)) + segments[segs] = False + if write_prof_data[name]['allow'].get(segs, False): + write_prof_data[name]['allow'].pop(segs) + if write_prof_data[name]['deny'].get(segs, False): + write_prof_data[name]['deny'].pop(segs) + segments['netdomain'] = True + + elif RE_PROFILE_CHANGE_HAT.search(line): + matches = RE_PROFILE_CHANGE_HAT.search(line).groups() + hat = matches[0] + hat = strip_quotes(hat) + if not write_prof_data[hat]['declared']: + correct = False + if correct: + data.append(line) + else: + #To-Do + pass + elif RE_PROFILE_HAT_DEF.search(line): + matches = RE_PROFILE_HAT_DEF.search(line).groups() + in_contained_hat = True + hat = matches[0] + hat = strip_quotes(hat) + flags = matches[3] + if not write_prof_data[hat]['flags'] == flags: + correct = False + if not write_prof_data[hat]['declared'] is False: + correct = False + if not write_filelist['profile'][profile][hat]: + correct = False + if correct: + data.append(line) + else: + #To-Do + pass + else: + if correct: + data.append(line) + else: + #To-Do + pass +# data.append('prof done') +# if write_filelist: +# data += write_alias(write_filelist, 0) +# data += write_list_vars(write_filelist, 0) +# data += write_includes(write_filelist, 0) +# data.append('from filelist over') +# data += write_piece(write_prof_data, 0, name, name, include_flags) + + string += '\n'.join(data) + + return string + '\n' + +def write_profile_ui_feedback(profile): + aaui.UI_Info(_('Writing updated profile for %s.') % profile) + write_profile(profile) + +def write_profile(profile): + prof_filename = None + if aa[profile][profile].get('filename', False): + prof_filename = aa[profile][profile]['filename'] + else: + prof_filename = get_profile_filename(profile) + + newprof = tempfile.NamedTemporaryFile('w', suffix='~', delete=False, dir=profile_dir) + if os.path.exists(prof_filename): + shutil.copymode(prof_filename, newprof.name) + else: + #permission_600 = stat.S_IRUSR | stat.S_IWUSR # Owner read and write + #os.chmod(newprof.name, permission_600) + pass + + serialize_options = {} + serialize_options['METADATA'] = True + + profile_string = serialize_profile(aa[profile], profile, serialize_options) + newprof.write(profile_string) + newprof.close() + + os.rename(newprof.name, prof_filename) + + changed.pop(profile) + original_aa[profile] = deepcopy(aa[profile]) + +def matchliteral(aa_regexp, literal): + p_regexp = '^' + convert_regexp(aa_regexp) + '$' + match = False + try: + match = re.search(p_regexp, literal) + except: + return None + return match + +def profile_known_exec(profile, typ, exec_target): + if typ == 'exec': + cm = None + am = None + m = [] + + cm, am, m = rematchfrag(profile, 'deny', exec_target) + if cm & apparmor.aamode.AA_MAY_EXEC: + return -1 + + cm, am, m = match_prof_incs_to_path(profile, 'deny', exec_target) + if cm & apparmor.aamode.AA_MAY_EXEC: + return -1 + + cm, am, m = rematchfrag(profile, 'allow', exec_target) + if cm & apparmor.aamode.AA_MAY_EXEC: + return 1 + + cm, am, m = match_prof_incs_to_path(profile, 'allow', exec_target) + if cm & apparmor.aamode.AA_MAY_EXEC: + return 1 + + return 0 + +def profile_known_capability(profile, capname): + if profile['deny']['capability'][capname].get('set', False): + return -1 + + if profile['allow']['capability'][capname].get('set', False): + return 1 + + for incname in profile['include'].keys(): + if include[incname][incname]['deny']['capability'][capname].get('set', False): + return -1 + if include[incname][incname]['allow']['capability'][capname].get('set', False): + return 1 + + return 0 + +def profile_known_network(profile, family, sock_type): + if netrules_access_check(profile['deny']['netdomain'], family, sock_type): + return -1 + if netrules_access_check(profile['allow']['netdomain'], family, sock_type): + return 1 + + for incname in profile['include'].keys(): + if netrules_access_check(include[incname][incname]['deny']['netdomain'], family, sock_type): + return -1 + if netrules_access_check(include[incname][incname]['allow']['netdomain'], family, sock_type): + return 1 + + return 0 + +def netrules_access_check(netrules, family, sock_type): + if not netrules: + return 0 + all_net = False + all_net_family = False + net_family_sock = False + if netrules['rule'].get('all', False): + all_net = True + if netrules['rule'].get(family, False) is True: + all_net_family = True + if (netrules['rule'].get(family, False) and + type(netrules['rule'][family]) == dict and + netrules['rule'][family][sock_type]): + net_family_sock = True + + if all_net or all_net_family or net_family_sock: + return True + else: + return False + +def reload_base(bin_path): + if not check_for_apparmor(): + return None + + prof_filename = get_profile_filename(bin_path) + + subprocess.call("cat '%s' | %s -I%s -r >/dev/null 2>&1" % (prof_filename, parser, profile_dir), shell=True) + +def reload(bin_path): + bin_path = find_executable(bin_path) + if not bin_path: + return None + + return reload_base(bin_path) + +def get_include_data(filename): + data = [] + filename = profile_dir + '/' + filename + if os.path.exists(filename): + with open_file_read(filename) as f_in: + data = f_in.readlines() + else: + raise AppArmorException(_('File Not Found: %s') % filename) + return data + +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): + 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))) + + return 0 + +def rematchfrag(frag, allow, path): + combinedmode = set() + combinedaudit = set() + matches = [] + if not frag: + return combinedmode, combinedaudit, matches + for entry in frag[allow]['path'].keys(): + match = matchliteral(entry, path) + if match: + #print(frag[allow]['path'][entry]['mode']) + combinedmode |= frag[allow]['path'][entry].get('mode', set()) + combinedaudit |= frag[allow]['path'][entry].get('audit', set()) + matches.append(entry) + + return combinedmode, combinedaudit, matches + +def match_include_to_path(incname, allow, path): + combinedmode = set() + combinedaudit = set() + matches = [] + includelist = [incname] + while includelist: + incfile = str(includelist.pop(0)) + # ret = load_include(incfile) + load_include(incfile) + if not include.get(incfile, {}): + continue + cm, am, m = rematchfrag(include[incfile].get(incfile, {}), allow, path) + #print(incfile, cm, am, m) + if cm: + combinedmode |= cm + combinedaudit |= am + matches += m + + if include[incfile][incfile][allow]['path'][path]: + combinedmode |= include[incfile][incfile][allow]['path'][path]['mode'] + combinedaudit |= include[incfile][incfile][allow]['path'][path]['audit'] + + if include[incfile][incfile]['include'].keys(): + includelist += include[incfile][incfile]['include'].keys() + + return combinedmode, combinedaudit, matches + +def match_prof_incs_to_path(frag, allow, path): + combinedmode = set() + combinedaudit = set() + matches = [] + + includelist = list(frag['include'].keys()) + while includelist: + incname = includelist.pop(0) + cm, am, m = match_include_to_path(incname, allow, path) + if cm: + combinedmode |= cm + combinedaudit |= am + matches += m + + return combinedmode, combinedaudit, matches + +def suggest_incs_for_path(incname, path, allow): + combinedmode = set() + combinedaudit = set() + matches = [] + + includelist = [incname] + while includelist: + inc = includelist.pop(0) + cm, am, m = rematchfrag(include[inc][inc], 'allow', path) + if cm: + combinedmode |= cm + combinedaudit |= am + matches += m + + if include[inc][inc]['allow']['path'].get(path, False): + combinedmode |= include[inc][inc]['allow']['path'][path]['mode'] + combinedaudit |= include[inc][inc]['allow']['path'][path]['audit'] + + if include[inc][inc]['include'].keys(): + includelist += include[inc][inc]['include'].keys() + + return combinedmode, combinedaudit, matches + +def check_qualifiers(program): + if cfg['qualifiers'].get(program, False): + if cfg['qualifiers'][program] != 'p': + fatal_error(_("%s is currently marked as a program that should not have its own\nprofile. Usually, programs are marked this way if creating a profile for \nthem is likely to break the rest of the system. If you know what you\'re\ndoing and are certain you want to create a profile for this program, edit\nthe corresponding entry in the [qualifiers] section in /etc/apparmor/logprof.conf.") % program) + return False + +def get_subdirectories(current_dir): + """Returns a list of all directories directly inside given directory""" + if sys.version_info < (3, 0): + return os.walk(current_dir).next()[1] + else: + return os.walk(current_dir).__next__()[1] + +def loadincludes(): + incdirs = get_subdirectories(profile_dir) + for idir in incdirs: + if is_skippable_dir(idir): + continue + for dirpath, dirname, files in os.walk(profile_dir + '/' + idir): + if is_skippable_dir(dirpath): + continue + for fi in files: + if is_skippable_file(fi): + continue + else: + fi = dirpath + '/' + fi + fi = fi.replace(profile_dir + '/', '', 1) + load_include(fi) + +def glob_common(path): + globs = [] + + if re.search('[\d\.]+\.so$', path) or re.search('\.so\.[\d\.]+$', path): + libpath = path + libpath = re.sub('[\d\.]+\.so$', '*.so', libpath) + libpath = re.sub('\.so\.[\d\.]+$', '.so.*', libpath) + if libpath != path: + globs.append(libpath) + + for glob in cfg['globs']: + if re.search(glob, path): + globbedpath = path + globbedpath = re.sub(glob, cfg['globs'][glob], path) + if globbedpath != path: + globs.append(globbedpath) + + return sorted(set(globs)) + +def combine_name(name1, name2): + if name1 == name2: + return name1 + else: + return '%s^%s' % (name1, name2) + +def split_name(name): + names = name.split('^') + if len(names) == 1: + return name, name + else: + return names[0], names[1] +def commonprefix(new, old): + match = re.search(r'^([^\0]*)[^\0]*(\0\1[^\0]*)*$', '\0'.join([new, old])) + if match: + return match.groups()[0] + return match + +def commonsuffix(new, old): + match = commonprefix(new[-1::-1], old[-1::-1]) + if match: + return match[-1::-1] + +def matchregexp(new, old): + if re.search('\{.*(\,.*)*\}', old): + return None + +# if re.search('\[.+\]', old) or re.search('\*', old) or re.search('\?', old): +# +# new_reg = convert_regexp(new) +# old_reg = convert_regexp(old) +# +# pref = commonprefix(new, old) +# if pref: +# if convert_regexp('(*,**)$') in pref: +# pref = pref.replace(convert_regexp('(*,**)$'), '') +# new = new.replace(pref, '', 1) +# old = old.replace(pref, '', 1) +# +# suff = commonsuffix(new, old) +# if suffix: +# pass + new_reg = convert_regexp(new) + if re.search(new_reg, old): + return True + + return None + +######Initialisations###### + +conf = apparmor.config.Config('ini', CONFDIR) +cfg = conf.read_config('logprof.conf') + +#print(cfg['settings']) +#if 'default_owner_prompt' in cfg['settings']: +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' +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/' + +parser = conf.find_first_file(cfg['settings']['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') + +filename = conf.find_first_file(cfg['settings']['logfiles']) or '/var/log/syslog' +if not os.path.isfile(filename): + raise AppArmorException('Can\'t find system log.') + +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/aamode.py b/utils/apparmor/aamode.py new file mode 100644 index 000000000..d0a5cb657 --- /dev/null +++ b/utils/apparmor/aamode.py @@ -0,0 +1,280 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import re + +def AA_OTHER(mode): + other = set() + for i in mode: + other.add('::%s' % i) + return other + +def AA_OTHER_REMOVE(mode): + other = set() + for i in mode: + if '::' in i: + other.add(i[2:]) + return other + +AA_MAY_EXEC = set('x') +AA_MAY_WRITE = set('w') +AA_MAY_READ = set('r') +AA_MAY_APPEND = set('a') +AA_MAY_LINK = set('l') +AA_MAY_LOCK = set('k') +AA_EXEC_MMAP = set('m') +AA_EXEC_UNSAFE = set(['execunsafe']) +AA_EXEC_INHERIT = set('i') +AA_EXEC_UNCONFINED = set('U') +AA_EXEC_PROFILE = set('P') +AA_EXEC_CHILD = set('C') +AA_EXEC_NT = set('N') +AA_LINK_SUBSET = set(['linksubset']) +#AA_OTHER_SHIFT = 14 +#AA_USER_MASK = 16384 - 1 + +AA_EXEC_TYPE = (AA_MAY_EXEC | AA_EXEC_UNSAFE | AA_EXEC_INHERIT | + AA_EXEC_UNCONFINED | AA_EXEC_PROFILE | AA_EXEC_CHILD | AA_EXEC_NT) + +ALL_AA_EXEC_TYPE = AA_EXEC_TYPE + +MODE_HASH = {'x': AA_MAY_EXEC, 'X': AA_MAY_EXEC, + 'w': AA_MAY_WRITE, 'W': AA_MAY_WRITE, + 'r': AA_MAY_READ, 'R': AA_MAY_READ, + 'a': AA_MAY_APPEND, 'A': AA_MAY_APPEND, + 'l': AA_MAY_LINK, 'L': AA_MAY_LINK, + 'k': AA_MAY_LOCK, 'K': AA_MAY_LOCK, + 'm': AA_EXEC_MMAP, 'M': AA_EXEC_MMAP, + 'i': AA_EXEC_INHERIT, 'I': AA_EXEC_INHERIT, + 'u': AA_EXEC_UNCONFINED | AA_EXEC_UNSAFE, # Unconfined + Unsafe + 'U': AA_EXEC_UNCONFINED, + 'p': AA_EXEC_PROFILE | AA_EXEC_UNSAFE, # Profile + unsafe + 'P': AA_EXEC_PROFILE, + 'c': AA_EXEC_CHILD | AA_EXEC_UNSAFE, # Child + Unsafe + 'C': AA_EXEC_CHILD, + 'n': AA_EXEC_NT | AA_EXEC_UNSAFE, + 'N': AA_EXEC_NT + } + +LOG_MODE_RE = re.compile('(r|w|l|m|k|a|x|ix|ux|px|cx|nx|pix|cix|Ix|Ux|Px|PUx|Cx|Nx|Pix|Cix)') +MODE_MAP_RE = re.compile('(r|w|l|m|k|a|x|i|u|p|c|n|I|U|P|C|N)') + +def str_to_mode(string): + if not string: + return set() + user, other = split_log_mode(string) + if not user: + user = other + + mode = sub_str_to_mode(user) + #print(string, mode) + #print(string, 'other', sub_str_to_mode(other)) + mode |= (AA_OTHER(sub_str_to_mode(other))) + #print (string, mode) + #print('str_to_mode:', mode) + return mode + +def sub_str_to_mode(string): + mode = set() + if not string: + return mode + while string: + tmp = MODE_MAP_RE.search(string) + if tmp: + tmp = tmp.groups()[0] + string = MODE_MAP_RE.sub('', string, 1) + if tmp and MODE_HASH.get(tmp, False): + mode |= MODE_HASH[tmp] + else: + pass + + return mode + +def split_log_mode(mode): + user = '' + other = '' + match = re.search('(.*?)::(.*)', mode) + if match: + user, other = match.groups() + else: + user = mode + other = mode + #print ('split_logmode:', user, mode) + return user, other + +def mode_contains(mode, subset): + # w implies a + if mode & AA_MAY_WRITE: + mode |= AA_MAY_APPEND + if mode & (AA_OTHER(AA_MAY_WRITE)): + mode |= (AA_OTHER(AA_MAY_APPEND)) + + return (mode & subset) == subset + +def contains(mode, string): + return mode_contains(mode, str_to_mode(string)) + +def validate_log_mode(mode): + if LOG_MODE_RE.search(mode): + #if LOG_MODE_RE.search(mode): + return True + else: + return False + +def hide_log_mode(mode): + mode = mode.replace('::', '') + return mode + +def map_log_mode(mode): + return mode + +def print_mode(mode): + user, other = split_mode(mode) + string = sub_mode_to_str(user) + '::' + sub_mode_to_str(other) + + return string + +def sub_mode_to_str(mode): + string = '' + # w(write) implies a(append) + if mode & AA_MAY_WRITE: + mode = mode - AA_MAY_APPEND + #string = ''.join(mode) + + if mode & AA_EXEC_MMAP: + string += 'm' + if mode & AA_MAY_READ: + string += 'r' + if mode & AA_MAY_WRITE: + string += 'w' + if mode & AA_MAY_APPEND: + string += 'a' + if mode & AA_MAY_LINK: + string += 'l' + if mode & AA_MAY_LOCK: + string += 'k' + + # modes P and C must appear before I and U else invalid syntax + if mode & (AA_EXEC_PROFILE | AA_EXEC_NT): + if mode & AA_EXEC_UNSAFE: + string += 'p' + else: + string += 'P' + + if mode & AA_EXEC_CHILD: + if mode & AA_EXEC_UNSAFE: + string += 'c' + else: + string += 'C' + + if mode & AA_EXEC_UNCONFINED: + if mode & AA_EXEC_UNSAFE: + string += 'u' + else: + string += 'U' + + if mode & AA_EXEC_INHERIT: + string += 'i' + + if mode & AA_MAY_EXEC: + string += 'x' + + return string + +def is_user_mode(mode): + user, other = split_mode(mode) + + if user and not other: + return True + else: + return False + +def profilemode(mode): + pass + +def split_mode(mode): + user = set() + for i in mode: + if not '::' in i: + user.add(i) + other = mode - user + other = AA_OTHER_REMOVE(other) + return user, other + +def mode_to_str(mode): + mode = flatten_mode(mode) + return sub_mode_to_str(mode) + +def flatten_mode(mode): + if not mode: + return set() + + user, other = split_mode(mode) + mode = user | other + mode |= (AA_OTHER(mode)) + + return mode + +def owner_flatten_mode(mode): + mode = flatten_mode(mode) + return mode + +def mode_to_str_user(mode): + user, other = split_mode(mode) + string = '' + + if not user: + user = set() + if not other: + other = set() + + if user - other: + if other: + string = sub_mode_to_str(other) + '+' + string += 'owner ' + sub_mode_to_str(user - other) + + elif is_user_mode(mode): + string = 'owner ' + sub_mode_to_str(user) + else: + string = sub_mode_to_str(flatten_mode(mode)) + + return string + +def log_str_to_mode(profile, string, nt_name): + mode = str_to_mode(string) + # If contains nx and nix + #print (profile, string, nt_name) + if contains(mode, 'Nx'): + # Transform to px, cx + match = re.search('(.+?)//(.+?)', nt_name) + if match: + lprofile, lhat = match.groups() + tmode = 0 + + if lprofile == profile: + if mode & AA_MAY_EXEC: + tmode = str_to_mode('Cx::') + if mode & AA_OTHER(AA_MAY_EXEC): + tmode |= str_to_mode('Cx') + nt_name = lhat + else: + if mode & AA_MAY_EXEC: + tmode = str_to_mode('Px::') + if mode & AA_OTHER(AA_MAY_EXEC): + tmode |= str_to_mode('Px') + nt_name = lhat + + mode = mode - str_to_mode('Nx') + mode |= tmode + + return mode, nt_name diff --git a/utils/apparmor/cleanprofile.py b/utils/apparmor/cleanprofile.py new file mode 100644 index 000000000..dad132f50 --- /dev/null +++ b/utils/apparmor/cleanprofile.py @@ -0,0 +1,153 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import re +import copy + +import apparmor + +class Prof(object): + def __init__(self, filename): + self.aa = apparmor.aa.aa + self.filelist = apparmor.aa.filelist + self.include = apparmor.aa.include + self.filename = filename + +class CleanProf(object): + def __init__(self, same_file, profile, other): + #If same_file we're basically comparing the file against itself to check superfluous rules + self.same_file = same_file + self.profile = profile + self.other = other + + def compare_profiles(self): + deleted = 0 + other_file_includes = list(self.other.filelist[self.other.filename]['include'].keys()) + + #Remove the duplicate file-level includes from other + for rule in self.profile.filelist[self.profile.filename]['include'].keys(): + if rule in other_file_includes: + self.other.filelist[self.other.filename]['include'].pop(rule) + + for profile in self.profile.aa.keys(): + deleted += self.remove_duplicate_rules(profile) + + return deleted + + def remove_duplicate_rules(self, program): + #Process the profile of the program + #Process every hat in the profile individually + file_includes = list(self.profile.filelist[self.profile.filename]['include'].keys()) + deleted = 0 + for hat in self.profile.aa[program].keys(): + #The combined list of includes from profile and the file + includes = list(self.profile.aa[program][hat]['include'].keys()) + file_includes + + #If different files remove duplicate includes in the other profile + if not self.same_file: + for inc in includes: + if self.other.aa[program][hat]['include'].get(inc, False): + self.other.aa[program][hat]['include'].pop(inc) + deleted += 1 + #Clean up superfluous rules from includes in the other profile + for inc in includes: + if not self.profile.include.get(inc, {}).get(inc, False): + apparmor.aa.load_include(inc) + deleted += apparmor.aa.delete_duplicates(self.other.aa[program][hat], inc) + + #Clean the duplicates of caps in other profile + deleted += delete_cap_duplicates(self.profile.aa[program][hat]['allow']['capability'], self.other.aa[program][hat]['allow']['capability'], self.same_file) + deleted += delete_cap_duplicates(self.profile.aa[program][hat]['deny']['capability'], self.other.aa[program][hat]['deny']['capability'], self.same_file) + + #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) + deleted += delete_path_duplicates(self.profile.aa[program][hat], self.other.aa[program][hat], 'deny', self.same_file) + + #Clean the duplicates of net rules in other profile + deleted += delete_net_duplicates(self.profile.aa[program][hat]['allow']['netdomain'], self.other.aa[program][hat]['allow']['netdomain'], self.same_file) + deleted += delete_net_duplicates(self.profile.aa[program][hat]['deny']['netdomain'], self.other.aa[program][hat]['deny']['netdomain'], self.same_file) + + return deleted + +def delete_path_duplicates(profile, profile_other, allow, same_profile=True): + deleted = [] + # Check if any individual rule makes any rule superfluous + for rule in profile[allow]['path'].keys(): + for entry in profile_other[allow]['path'].keys(): + if rule == entry: + # Check the modes + cm = profile[allow]['path'][rule]['mode'] + am = profile[allow]['path'][rule]['audit'] + # If modes of rule are a superset of rules implied by entry we can safely remove it + if apparmor.aa.mode_contains(cm, profile_other[allow]['path'][entry]['mode']) and apparmor.aa.mode_contains(am, profile_other[allow]['path'][entry]['audit']): + if not same_profile: + deleted.append(entry) + continue + if re.search('#?\s*include', rule) or re.search('#?\s*include', entry): + continue + # Check if the rule implies entry + if apparmor.aa.matchliteral(rule, entry): + # Check the modes + cm = profile[allow]['path'][rule]['mode'] + am = profile[allow]['path'][rule]['audit'] + # If modes of rule are a superset of rules implied by entry we can safely remove it + if apparmor.aa.mode_contains(cm, profile_other[allow]['path'][entry]['mode']) and apparmor.aa.mode_contains(am, profile_other[allow]['path'][entry]['audit']): + deleted.append(entry) + + for entry in deleted: + profile_other[allow]['path'].pop(entry) + + return len(deleted) + +def delete_cap_duplicates(profilecaps, profilecaps_other, same_profile=True): + deleted = [] + if profilecaps and profilecaps_other and not same_profile: + for capname in profilecaps.keys(): + if profilecaps_other[capname].get('set', False): + deleted.append(capname) + for capname in deleted: + profilecaps_other.pop(capname) + + return len(deleted) + +def delete_net_duplicates(netrules, netrules_other, same_profile=True): + deleted = 0 + hasher_obj = apparmor.aa.hasher() + copy_netrules_other = copy.deepcopy(netrules_other) + if netrules_other and netrules: + netglob = False + # Delete matching rules + if netrules.get('all', False): + netglob = True + # Iterate over a copy of the rules in the other profile + for fam in copy_netrules_other['rule'].keys(): + if netglob or (type(netrules['rule'][fam]) != type(hasher_obj) and netrules['rule'][fam]): + if not same_profile: + if type(netrules_other['rule'][fam]) == type(hasher_obj): + deleted += len(netrules_other['rule'][fam].keys()) + else: + deleted += 1 + netrules_other['rule'].pop(fam) + elif type(netrules_other['rule'][fam]) != type(hasher_obj) and netrules_other['rule'][fam]: + if type(netrules['rule'][fam]) != type(hasher_obj) and netrules['rule'][fam]: + if not same_profile: + netrules_other['rule'].pop(fam) + deleted += 1 + else: + for sock_type in netrules_other['rule'][fam].keys(): + if netrules['rule'].get(fam, False): + if netrules['rule'][fam].get(sock_type, False): + if not same_profile: + netrules_other['rule'][fam].pop(sock_type) + deleted += 1 + return deleted diff --git a/utils/apparmor/common.py b/utils/apparmor/common.py index 983690d0a..0037233a1 100644 --- a/utils/apparmor/common.py +++ b/utils/apparmor/common.py @@ -1,6 +1,7 @@ # ------------------------------------------------------------------ # # Copyright (C) 2012 Canonical Ltd. +# 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 @@ -9,11 +10,20 @@ # ------------------------------------------------------------------ from __future__ import print_function +import codecs +import collections +import glob +import logging +import os +import re import subprocess import sys +import termios +import tty DEBUGGING = False + # # Utility classes # @@ -93,3 +103,154 @@ def cmd_pipe(command1, command2): return [sp2.returncode, out] +def valid_path(path): + '''Valid path''' + # No relative paths + m = "Invalid path: %s" % (path) + if not path.startswith('/'): + debug("%s (relative)" % (m)) + return False + + if '"' in path: # We double quote elsewhere + return False + + try: + os.path.normpath(path) + except Exception: + debug("%s (could not normalize)" % (m)) + return False + return True + +def get_directory_contents(path): + '''Find contents of the given directory''' + if not valid_path(path): + return None + + files = [] + for f in glob.glob(path + "/*"): + files.append(f) + + files.sort() + return files + +def open_file_read(path, encoding='UTF-8'): + '''Open specified file read-only''' + try: + orig = codecs.open(path, 'r', encoding) + except Exception: + raise + + return orig + +def open_file_write(path): + '''Open specified file in write/overwrite mode''' + try: + orig = codecs.open(path, 'w', 'UTF-8') + except Exception: + raise + return orig + +def readkey(): + '''Returns the pressed key''' + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + + return ch + +def hasher(): + '''A neat alternative to perl's hash reference''' + # Creates a dictionary for any depth and returns empty dictionary otherwise + return collections.defaultdict(hasher) + +def convert_regexp(regexp): + regex_paren = re.compile('^(.*){([^}]*)}(.*)$') + regexp = regexp.strip() + new_reg = re.sub(r'(? +# +# 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. +# +# ---------------------------------------------------------------------- +from __future__ import with_statement +import os +import shlex +import shutil +import stat +import sys +import tempfile +if sys.version_info < (3, 0): + import ConfigParser as configparser + + # Class to provide the object[section][option] behavior in Python2 + class configparser_py2(configparser.ConfigParser): + def __getitem__(self, section): + section_val = self.items(section) + section_options = dict() + for option, value in section_val: + section_options[option] = value + return section_options + + +else: + import configparser + + +from apparmor.common import AppArmorException, open_file_read # , warn, msg, + + +# CFG = None +# REPO_CFG = None +# SHELL_FILES = ['easyprof.conf', 'notify.conf', 'parser.conf', 'subdomain.conf'] +class Config(object): + def __init__(self, conf_type, conf_dir='/etc/apparmor'): + self.CONF_DIR = conf_dir + # The type of config file that'll be read and/or written + if conf_type == 'shell' or conf_type == 'ini': + self.conf_type = conf_type + self.input_file = None + else: + raise AppArmorException("Unknown configuration file type") + + def new_config(self): + if self.conf_type == 'shell': + config = {'': dict()} + elif self.conf_type == 'ini': + config = configparser.ConfigParser() + return config + + def read_config(self, filename): + """Reads the file and returns a config[section][attribute]=property object""" + # LP: Bug #692406 + # Explicitly disabled repository + filepath = self.CONF_DIR + '/' + filename + self.input_file = filepath + if filename == "repository.conf": + config = dict() + config['repository'] = {'enabled': 'no'} + elif self.conf_type == 'shell': + config = self.read_shell(filepath) + elif self.conf_type == 'ini': + if sys.version_info > (3, 0): + config = configparser.ConfigParser() + else: + config = configparser_py2() + # Set the option form to string -prevents forced conversion to lowercase + config.optionxform = str + if sys.version_info > (3, 0): + config.read(filepath) + else: + try: + config.read(filepath) + except configparser.ParsingError: + tmp_filepath = py2_parser(filepath) + config.read(tmp_filepath.name) + ##config.__get__() + return config + + def write_config(self, filename, config): + """Writes the given config to the specified file""" + filepath = self.CONF_DIR + '/' + filename + permission_600 = stat.S_IRUSR | stat.S_IWUSR # Owner read and write + try: + # Open a temporary file in the CONF_DIR to write the config file + config_file = tempfile.NamedTemporaryFile('w', prefix='aa_temp', delete=False, dir=self.CONF_DIR) + if os.path.exists(self.input_file): + # Copy permissions from an existing file to temporary file + shutil.copymode(self.input_file, config_file.name) + else: + # If no existing permission set the file permissions as 0600 + os.chmod(config_file.name, permission_600) + if self.conf_type == 'shell': + self.write_shell(filepath, config_file, config) + elif self.conf_type == 'ini': + self.write_configparser(filepath, config_file, config) + config_file.close() + except IOError: + raise AppArmorException("Unable to write to %s" % filename) + else: + # Replace the target config file with the temporary file + os.rename(config_file.name, filepath) + + 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 + return filename + + def find_first_dir(self, dir_list): + """Returns name of first matching directory None otherwise""" + dirname = None + if dir_list: + for direc in dir_list.split(): + if os.path.isdir(direc): + dirname = direc + break + return dirname + + def read_shell(self, filepath): + """Reads the shell type conf files and returns config[''][option]=value""" + config = {'': dict()} + with open_file_read(filepath) as conf_file: + for line in conf_file: + result = shlex.split(line, True) + # If not a comment of empty line + if result: + # option="value" or option=value type + if '=' in result[0]: + option, value = result[0].split('=') + # option type + else: + option = result[0] + value = None + config[''][option] = value + return config + + def write_shell(self, filepath, f_out, config): + """Writes the config object in shell file format""" + # All the options in the file + options = [key for key in config[''].keys()] + # If a previous file exists modify it keeping the comments + if os.path.exists(self.input_file): + with open_file_read(self.input_file) as f_in: + for line in f_in: + result = shlex.split(line, True) + # If line is not empty or comment + if result: + # If option=value or option="value" type + if '=' in result[0]: + option, value = result[0].split('=') + if '#' in line: + comment = value.split('#', 1)[1] + comment = '#' + comment + else: + comment = '' + # If option exists in the new config file + if option in options: + # If value is different + if value != config[''][option]: + value_new = config[''][option] + if value_new is not None: + # Update value + if '"' in line: + value_new = '"' + value_new + '"' + line = option + '=' + value_new + comment + '\n' + else: + # If option changed to option type from option=value type + line = option + comment + '\n' + f_out.write(line) + # Remove from remaining options list + options.remove(option) + else: + # If option type + option = result[0] + value = None + # If option exists in the new config file + if option in options: + # If its no longer option type + if config[''][option] is not None: + value = config[''][option] + line = option + '=' + value + '\n' + f_out.write(line) + # Remove from remaining options list + options.remove(option) + else: + # If its empty or comment copy as it is + f_out.write(line) + # If any new options are present + if options: + for option in options: + value = config[''][option] + # option type entry + if value is None: + line = option + '\n' + # option=value type entry + else: + line = option + '=' + value + '\n' + f_out.write(line) + + def write_configparser(self, filepath, f_out, config): + """Writes/updates the given file with given config object""" + # All the sections in the file + sections = config.sections() + write = True + section = None + options = [] + # If a previous file exists modify it keeping the comments + if os.path.exists(self.input_file): + with open_file_read(self.input_file) as f_in: + for line in f_in: + # If its a section + if line.lstrip().startswith('['): + # If any options from preceding section remain write them + if options: + for option in options: + line_new = ' ' + option + ' = ' + config[section][option] + '\n' + f_out.write(line_new) + options = [] + if section in sections: + # Remove the written section from the list + sections.remove(section) + section = line.strip()[1:-1] + if section in sections: + # enable write for all entries in that section + write = True + options = config.options(section) + # write the section + f_out.write(line) + else: + # disable writing until next valid section + write = False + # If write enabled + elif write: + value = shlex.split(line, True) + # If the line is empty or a comment + if not value: + f_out.write(line) + else: + option, value = line.split('=', 1) + try: + # split any inline comments + value, comment = value.split('#', 1) + comment = '#' + comment + except ValueError: + comment = '' + if option.strip() in options: + if config[section][option.strip()] != value.strip(): + value = value.replace(value, config[section][option.strip()]) + line = option + '=' + value + comment + f_out.write(line) + options.remove(option.strip()) + # If any options remain from the preceding section + if options: + for option in options: + line = ' ' + option + ' = ' + config[section][option] + '\n' + f_out.write(line) + options = [] + # If any new sections are present + if section in sections: + sections.remove(section) + for section in sections: + f_out.write('\n[%s]\n' % section) + options = config.options(section) + for option in options: + line = ' ' + option + ' = ' + config[section][option] + '\n' + f_out.write(line) + +def py2_parser(filename): + """Returns the de-dented ini file from the new format ini""" + tmp = tempfile.NamedTemporaryFile('rw') + f_out = open(tmp.name, 'w') + if os.path.exists(filename): + with open_file_read(filename) as f_in: + for line in f_in: + # The ini format allows for multi-line entries, with the subsequent + # entries being indented deeper hence simple lstrip() is not appropriate + if line[:2] == ' ': + line = line[2:] + elif line[0] == '\t': + line = line[1:] + f_out.write(line) + f_out.flush() + return tmp diff --git a/utils/apparmor/easyprof.py b/utils/apparmor/easyprof.py index 035edf502..38b9bb0d5 100644 --- a/utils/apparmor/easyprof.py +++ b/utils/apparmor/easyprof.py @@ -1,6 +1,6 @@ # ------------------------------------------------------------------ # -# Copyright (C) 2011-2012 Canonical Ltd. +# 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 @@ -11,10 +11,13 @@ from __future__ import with_statement import codecs +import copy import glob +import json import optparse import os import re +import shutil import subprocess import sys import tempfile @@ -123,29 +126,117 @@ def valid_binary_path(path): return True -def valid_variable_name(var): +def valid_variable(v): '''Validate variable name''' - if re.search(r'[a-zA-Z0-9_]+$', var): + debug("Checking '%s'" % v) + try: + (key, value) = v.split('=') + except Exception: + return False + + if not re.search(r'^@\{[a-zA-Z0-9_]+\}$', key): + return False + + if '/' in value: + rel_ok = False + if not value.startswith('/'): + rel_ok = True + if not valid_path(value, relative_ok=rel_ok): + return False + + if '"' in value: + return False + + # If we made it here, we are safe + return True + + +def valid_path(path, relative_ok=False): + '''Valid path''' + m = "Invalid path: %s" % (path) + if not relative_ok and not path.startswith('/'): + debug("%s (relative)" % (m)) + return False + + if '"' in path: # We double quote elsewhere + debug("%s (quote)" % (m)) + return False + + if '../' in path: + debug("%s (../ path escape)" % (m)) + return False + + try: + p = os.path.normpath(path) + except Exception: + debug("%s (could not normalize)" % (m)) + return False + + if p != path: + debug("%s (normalized path != path (%s != %s))" % (m, p, path)) + return False + + # If we made it here, we are safe + return True + + +def _is_safe(s): + '''Known safe regex''' + if re.search(r'^[a-zA-Z_0-9\-\.]+$', s): return True return False -def valid_path(path): - '''Valid path''' - # No relative paths - m = "Invalid path: %s" % (path) - if not path.startswith('/'): - debug("%s (relative)" % (m)) - return False +def valid_policy_vendor(s): + '''Verify the policy vendor''' + return _is_safe(s) + +def valid_policy_version(v): + '''Verify the policy version''' try: - os.path.normpath(path) - except Exception: - debug("%s (could not normalize)" % (m)) + float(v) + except ValueError: + return False + if float(v) < 0: return False return True +def valid_template_name(s, strict=False): + '''Verify the template name''' + if not strict and s.startswith('/'): + if not valid_path(s): + return False + return True + return _is_safe(s) + + +def valid_abstraction_name(s): + '''Verify the template name''' + return _is_safe(s) + + +def valid_profile_name(s): + '''Verify the profile name''' + # profile name specifies path + if s.startswith('/'): + if not valid_path(s): + return False + return True + + # profile name does not specify path + # alpha-numeric and Debian version, plus '_' + if re.search(r'^[a-zA-Z0-9][a-zA-Z0-9_\+\-\.:~]+$', s): + return True + return False + + +def valid_policy_group_name(s): + '''Verify policy group name''' + return _is_safe(s) + + def get_directory_contents(path): '''Find contents of the given directory''' if not valid_path(path): @@ -202,6 +293,7 @@ def verify_policy(policy): class AppArmorEasyProfile: '''Easy profile class''' def __init__(self, binary, opt): + verify_options(opt) opt.ensure_value("conffile", "/etc/apparmor/easyprof.conf") self.conffile = os.path.abspath(opt.conffile) @@ -222,6 +314,25 @@ class AppArmorEasyProfile: if opt.policy_groups_dir and os.path.isdir(opt.policy_groups_dir): self.dirs['policygroups'] = os.path.abspath(opt.policy_groups_dir) + + self.policy_version = None + self.policy_vendor = None + if (opt.policy_version and not opt.policy_vendor) or \ + (opt.policy_vendor and not opt.policy_version): + raise AppArmorException("Must specify both policy version and vendor") + if opt.policy_version and opt.policy_vendor: + self.policy_vendor = opt.policy_vendor + self.policy_version = str(opt.policy_version) + + for i in ['templates', 'policygroups']: + d = os.path.join(self.dirs[i], \ + self.policy_vendor, \ + self.policy_version) + if not os.path.isdir(d): + raise AppArmorException( + "Could not find %s directory '%s'" % (i, d)) + self.dirs[i] = d + if not 'templates' in self.dirs: raise AppArmorException("Could not find templates directory") if not 'policygroups' in self.dirs: @@ -230,19 +341,29 @@ class AppArmorEasyProfile: self.aa_topdir = "/etc/apparmor.d" self.binary = binary - if binary != None: + if binary: if not valid_binary_path(binary): raise AppArmorException("Invalid path for binary: '%s'" % binary) - self.set_template(opt.template) + if opt.manifest: + self.set_template(opt.template, allow_abs_path=False) + else: + self.set_template(opt.template) + self.set_policygroup(opt.policy_groups) if opt.name: self.set_name(opt.name) elif self.binary != None: self.set_name(self.binary) - self.templates = get_directory_contents(self.dirs['templates']) - self.policy_groups = get_directory_contents(self.dirs['policygroups']) + self.templates = [] + for f in get_directory_contents(self.dirs['templates']): + if os.path.isfile(f): + self.templates.append(f) + self.policy_groups = [] + for f in get_directory_contents(self.dirs['policygroups']): + if os.path.isfile(f): + self.policy_groups.append(f) def _get_defaults(self): '''Read in defaults from configuration''' @@ -282,11 +403,18 @@ class AppArmorEasyProfile: '''Get contents of current template''' return open(self.template).read() - def set_template(self, template): + def set_template(self, template, allow_abs_path=True): '''Set current template''' - self.template = template - if not template.startswith('/'): + if "../" in template: + raise AppArmorException('template "%s" contains "../" escape path' % (template)) + elif template.startswith('/') and not allow_abs_path: + raise AppArmorException("Cannot use an absolute path template '%s'" % template) + + if template.startswith('/'): + self.template = template + else: self.template = os.path.join(self.dirs['templates'], template) + if not os.path.exists(self.template): raise AppArmorException('%s does not exist' % (self.template)) @@ -327,9 +455,11 @@ class AppArmorEasyProfile: def gen_variable_declaration(self, dec): '''Generate a variable declaration''' - if not re.search(r'^@\{[a-zA-Z_]+\}=.+', dec): + if not valid_variable(dec): raise AppArmorException("Invalid variable declaration '%s'" % dec) - return dec + # Make sure we always quote + k, v = dec.split('=') + return '%s="%s"' % (k, v) def gen_path_rule(self, path, access): rule = [] @@ -352,7 +482,18 @@ class AppArmorEasyProfile: return rule - def gen_policy(self, name, binary, template_var=[], abstractions=None, policy_groups=None, read_path=[], write_path=[], author=None, comment=None, copyright=None): + def gen_policy(self, name, + binary=None, + profile_name=None, + template_var=[], + abstractions=None, + policy_groups=None, + read_path=[], + write_path=[], + author=None, + comment=None, + copyright=None, + no_verify=False): def find_prefix(t, s): '''Calculate whitespace prefix based on occurrence of s in t''' pat = re.compile(r'^ *%s' % s) @@ -375,12 +516,22 @@ class AppArmorEasyProfile: tmp += line + "\n" policy = tmp - # Fill-in profile name and binary - policy = re.sub(r'###NAME###', name, policy) - if binary.startswith('/'): - policy = re.sub(r'###BINARY###', binary, policy) + attachment = "" + if binary: + if not valid_binary_path(binary): + raise AppArmorException("Invalid path for binary: '%s'" % \ + binary) + if profile_name: + attachment = 'profile "%s" "%s"' % (profile_name, binary) + else: + attachment = '"%s"' % binary + elif profile_name: + attachment = 'profile "%s"' % profile_name else: - policy = re.sub(r'###BINARY###', "profile %s" % binary, policy) + raise AppArmorException("Must specify binary and/or profile name") + policy = re.sub(r'###PROFILEATTACH###', attachment, policy) + + policy = re.sub(r'###NAME###', name, policy) # Fill-in various comment fields if comment != None: @@ -398,7 +549,9 @@ class AppArmorEasyProfile: s = "%s# No abstractions specified" % prefix if abstractions != None: s = "%s# Specified abstractions" % (prefix) - for i in abstractions.split(','): + t = abstractions.split(',') + t.sort() + for i in t: s += "\n%s%s" % (prefix, self.gen_abstraction_rule(i)) policy = re.sub(r' *%s' % search, s, policy) @@ -407,7 +560,9 @@ class AppArmorEasyProfile: s = "%s# No policy groups specified" % prefix if policy_groups != None: s = "%s# Rules specified via policy groups" % (prefix) - for i in policy_groups.split(','): + t = policy_groups.split(',') + t.sort() + for i in t: for line in self.get_policygroup(i).splitlines(): s += "\n%s%s" % (prefix, line) if i != policy_groups.split(',')[-1]: @@ -419,6 +574,7 @@ class AppArmorEasyProfile: s = "%s# No template variables specified" % prefix if len(template_var) > 0: s = "%s# Specified profile variables" % (prefix) + template_var.sort() for i in template_var: s += "\n%s%s" % (prefix, self.gen_variable_declaration(i)) policy = re.sub(r' *%s' % search, s, policy) @@ -428,8 +584,9 @@ class AppArmorEasyProfile: s = "%s# No read paths specified" % prefix if len(read_path) > 0: s = "%s# Specified read permissions" % (prefix) + read_path.sort() for i in read_path: - for r in self.gen_path_rule(i, 'r'): + for r in self.gen_path_rule(i, 'rk'): s += "\n%s%s" % (prefix, r) policy = re.sub(r' *%s' % search, s, policy) @@ -438,17 +595,110 @@ class AppArmorEasyProfile: s = "%s# No write paths specified" % prefix if len(write_path) > 0: s = "%s# Specified write permissions" % (prefix) + write_path.sort() for i in write_path: for r in self.gen_path_rule(i, 'rwk'): s += "\n%s%s" % (prefix, r) policy = re.sub(r' *%s' % search, s, policy) - if not verify_policy(policy): - debug("\n" + policy) + if no_verify: + debug("Skipping policy verification") + elif not verify_policy(policy): + msg("\n" + policy) raise AppArmorException("Invalid policy") return policy + def output_policy(self, params, count=0, dir=None): + '''Output policy''' + policy = self.gen_policy(**params) + if not dir: + if count: + sys.stdout.write('### aa-easyprof profile #%d ###\n' % count) + sys.stdout.write('%s\n' % policy) + else: + out_fn = "" + if 'profile_name' in params: + out_fn = params['profile_name'] + elif 'binary' in params: + out_fn = params['binary'] + else: # should not ever reach this + raise AppArmorException("Could not determine output filename") + + # Generate an absolute path, convertng any path delimiters to '.' + out_fn = os.path.join(dir, re.sub(r'/', '.', out_fn.lstrip('/'))) + if os.path.exists(out_fn): + raise AppArmorException("'%s' already exists" % out_fn) + + if not os.path.exists(dir): + os.mkdir(dir) + + if not os.path.isdir(dir): + raise AppArmorException("'%s' is not a directory" % dir) + + f, fn = tempfile.mkstemp(prefix='aa-easyprof') + if not isinstance(policy, bytes): + policy = policy.encode('utf-8') + os.write(f, policy) + os.close(f) + + shutil.move(fn, out_fn) + + def gen_manifest(self, params): + '''Take params list and output a JSON file''' + d = dict() + d['security'] = dict() + d['security']['profiles'] = dict() + + pkey = "" + if 'profile_name' in params: + pkey = params['profile_name'] + elif 'binary' in params: + # when profile_name is not specified, the binary (path attachment) + # also functions as the profile name + pkey = params['binary'] + else: + raise AppArmorException("Must supply binary or profile name") + + d['security']['profiles'][pkey] = dict() + + # Add the template since it isn't part of 'params' + template = os.path.basename(self.template) + if template != 'default': + d['security']['profiles'][pkey]['template'] = template + + # Add the policy_version since it isn't part of 'params' + if self.policy_version: + d['security']['profiles'][pkey]['policy_version'] = float(self.policy_version) + if self.policy_vendor: + d['security']['profiles'][pkey]['policy_vendor'] = self.policy_vendor + + for key in params: + if key == 'profile_name' or \ + (key == 'binary' and not 'profile_name' in params): + continue # don't re-add the pkey + elif key == 'binary' and not params[key]: + continue # binary can by None when specifying --profile-name + elif key == 'template_var': + d['security']['profiles'][pkey]['template_variables'] = dict() + for tvar in params[key]: + if not self.gen_variable_declaration(tvar): + raise AppArmorException("Malformed template_var '%s'" % tvar) + (k, v) = tvar.split('=') + k = k.lstrip('@').lstrip('{').rstrip('}') + d['security']['profiles'][pkey]['template_variables'][k] = v + elif key == 'abstractions' or key == 'policy_groups': + d['security']['profiles'][pkey][key] = params[key].split(",") + d['security']['profiles'][pkey][key].sort() + else: + d['security']['profiles'][pkey][key] = params[key] + json_str = json.dumps(d, + sort_keys=True, + indent=2, + separators=(',', ': ') + ) + return json_str + def print_basefilenames(files): for i in files: sys.stdout.write("%s\n" % (os.path.basename(i))) @@ -458,22 +708,65 @@ def print_files(files): with open(i) as f: sys.stdout.write(f.read()+"\n") +def check_manifest_conflict_args(option, opt_str, value, parser): + '''Check for -m/--manifest with conflicting args''' + conflict_args = ['abstractions', + 'read_path', + 'write_path', + # template always get set to 'default', can't conflict + # 'template', + 'policy_groups', + 'policy_version', + 'policy_vendor', + 'name', + 'profile_name', + 'comment', + 'copyright', + 'author', + 'template_var'] + for conflict in conflict_args: + if getattr(parser.values, conflict, False): + raise optparse.OptionValueError("can't use --%s with --manifest " \ + "argument" % conflict) + setattr(parser.values, option.dest, value) + +def check_for_manifest_arg(option, opt_str, value, parser): + '''Check for -m/--manifest with conflicting args''' + if parser.values.manifest: + raise optparse.OptionValueError("can't use --%s with --manifest " \ + "argument" % opt_str.lstrip('-')) + setattr(parser.values, option.dest, value) + +def check_for_manifest_arg_append(option, opt_str, value, parser): + '''Check for -m/--manifest with conflicting args (with append)''' + if parser.values.manifest: + raise optparse.OptionValueError("can't use --%s with --manifest " \ + "argument" % opt_str.lstrip('-')) + parser.values.ensure_value(option.dest, []).append(value) + def add_parser_policy_args(parser): '''Add parser arguments''' parser.add_option("-a", "--abstractions", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="abstractions", help="Comma-separated list of abstractions", metavar="ABSTRACTIONS") parser.add_option("--read-path", + action="callback", + callback=check_for_manifest_arg_append, + type=str, dest="read_path", help="Path allowing owner reads", - metavar="PATH", - action="append") + metavar="PATH") parser.add_option("--write-path", + action="callback", + callback=check_for_manifest_arg_append, + type=str, dest="write_path", help="Path allowing owner writes", - metavar="PATH", - action="append") + metavar="PATH") parser.add_option("-t", "--template", dest="template", help="Use non-default policy template", @@ -484,12 +777,36 @@ def add_parser_policy_args(parser): help="Use non-default templates directory", metavar="DIR") parser.add_option("-p", "--policy-groups", + action="callback", + callback=check_for_manifest_arg, + type=str, help="Comma-separated list of policy groups", metavar="POLICYGROUPS") parser.add_option("--policy-groups-dir", dest="policy_groups_dir", help="Use non-default policy-groups directory", metavar="DIR") + parser.add_option("--policy-version", + action="callback", + callback=check_for_manifest_arg, + type=str, + dest="policy_version", + help="Specify version for templates and policy groups", + metavar="VERSION") + parser.add_option("--policy-vendor", + action="callback", + callback=check_for_manifest_arg, + type=str, + dest="policy_vendor", + help="Specify vendor for templates and policy groups", + metavar="VENDOR") + parser.add_option("--profile-name", + action="callback", + callback=check_for_manifest_arg, + type=str, + dest="profile_name", + help="AppArmor profile name", + metavar="PROFILENAME") def parse_args(args=None, parser=None): '''Parse arguments''' @@ -506,6 +823,10 @@ def parse_args(args=None, parser=None): help="Show debugging output", action='store_true', default=False) + parser.add_option("--no-verify", + help="Don't verify policy using 'apparmor_parser -p'", + action='store_true', + default=False) parser.add_option("--list-templates", help="List available templates", action='store_true', @@ -523,31 +844,72 @@ def parse_args(args=None, parser=None): action='store_true', default=False) parser.add_option("-n", "--name", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="name", - help="Name of policy", - metavar="NAME") + help="Name of policy (not AppArmor profile name)", + metavar="COMMENT") parser.add_option("--comment", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="comment", help="Comment for policy", metavar="COMMENT") parser.add_option("--author", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="author", help="Author of policy", metavar="COMMENT") parser.add_option("--copyright", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="copyright", help="Copyright for policy", metavar="COMMENT") parser.add_option("--template-var", + action="callback", + callback=check_for_manifest_arg_append, + type=str, dest="template_var", help="Declare AppArmor variable", - metavar="@{VARIABLE}=VALUE", - action="append") + metavar="@{VARIABLE}=VALUE") + parser.add_option("--output-format", + action="store", + dest="output_format", + help="Specify output format as text (default) or json", + metavar="FORMAT", + default="text") + parser.add_option("--output-directory", + action="store", + dest="output_directory", + help="Output policy to this directory", + metavar="DIR") + # This option conflicts with any of the value arguments, e.g. name, + # author, template-var, etc. + parser.add_option("-m", "--manifest", + action="callback", + callback=check_manifest_conflict_args, + type=str, + dest="manifest", + help="JSON manifest file", + metavar="FILE") + parser.add_option("--verify-manifest", + action="store_true", + default=False, + dest="verify_manifest", + help="Verify JSON manifest file") + # add policy args now add_parser_policy_args(parser) (my_opt, my_args) = parser.parse_args(args) + if my_opt.debug: DEBUGGING = True return (my_opt, my_args) @@ -555,10 +917,21 @@ def parse_args(args=None, parser=None): def gen_policy_params(binary, opt): '''Generate parameters for gen_policy''' params = dict(binary=binary) + + if not binary and not opt.profile_name: + raise AppArmorException("Must specify binary and/or profile name") + + if opt.profile_name: + params['profile_name'] = opt.profile_name + if opt.name: params['name'] = opt.name else: - params['name'] = os.path.basename(binary) + if opt.profile_name: + params['name'] = opt.profile_name + elif binary: + params['name'] = os.path.basename(binary) + if opt.template_var: # What about specified multiple times? params['template_var'] = opt.template_var if opt.abstractions: @@ -569,14 +942,183 @@ def gen_policy_params(binary, opt): params['read_path'] = opt.read_path if opt.write_path: params['write_path'] = opt.write_path - if opt.abstractions: - params['abstractions'] = opt.abstractions if opt.comment: params['comment'] = opt.comment if opt.author: params['author'] = opt.author if opt.copyright: params['copyright'] = opt.copyright + if opt.policy_version and opt.output_format == "json": + params['policy_version'] = opt.policy_version + if opt.policy_vendor and opt.output_format == "json": + params['policy_vendor'] = opt.policy_vendor return params +def parse_manifest(manifest, opt_orig): + '''Take a JSON manifest as a string and updates options, returning an + updated binary. Note that a JSON file may contain multiple profiles.''' + + try: + m = json.loads(manifest) + except ValueError: + raise AppArmorException("Could not parse manifest") + + if 'security' in m: + top_table = m['security'] + else: + top_table = m + + if 'profiles' not in top_table: + raise AppArmorException("Could not parse manifest (could not find 'profiles')") + table = top_table['profiles'] + + # generally mirrors what is settable in gen_policy_params() + valid_keys = ['abstractions', + 'author', + 'binary', + 'comment', + 'copyright', + 'name', + 'policy_groups', + 'policy_version', + 'policy_vendor', + 'profile_name', + 'read_path', + 'template', + 'template_variables', + 'write_path', + ] + + profiles = [] + + for profile_name in table: + if not isinstance(table[profile_name], dict): + raise AppArmorException("Wrong JSON structure") + opt = copy.deepcopy(opt_orig) + + # The JSON structure is: + # { + # "security": { + # : { + # "binary": ... + # ... + # but because binary can be the profile name, we need to handle + # 'profile_name' and 'binary' special. If a profile_name starts with + # '/', then it is considered the binary. Otherwise, set the + # profile_name and set the binary if it is in the JSON. + binary = None + if profile_name.startswith('/'): + if 'binary' in table[profile_name]: + raise AppArmorException("Profile name should not specify path with binary") + binary = profile_name + else: + setattr(opt, 'profile_name', profile_name) + if 'binary' in table[profile_name]: + binary = table[profile_name]['binary'] + setattr(opt, 'binary', binary) + + for key in table[profile_name]: + if key not in valid_keys: + raise AppArmorException("Invalid key '%s'" % key) + + if key == 'binary': + continue # handled above + elif key == 'abstractions' or key == 'policy_groups': + setattr(opt, key, ",".join(table[profile_name][key])) + elif key == "template_variables": + t = table[profile_name]['template_variables'] + vlist = [] + for v in t.keys(): + vlist.append("@{%s}=%s" % (v, t[v])) + setattr(opt, 'template_var', vlist) + else: + if hasattr(opt, key): + setattr(opt, key, table[profile_name][key]) + + profiles.append( (binary, opt) ) + + return profiles + + +def verify_options(opt, strict=False): + '''Make sure our options are valid''' + if hasattr(opt, 'binary') and opt.binary and not valid_path(opt.binary): + raise AppArmorException("Invalid binary '%s'" % opt.binary) + if hasattr(opt, 'profile_name') and opt.profile_name != None and \ + not valid_profile_name(opt.profile_name): + raise AppArmorException("Invalid profile name '%s'" % opt.profile_name) + if hasattr(opt, 'binary') and opt.binary and \ + hasattr(opt, 'profile_name') and opt.profile_name != None and \ + opt.profile_name.startswith('/'): + raise AppArmorException("Profile name should not specify path with binary") + if hasattr(opt, 'policy_vendor') and opt.policy_vendor and \ + not valid_policy_vendor(opt.policy_vendor): + raise AppArmorException("Invalid policy vendor '%s'" % \ + opt.policy_vendor) + if hasattr(opt, 'policy_version') and opt.policy_version and \ + not valid_policy_version(opt.policy_version): + raise AppArmorException("Invalid policy version '%s'" % \ + opt.policy_version) + if hasattr(opt, 'template') and opt.template and \ + not valid_template_name(opt.template, strict): + raise AppArmorException("Invalid template '%s'" % opt.template) + if hasattr(opt, 'template_var') and opt.template_var: + for i in opt.template_var: + if not valid_variable(i): + raise AppArmorException("Invalid variable '%s'" % i) + if hasattr(opt, 'policy_groups') and opt.policy_groups: + for i in opt.policy_groups.split(','): + if not valid_policy_group_name(i): + raise AppArmorException("Invalid policy group '%s'" % i) + if hasattr(opt, 'abstractions') and opt.abstractions: + for i in opt.abstractions.split(','): + if not valid_abstraction_name(i): + raise AppArmorException("Invalid abstraction '%s'" % i) + if hasattr(opt, 'read_paths') and opt.read_paths: + for i in opt.read_paths: + if not valid_path(i): + raise AppArmorException("Invalid read path '%s'" % i) + if hasattr(opt, 'write_paths') and opt.write_paths: + for i in opt.write_paths: + if not valid_path(i): + raise AppArmorException("Invalid write path '%s'" % i) + + +def verify_manifest(params): + '''Verify manifest for safe and unsafe options''' + err_str = "" + (opt, args) = parse_args() + fake_easyp = AppArmorEasyProfile(None, opt) + + unsafe_keys = ['read_path', 'write_path'] + safe_abstractions = ['base'] + for k in params: + debug("Examining %s=%s" % (k, params[k])) + if k in unsafe_keys: + err_str += "\nfound %s key" % k + elif k == 'profile_name': + if params['profile_name'].startswith('/') or \ + '*' in params['profile_name']: + err_str += "\nprofile_name '%s'" % params['profile_name'] + elif k == 'abstractions': + for a in params['abstractions'].split(','): + if not a in safe_abstractions: + err_str += "\nfound '%s' abstraction" % a + elif k == "template_var": + pat = re.compile(r'[*/\{\}\[\]]') + for tv in params['template_var']: + if not fake_easyp.gen_variable_declaration(tv): + err_str += "\n%s" % tv + continue + tv_val = tv.split('=')[1] + debug("Examining %s" % tv_val) + if '..' in tv_val or pat.search(tv_val): + err_str += "\n%s" % tv + + if err_str: + warn("Manifest definition is potentially unsafe%s" % err_str) + return False + + return True + diff --git a/utils/apparmor/logparser.py b/utils/apparmor/logparser.py new file mode 100644 index 000000000..4e84f5dde --- /dev/null +++ b/utils/apparmor/logparser.py @@ -0,0 +1,395 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import os +import re +import sys +import time +import LibAppArmor +from apparmor.common import AppArmorException, open_file_read, DebugLogger + +from apparmor.aamode import validate_log_mode, log_str_to_mode, hide_log_mode, AA_MAY_EXEC + +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() + +class ReadLog: + RE_LOG_v2_6_syslog = re.compile('kernel:\s+(\[[\d\.\s]+\]\s+)?type=\d+\s+audit\([\d\.\:]+\):\s+apparmor=') + RE_LOG_v2_6_audit = re.compile('type=AVC\s+(msg=)?audit\([\d\.\:]+\):\s+apparmor=') + MODE_MAP_RE = re.compile('r|w|l|m|k|a|x|i|u|p|c|n|I|U|P|C|N') + LOG_MODE_RE = re.compile('r|w|l|m|k|a|x|ix|ux|px|cx|nx|pix|cix|Ix|Ux|Px|PUx|Cx|Nx|Pix|Cix') + PROFILE_MODE_RE = re.compile('r|w|l|m|k|a|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix') + PROFILE_MODE_NT_RE = re.compile('r|w|l|m|k|a|x|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix') + PROFILE_MODE_DENY_RE = re.compile('r|w|l|m|k|a|x') + # Used by netdomain to identify the operation types + # New socket names + OPERATION_TYPES = {'create': 'net', + 'post_create': 'net', + 'bind': 'net', + 'connect': 'net', + 'listen': 'net', + 'accept': 'net', + 'sendmsg': 'net', + 'recvmsg': 'net', + 'getsockname': 'net', + 'getpeername': 'net', + 'getsockopt': 'net', + 'setsockopt': 'net', + 'sock_shutdown': 'net' + } + + def __init__(self, pid, filename, existing_profiles, profile_dir, log): + self.filename = filename + self.profile_dir = profile_dir + self.pid = pid + self.existing_profiles = existing_profiles + self.log = log + self.debug_logger = DebugLogger('ReadLog') + self.LOG = None + self.logmark = '' + self.seenmark = None + self.next_log_entry = None + + def prefetch_next_log_entry(self): + 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): + self.next_log_entry = self.LOG.readline() + if not self.next_log_entry: + break + + def get_next_log_entry(self): + # If no next log entry fetch it + if not self.next_log_entry: + self.prefetch_next_log_entry() + log_entry = self.next_log_entry + self.next_log_entry = None + return log_entry + + def peek_at_next_log_entry(self): + # Take a peek at the next log entry + if not self.next_log_entry: + self.prefetch_next_log_entry() + return self.next_log_entry + + def throw_away_next_log_entry(self): + self.next_log_entry = None + + def parse_log_record(self, record): + self.debug_logger.debug('parse_log_record: %s' % record) + + record_event = self.parse_event(record) + return record_event + + def parse_event(self, msg): + """Parse the event from log into key value pairs""" + msg = msg.strip() + self.debug_logger.info('parse_event: %s' % msg) + #print(repr(msg)) + if sys.version_info < (3, 0): + # parse_record fails with u'foo' style strings hence typecasting to string + msg = str(msg) + event = LibAppArmor.parse_record(msg) + ev = dict() + ev['resource'] = event.info + ev['active_hat'] = event.active_hat + ev['aamode'] = event.event + ev['time'] = event.epoch + ev['operation'] = event.operation + ev['profile'] = event.profile + ev['name'] = event.name + ev['name2'] = event.name2 + ev['attr'] = event.attribute + ev['parent'] = event.parent + ev['pid'] = event.pid + ev['task'] = event.task + ev['info'] = event.info + dmask = event.denied_mask + rmask = event.requested_mask + ev['magic_token'] = event.magic_token + if ev['operation'] and self.op_type(ev['operation']) == 'net': + ev['family'] = event.net_family + ev['protocol'] = event.net_protocol + ev['sock_type'] = event.net_sock_type + LibAppArmor.free_record(event) + # Map c (create) to a and d (delete) to w, logprof doesn't support c and d + if rmask: + rmask = rmask.replace('c', 'a') + rmask = rmask.replace('d', 'w') + if not validate_log_mode(hide_log_mode(rmask)): + raise AppArmorException(_('Log contains unknown mode %s') % rmask) + if dmask: + dmask = dmask.replace('c', 'a') + dmask = dmask.replace('d', 'w') + if not validate_log_mode(hide_log_mode(dmask)): + raise AppArmorException(_('Log contains unknown mode %s') % dmask) + #print('parse_event:', ev['profile'], dmask, ev['name2']) + mask, name = log_str_to_mode(ev['profile'], dmask, ev['name2']) + + ev['denied_mask'] = mask + ev['name2'] = name + + mask, name = log_str_to_mode(ev['profile'], rmask, ev['name2']) + ev['request_mask'] = mask + ev['name2'] = name + + if not ev['time']: + ev['time'] = int(time.time()) + # Remove None keys + #for key in ev.keys(): + # if not ev[key] or not re.search('[\w]+', ev[key]): + # ev.pop(key) + + if ev['aamode']: + # Convert aamode values to their counter-parts + mode_convertor = {0: 'UNKNOWN', + 1: 'ERROR', + 2: 'AUDITING', + 3: 'PERMITTING', + 4: 'REJECTING', + 5: 'HINT', + 6: 'STATUS' + } + try: + ev['aamode'] = mode_convertor[ev['aamode']] + except KeyError: + ev['aamode'] = None + + if ev['aamode']: + #debug_logger.debug(ev) + return ev + else: + return None + + def add_to_tree(self, loc_pid, parent, type, event): + self.debug_logger.info('add_to_tree: pid [%s] type [%s] event [%s]' % (loc_pid, type, event)) + if not self.pid.get(loc_pid, False): + profile, hat = event[:2] + if parent and self.pid.get(parent, False): + if not hat: + hat = 'null-complain-profile' + arrayref = [] + self.pid[parent].append(arrayref) + self.pid[loc_pid] = arrayref + for ia in ['fork', loc_pid, profile, hat]: + arrayref.append(ia) +# self.pid[parent].append(array_ref) +# self.pid[loc_pid] = array_ref + else: + arrayref = [] + self.log.append(arrayref) + self.pid[loc_pid] = arrayref +# self.log.append(array_ref) +# self.pid[loc_pid] = array_ref + self.pid[loc_pid].append([type, loc_pid] + event) + #print("\n\npid",self.pid) + #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']): + aamode = 'AUDIT' + elif re.search('(UNKNOWN\[1502\]|APPARMOR_ALLOWED|1502)', e['type']): + aamode = 'PERMITTING' + elif re.search('(UNKNOWN\[1503\]|APPARMOR_DENIED|1503)', e['type']): + aamode = 'REJECTING' + elif re.search('(UNKNOWN\[1504\]|APPARMOR_HINT|1504)', e['type']): + aamode = 'HINT' + elif re.search('(UNKNOWN\[1505\]|APPARMOR_STATUS|1505)', e['type']): + aamode = 'STATUS' + elif re.search('(UNKNOWN\[1506\]|APPARMOR_ERROR|1506)', e['type']): + aamode = 'ERROR' + else: + aamode = 'UNKNOWN' + + if aamode in ['UNKNOWN', 'AUDIT', 'STATUS', 'ERROR']: + return None + + if 'profile_set' in e['operation']: + return None + + # Skip if AUDIT event was issued due to a change_hat in unconfined mode + if not e.get('profile', False): + return None + + # Convert new null profiles to old single level null profile + if '//null-' in e['profile']: + e['profile'] = 'null-complain-profile' + + profile = e['profile'] + hat = None + + if '//' in e['profile']: + profile, hat = e['profile'].split('//')[:2] + + # Filter out change_hat events that aren't from learning + if e['operation'] == 'change_hat': + if aamode != 'HINT' and aamode != 'PERMITTING': + return None + profile = e['name'] + #hat = None + if '//' in e['name']: + profile, hat = e['name'].split('//')[:2] + + if not hat: + hat = profile + + # prog is no longer passed around consistently + prog = 'HINT' + + if profile != 'null-complain-profile' and not self.profile_exists(profile): + return None + if e['operation'] == 'exec': + if e.get('info', False) and e['info'] == 'mandatory profile missing': + self.add_to_tree(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']: + self.add_to_tree(e['pid'], e['parent'], 'exec', + [profile, hat, prog, aamode, e['denied_mask'], e['name'], '']) + elif e.get('name', False): + self.add_to_tree(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']) + + elif 'file_' in e['operation']: + self.add_to_tree(e['pid'], e['parent'], 'path', + [profile, hat, prog, aamode, e['denied_mask'], e['name'], '']) + elif e['operation'] in ['open', 'truncate', 'mkdir', 'mknod', 'rename_src', + 'rename_dest', 'unlink', 'rmdir', 'symlink_create', 'link']: + #print(e['operation'], e['name']) + self.add_to_tree(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', + [profile, hat, prog, aamode, e['name'], '']) + elif e['operation'] == 'setattr' or 'xattr' in e['operation']: + self.add_to_tree(e['pid'], e['parent'], 'path', + [profile, hat, prog, aamode, e['denied_mask'], e['name'], '']) + elif 'inode_' in e['operation']: + is_domain_change = False + if e['operation'] == 'inode_permission' and (e['denied_mask'] & AA_MAY_EXEC) and aamode == 'PERMITTING': + following = self.peek_at_next_log_entry() + if following: + entry = self.parse_log_record(following) + if entry and entry.get('info', False) == 'set profile': + is_domain_change = True + self.throw_away_next_log_entry() + + if is_domain_change: + self.add_to_tree(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', + [profile, hat, prog, aamode, e['denied_mask'], e['name'], '']) + + elif e['operation'] == 'sysctl': + self.add_to_tree(e['pid'], e['parent'], 'path', + [profile, hat, prog, aamode, e['denied_mask'], e['name'], '']) + + elif e['operation'] == 'clone': + parent, child = e['pid'], e['task'] + if not parent: + parent = 'null-complain-profile' + if not hat: + hat = 'null-complain-profile' + arrayref = [] + if self.pid.get(parent, False): + self.pid[parent].append(arrayref) + else: + self.log.append(arrayref) + self.pid[child].append(arrayref) + for ia in ['fork', child, profile, hat]: + arrayref.append(ia) +# if self.pid.get(parent, False): +# self.pid[parent] += [arrayref] +# else: +# self.log += [arrayref] +# self.pid[child] = arrayref + + elif self.op_type(e['operation']) == 'net': + self.add_to_tree(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', + [profile, hat, aamode, hat]) + else: + self.debug_logger.debug('UNHANDLED: %s' % e) + + def read_log(self, logmark): + self.logmark = logmark + seenmark = True + if self.logmark: + seenmark = False + #last = None + #event_type = None + try: + #print(self.filename) + self.LOG = open_file_read(self.filename) + except IOError: + raise AppArmorException('Can not read AppArmor logfile: ' + self.filename) + #LOG = open_file_read(log_open) + line = True + while line: + line = self.get_next_log_entry() + if not line: + break + line = line.strip() + self.debug_logger.debug('read_log: %s' % line) + if self.logmark in line: + seenmark = True + + self.debug_logger.debug('read_log: seenmark = %s' % seenmark) + if not seenmark: + continue + + event = self.parse_log_record(line) + #print(event) + if event: + self.add_event_to_tree(event) + self.LOG.close() + self.logmark = '' + return self.log + + def op_type(self, operation): + """Returns the operation type if known, unkown otherwise""" + operation_type = self.OPERATION_TYPES.get(operation, 'unknown') + return operation_type + + def profile_exists(self, program): + """Returns True if profile exists, False otherwise""" + # Check cache of profiles + if self.existing_profiles.get(program, False): + return True + # Check the disk for profile + prof_path = self.get_profile_filename(program) + #print(prof_path) + if os.path.isfile(prof_path): + # Add to cache of profile + self.existing_profiles[program] = prof_path + return True + return False + + def get_profile_filename(self, profile): + """Returns the full profile name""" + if profile.startswith('/'): + # Remove leading / + profile = profile[1:] + else: + profile = "profile_" + profile + profile = profile.replace('/', '.') + full_profilename = self.profile_dir + '/' + profile + return full_profilename diff --git a/utils/apparmor/sandbox.py b/utils/apparmor/sandbox.py index dc2d19151..51048f6ff 100644 --- a/utils/apparmor/sandbox.py +++ b/utils/apparmor/sandbox.py @@ -706,6 +706,7 @@ def run_xsandbox(command, opt): x.start() except Exception as e: error(e) + os.chdir(old_cwd) if not opt.read_path: opt.read_path = [] @@ -721,6 +722,5 @@ def run_xsandbox(command, opt): x.cleanup() raise x.cleanup() - os.chdir(old_cwd) return rc, report diff --git a/utils/apparmor/severity.py b/utils/apparmor/severity.py new file mode 100644 index 000000000..4c11ac71a --- /dev/null +++ b/utils/apparmor/severity.py @@ -0,0 +1,208 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +from __future__ import with_statement +import os +import re +from apparmor.common import AppArmorException, open_file_read, warn, convert_regexp # , msg, error, debug + +class Severity(object): + def __init__(self, dbname=None, default_rank=10): + """Initialises the class object""" + self.PROF_DIR = '/etc/apparmor.d' # The profile directory + self.severity = dict() + self.severity['DATABASENAME'] = dbname + self.severity['CAPABILITIES'] = {} + self.severity['FILES'] = {} + self.severity['REGEXPS'] = {} + self.severity['DEFAULT_RANK'] = default_rank + # For variable expansions for the profile + self.severity['VARIABLES'] = dict() + if not dbname: + return None + + with open_file_read(dbname) as database: # open(dbname, 'r') + for lineno, line in enumerate(database, start=1): + line = line.strip() # or only rstrip and lstrip? + if line == '' or line.startswith('#'): + continue + if line.startswith('/'): + try: + path, read, write, execute = line.split() + read, write, execute = int(read), int(write), int(execute) + except ValueError: + raise AppArmorException("Insufficient values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) + else: + if read not in range(0, 11) or write not in range(0, 11) or execute not in range(0, 11): + raise AppArmorException("Inappropriate values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) + path = path.lstrip('/') + if '*' not in path: + self.severity['FILES'][path] = {'r': read, 'w': write, 'x': execute} + else: + ptr = self.severity['REGEXPS'] + pieces = path.split('/') + for index, piece in enumerate(pieces): + if '*' in piece: + path = '/'.join(pieces[index:]) + regexp = convert_regexp(path) + ptr[regexp] = {'AA_RANK': {'r': read, 'w': write, 'x': execute}} + break + else: + ptr[piece] = ptr.get(piece, {}) + ptr = ptr[piece] + elif line.startswith('CAP_'): + try: + resource, severity = line.split() + severity = int(severity) + except ValueError: + error_message = 'No severity value present in file: %s\n\t[Line %s]: %s' % (dbname, lineno, line) + #error(error_message) + raise AppArmorException(error_message) # from None + else: + if severity not in range(0, 11): + raise AppArmorException("Inappropriate severity value present in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) + self.severity['CAPABILITIES'][resource] = severity + else: + raise AppArmorException("Unexpected line in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) + + def handle_capability(self, resource): + """Returns the severity of for the capability resource, default value if no match""" + if resource in self.severity['CAPABILITIES'].keys(): + return self.severity['CAPABILITIES'][resource] + # raise ValueError("unexpected capability rank input: %s"%resource) + warn("unknown capability: %s" % resource) + return self.severity['DEFAULT_RANK'] + + def check_subtree(self, tree, mode, sev, segments): + """Returns the max severity from the regex tree""" + if len(segments) == 0: + first = '' + else: + first = segments[0] + rest = segments[1:] + path = '/'.join([first] + rest) + # Check if we have a matching directory tree to descend into + if tree.get(first, False): + sev = self.check_subtree(tree[first], mode, sev, rest) + # If severity still not found, match against globs + if sev is None: + # Match against all globs at this directory level + for chunk in tree.keys(): + if '*' in chunk: + # Match rest of the path + if re.search("^" + chunk, path): + # Find max rank + if "AA_RANK" in tree[chunk].keys(): + for m in mode: + if sev is None or tree[chunk]["AA_RANK"].get(m, -1) > sev: + sev = tree[chunk]["AA_RANK"].get(m, None) + return sev + + def handle_file(self, resource, mode): + """Returns the severity for the file, default value if no match found""" + resource = resource[1:] # remove initial / from path + pieces = resource.split('/') # break path into directory level chunks + sev = None + # Check for an exact match in the db + if resource in self.severity['FILES'].keys(): + # Find max value among the given modes + for m in mode: + if sev is None or self.severity['FILES'][resource].get(m, -1) > sev: + sev = self.severity['FILES'][resource].get(m, None) + else: + # Search regex tree for matching glob + sev = self.check_subtree(self.severity['REGEXPS'], mode, sev, pieces) + if sev is None: + # Return default rank if severity cannot be found + return self.severity['DEFAULT_RANK'] + else: + return sev + + def rank(self, resource, mode=None): + """Returns the rank for the resource file/capability""" + if '@' in resource: # path contains variable + return self.handle_variable_rank(resource, mode) + elif resource[0] == '/': # file resource + return self.handle_file(resource, mode) + elif resource[0:4] == 'CAP_': # capability resource + return self.handle_capability(resource) + else: + raise AppArmorException("Unexpected rank input: %s" % resource) + + def handle_variable_rank(self, resource, mode): + """Returns the max possible rank for file resources containing variables""" + regex_variable = re.compile('@{([^{.]*)}') + rank = None + if '@' in resource: + variable = regex_variable.search(resource).groups()[0] + variable = '@{%s}' % variable + #variables = regex_variable.findall(resource) + for replacement in self.severity['VARIABLES'][variable]: + resource_replaced = self.variable_replace(variable, replacement, resource) + rank_new = self.handle_variable_rank(resource_replaced, mode) + #rank_new = self.handle_variable_rank(resource.replace('@{'+variable+'}', replacement), mode) + if rank is None or rank_new > rank: + rank = rank_new + return rank + else: + return self.handle_file(resource, mode) + + def variable_replace(self, variable, replacement, resource): + """Returns the expanded path for the passed variable""" + leading = False + trailing = False + # Check for leading or trailing / that may need to be collapsed + if resource.find("/" + variable) != -1 and resource.find("//" + variable) == -1: # find that a single / exists before variable or not + leading = True + if resource.find(variable + "/") != -1 and resource.find(variable + "//") == -1: + trailing = True + if replacement[0] == '/' and replacement[:2] != '//' and leading: # finds if the replacement has leading / or not + replacement = replacement[1:] + if replacement[-1] == '/' and replacement[-2:] != '//' and trailing: + replacement = replacement[:-1] + return resource.replace(variable, replacement) + + def load_variables(self, prof_path): + """Loads the variables for the given profile""" + regex_include = re.compile('^#?include\s*<(\S*)>') + if os.path.isfile(prof_path): + with open_file_read(prof_path) as f_in: + for line in f_in: + line = line.strip() + # If any includes, load variables from them first + match = regex_include.search(line) + if match: + new_path = match.groups()[0] + new_path = self.PROF_DIR + '/' + new_path + self.load_variables(new_path) + else: + # Remove any comments + if '#' in line: + line = line.split('#')[0].rstrip() + # Expected format is @{Variable} = value1 value2 .. + if line.startswith('@') and '=' in line: + if '+=' in line: + line = line.split('+=') + try: + self.severity['VARIABLES'][line[0]] += [i.strip('"') for i in line[1].split()] + except KeyError: + raise AppArmorException("Variable %s was not previously declared, but is being assigned additional value in file: %s" % (line[0], prof_path)) + else: + line = line.split('=') + if line[0] in self.severity['VARIABLES'].keys(): + raise AppArmorException("Variable %s was previously declared in file: %s" % (line[0], prof_path)) + self.severity['VARIABLES'][line[0]] = [i.strip('"') for i in line[1].split()] + + def unload_variables(self): + """Clears all loaded variables""" + self.severity['VARIABLES'] = dict() diff --git a/utils/apparmor/tools.py b/utils/apparmor/tools.py new file mode 100644 index 000000000..23d704c8a --- /dev/null +++ b/utils/apparmor/tools.py @@ -0,0 +1,185 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import os +import sys + +import apparmor.aa as apparmor +import apparmor.ui as aaui +from apparmor.common import user_perm + +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() + +class aa_tools: + def __init__(self, tool_name, args): + self.name = tool_name + self.profiledir = args.dir + self.profiling = args.program + self.check_profile_dir() + self.silent = None + + if tool_name in ['audit', 'complain']: + self.remove = args.remove + elif tool_name == 'disable': + self.revert = args.revert + self.disabledir = apparmor.profile_dir + '/disable' + self.check_disable_dir() + elif tool_name == 'autodep': + self.force = args.force + self.aa_mountpoint = apparmor.check_for_apparmor() + elif tool_name == 'cleanprof': + self.silent = args.silent + + def check_profile_dir(self): + if self.profiledir: + apparmor.profile_dir = apparmor.get_full_path(self.profiledir) + if not os.path.isdir(apparmor.profile_dir): + raise apparmor.AppArmorException("%s is not a directory." % self.profiledir) + + if not user_perm(apparmor.profile_dir): + raise apparmor.AppArmorException("Cannot write to profile directory: %s" % (apparmor.profile_dir)) + + def check_disable_dir(self): + if not os.path.isdir(self.disabledir): + raise apparmor.AppArmorException("Can't find AppArmor disable directory %s" % self.disabledir) + + def act(self): + for p in self.profiling: + if not p: + continue + + program = None + if os.path.exists(p): + program = apparmor.get_full_path(p).strip() + else: + which = apparmor.which(p) + if which: + program = apparmor.get_full_path(which) + + apparmor.read_profiles() + #If program does not exists on the system but its profile does + if not program and apparmor.profile_exists(p): + program = p + + if not program or not(os.path.exists(program) or apparmor.profile_exists(program)): + if program and not program.startswith('/'): + program = aaui.UI_GetString(_('The given program cannot be found, please try with the fully qualified path name of the program: '), '') + else: + aaui.UI_Info(_("%s does not exist, please double-check the path.") % p) + sys.exit(1) + + if self.name == 'autodep' and program and os.path.exists(program): + self.use_autodep(program) + + elif program and apparmor.profile_exists(program): + if self.name == 'cleanprof': + self.clean_profile(program, p) + + else: + filename = apparmor.get_profile_filename(program) + + if not os.path.isfile(filename) or apparmor.is_skippable_file(filename): + aaui.UI_Info(_('Profile for %s not found, skipping') % p) + + elif self.name == 'disable': + if not self.revert: + aaui.UI_Info(_('Disabling %s.') % program) + self.disable_profile(filename) + else: + aaui.UI_Info(_('Enabling %s.') % program) + self.enable_profile(filename) + + elif self.name == 'audit': + if not self.remove: + aaui.UI_Info(_('Setting %s to audit mode.') % program) + else: + aaui.UI_Info(_('Removing audit mode from %s.') % program) + apparmor.change_profile_flags(filename, program, 'audit', not self.remove) + + elif self.name == 'complain': + if not self.remove: + apparmor.set_complain(filename, program) + else: + apparmor.set_enforce(filename, program) + #apparmor.set_profile_flags(filename, self.name) + else: + # One simply does not walk in here! + raise apparmor.AppArmorException('Unknown tool: %s' % self.name) + + cmd_info = apparmor.cmd([apparmor.parser, filename, '-I%s' % apparmor.profile_dir, '-R 2>&1', '1>/dev/null']) + #cmd_info = apparmor.cmd(['cat', filename, '|', apparmor.parser, '-I%s'%apparmor.profile_dir, '-R 2>&1', '1>/dev/null']) + + if cmd_info[0] != 0: + raise apparmor.AppArmorException(cmd_info[1]) + + else: + if '/' not in p: + aaui.UI_Info(_("Can't find %s in the system path list. If the name of the application\nis correct, please run 'which %s' as a user with correct PATH\nenvironment set up in order to find the fully-qualified path and\nuse the full path as parameter.") % (p, p)) + else: + aaui.UI_Info(_("%s does not exist, please double-check the path.") % p) + sys.exit(1) + + def clean_profile(self, program, p): + filename = apparmor.get_profile_filename(program) + import apparmor.cleanprofile as cleanprofile + prof = cleanprofile.Prof(filename) + cleanprof = cleanprofile.CleanProf(True, prof, prof) + deleted = cleanprof.remove_duplicate_rules(program) + aaui.UI_Info(_("\nDeleted %s rules.") % deleted) + apparmor.changed[program] = True + + if filename: + if not self.silent: + q = apparmor.hasher() + q['title'] = 'Changed Local Profiles' + q['headers'] = [] + q['explanation'] = _('The local profile for %s in file %s was changed. Would you like to save it?') % (program, filename) + q['functions'] = ['CMD_SAVE_CHANGES', 'CMD_VIEW_CHANGES', 'CMD_ABORT'] + q['default'] = 'CMD_VIEW_CHANGES' + q['options'] = [] + q['selected'] = 0 + p = None + ans = '' + arg = None + while ans != 'CMD_SAVE_CHANGES': + ans, arg = aaui.UI_PromptUser(q) + if ans == 'CMD_SAVE_CHANGES': + apparmor.write_profile_ui_feedback(program) + apparmor.reload_base(program) + elif ans == 'CMD_VIEW_CHANGES': + #oldprofile = apparmor.serialize_profile(apparmor.original_aa[program], program, '') + newprofile = apparmor.serialize_profile(apparmor.aa[program], program, '') + apparmor.display_changes_with_comments(filename, newprofile) + else: + apparmor.write_profile_ui_feedback(program) + apparmor.reload_base(program) + else: + raise apparmor.AppArmorException(_('The profile for %s does not exists. Nothing to clean.') % p) + + def use_autodep(self, program): + apparmor.check_qualifiers(program) + + if os.path.exists(apparmor.get_profile_filename(program)) and not self.force: + aaui.UI_Info('Profile for %s already exists - skipping.' % program) + else: + apparmor.autodep(program) + if self.aa_mountpoint: + apparmor.reload(program) + + def enable_profile(self, filename): + apparmor.delete_symlink('disable', filename) + + def disable_profile(self, filename): + apparmor.create_symlink('disable', filename) diff --git a/utils/apparmor/translations.py b/utils/apparmor/translations.py new file mode 100644 index 000000000..fb6d5c40d --- /dev/null +++ b/utils/apparmor/translations.py @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------ +# +# 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 gettext + +TRANSLATION_DOMAIN = 'apparmor-utils' + +__apparmor_gettext__ = None + +def init_translation(): + global __apparmor_gettext__ + if __apparmor_gettext__ is None: + t = gettext.translation(TRANSLATION_DOMAIN, fallback=True) + __apparmor_gettext__ = t.gettext + return __apparmor_gettext__ diff --git a/utils/apparmor/ui.py b/utils/apparmor/ui.py new file mode 100644 index 000000000..570d28cfa --- /dev/null +++ b/utils/apparmor/ui.py @@ -0,0 +1,456 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import sys +import re +from apparmor.yasti import yastLog, SendDataToYast, GetDataFromYast + +from apparmor.common import readkey, AppArmorException, DebugLogger + +# setup module translations +from apparmor.translations import init_translation +_ = init_translation() + +# Set up UI logger for separate messages from UI module +debug_logger = DebugLogger('UI') + +# The operating mode: yast or text, text by default +UI_mode = 'text' + +ARROWS = {'A': 'UP', 'B': 'DOWN', 'C': 'RIGHT', 'D': 'LEFT'} + +def getkey(): + key = readkey() + if key == '\x1B': + key = readkey() + if key == '[': + key = readkey() + if(ARROWS.get(key, False)): + key = ARROWS[key] + return key.strip() + +def UI_Info(text): + debug_logger.info(text) + if UI_mode == 'text': + sys.stdout.write(text + '\n') + else: + yastLog(text) + +def UI_Important(text): + debug_logger.debug(text) + if UI_mode == 'text': + sys.stdout.write('\n' + text + '\n') + else: + SendDataToYast({'type': 'dialog-error', + 'message': text + }) + path, yarg = GetDataFromYast() + +def get_translated_hotkey(translated, cmsg=''): + msg = 'PromptUser: ' + _('Invalid hotkey for') + + # Originally (\S) was used but with translations it would not work :( + if re.search('\((\S+)\)', translated, re.LOCALE): + return re.search('\((\S+)\)', translated, re.LOCALE).groups()[0] + else: + if cmsg: + raise AppArmorException(cmsg) + else: + raise AppArmorException('%s %s' % (msg, translated)) + +def UI_YesNo(text, default): + debug_logger.debug('UI_YesNo: %s: %s %s' % (UI_mode, text, default)) + default = default.lower() + ans = None + if UI_mode == 'text': + yes = _('(Y)es') + no = _('(N)o') + yeskey = get_translated_hotkey(yes).lower() + nokey = get_translated_hotkey(no).lower() + ans = 'XXXINVALIDXXX' + while ans not in ['y', 'n']: + sys.stdout.write('\n' + text + '\n') + if default == 'y': + sys.stdout.write('\n[%s] / %s\n' % (yes, no)) + else: + sys.stdout.write('\n%s / [%s]\n' % (yes, no)) + ans = getkey() + if ans: + # Get back to english from localised answer + ans = ans.lower() + if ans == yeskey: + ans = 'y' + elif ans == nokey: + ans = 'n' + elif ans == 'left': + default = 'y' + elif ans == 'right': + default = 'n' + else: + ans = 'XXXINVALIDXXX' + continue # If user presses any other button ask again + else: + ans = default + + else: + SendDataToYast({'type': 'dialog-yesno', + 'question': text + }) + ypath, yarg = GetDataFromYast() + ans = yarg['answer'] + if not ans: + ans = default + return ans + +def UI_YesNoCancel(text, default): + debug_logger.debug('UI_YesNoCancel: %s: %s %s' % (UI_mode, text, default)) + default = default.lower() + ans = None + if UI_mode == 'text': + yes = _('(Y)es') + no = _('(N)o') + cancel = _('(C)ancel') + + yeskey = get_translated_hotkey(yes).lower() + nokey = get_translated_hotkey(no).lower() + cancelkey = get_translated_hotkey(cancel).lower() + + ans = 'XXXINVALIDXXX' + while ans not in ['c', 'n', 'y']: + sys.stdout.write('\n' + text + '\n') + if default == 'y': + sys.stdout.write('\n[%s] / %s / %s\n' % (yes, no, cancel)) + elif default == 'n': + sys.stdout.write('\n%s / [%s] / %s\n' % (yes, no, cancel)) + else: + sys.stdout.write('\n%s / %s / [%s]\n' % (yes, no, cancel)) + ans = getkey() + if ans: + # Get back to english from localised answer + ans = ans.lower() + if ans == yeskey: + ans = 'y' + elif ans == nokey: + ans = 'n' + elif ans == cancelkey: + ans = 'c' + elif ans == 'left': + if default == 'n': + default = 'y' + elif default == 'c': + default = 'n' + elif ans == 'right': + if default == 'y': + default = 'n' + elif default == 'n': + default = 'c' + else: + ans = default + else: + SendDataToYast({'type': 'dialog-yesnocancel', + 'question': text + }) + ypath, yarg = GetDataFromYast() + ans = yarg['answer'] + if not ans: + ans = default + return ans + +def UI_GetString(text, default): + debug_logger.debug('UI_GetString: %s: %s %s' % (UI_mode, text, default)) + string = default + if UI_mode == 'text': + sys.stdout.write('\n' + text) + string = sys.stdin.readline() + else: + SendDataToYast({'type': 'dialog-getstring', + 'label': text, + 'default': default + }) + ypath, yarg = GetDataFromYast() + string = yarg['string'] + return string.strip() + +def UI_GetFile(file): + debug_logger.debug('UI_GetFile: %s' % UI_mode) + filename = None + if UI_mode == 'text': + sys.stdout.write(file['description'] + '\n') + filename = sys.stdin.read() + else: + file['type'] = 'dialog-getfile' + SendDataToYast(file) + ypath, yarg = GetDataFromYast() + if yarg['answer'] == 'okay': + filename = yarg['filename'] + return filename + +def UI_BusyStart(message): + debug_logger.debug('UI_BusyStart: %s' % UI_mode) + if UI_mode == 'text': + UI_Info(message) + else: + SendDataToYast({'type': 'dialog-busy-start', + 'message': message + }) + ypath, yarg = GetDataFromYast() + +def UI_BusyStop(): + debug_logger.debug('UI_BusyStop: %s' % UI_mode) + if UI_mode != 'text': + SendDataToYast({'type': 'dialog-busy-stop'}) + ypath, yarg = GetDataFromYast() + +CMDS = {'CMD_ALLOW': _('(A)llow'), + 'CMD_OTHER': _('(M)ore'), + 'CMD_AUDIT_NEW': _('Audi(t)'), + 'CMD_AUDIT_OFF': _('Audi(t) off'), + 'CMD_AUDIT_FULL': _('Audit (A)ll'), + #'CMD_OTHER': '(O)pts', + 'CMD_USER_ON': _('(O)wner permissions on'), + 'CMD_USER_OFF': _('(O)wner permissions off'), + 'CMD_DENY': _('(D)eny'), + 'CMD_ABORT': _('Abo(r)t'), + 'CMD_FINISHED': _('(F)inish'), + 'CMD_ix': _('(I)nherit'), + 'CMD_px': _('(P)rofile'), + 'CMD_px_safe': _('(P)rofile Clean Exec'), + 'CMD_cx': _('(C)hild'), + 'CMD_cx_safe': _('(C)hild Clean Exec'), + 'CMD_nx': _('(N)amed'), + 'CMD_nx_safe': _('(N)amed Clean Exec'), + 'CMD_ux': _('(U)nconfined'), + 'CMD_ux_safe': _('(U)nconfined Clean Exec'), + 'CMD_pix': _('(P)rofile Inherit'), + 'CMD_pix_safe': _('(P)rofile Inherit Clean Exec'), + 'CMD_cix': _('(C)hild Inherit'), + 'CMD_cix_safe': _('(C)hild Inherit Clean Exec'), + 'CMD_nix': _('(N)amed Inherit'), + 'CMD_nix_safe': _('(N)amed Inherit Clean Exec'), + 'CMD_EXEC_IX_ON': _('(X) ix On'), + 'CMD_EXEC_IX_OFF': _('(X) ix Off'), + 'CMD_SAVE': _('(S)ave Changes'), + 'CMD_CONTINUE': _('(C)ontinue Profiling'), + 'CMD_NEW': _('(N)ew'), + 'CMD_GLOB': _('(G)lob'), + 'CMD_GLOBEXT': _('Glob with (E)xtension'), + 'CMD_ADDHAT': _('(A)dd Requested Hat'), + 'CMD_USEDEFAULT': _('(U)se Default Hat'), + 'CMD_SCAN': _('(S)can system log for AppArmor events'), + 'CMD_HELP': _('(H)elp'), + 'CMD_VIEW_PROFILE': _('(V)iew Profile'), + 'CMD_USE_PROFILE': _('(U)se Profile'), + 'CMD_CREATE_PROFILE': _('(C)reate New Profile'), + 'CMD_UPDATE_PROFILE': _('(U)pdate Profile'), + 'CMD_IGNORE_UPDATE': _('(I)gnore Update'), + 'CMD_SAVE_CHANGES': _('(S)ave Changes'), + 'CMD_SAVE_SELECTED': _('Save Selec(t)ed Profile'), + 'CMD_UPLOAD_CHANGES': _('(U)pload Changes'), + 'CMD_VIEW_CHANGES': _('(V)iew Changes'), + 'CMD_VIEW_CHANGES_CLEAN': _('View Changes b/w (C)lean profiles'), + 'CMD_VIEW': _('(V)iew'), + 'CMD_ENABLE_REPO': _('(E)nable Repository'), + 'CMD_DISABLE_REPO': _('(D)isable Repository'), + 'CMD_ASK_NEVER': _('(N)ever Ask Again'), + 'CMD_ASK_LATER': _('Ask Me (L)ater'), + 'CMD_YES': _('(Y)es'), + 'CMD_NO': _('(N)o'), + 'CMD_ALL_NET': _('Allow All (N)etwork'), + 'CMD_NET_FAMILY': _('Allow Network Fa(m)ily'), + 'CMD_OVERWRITE': _('(O)verwrite Profile'), + 'CMD_KEEP': _('(K)eep Profile'), + 'CMD_CONTINUE': _('(C)ontinue'), + 'CMD_IGNORE_ENTRY': _('(I)gnore') + } + +def UI_PromptUser(q, params=''): + cmd = None + arg = None + if UI_mode == 'text': + cmd, arg = Text_PromptUser(q) + else: + q['type'] = 'wizard' + SendDataToYast(q) + ypath, yarg = GetDataFromYast() + if not cmd: + cmd = 'CMD_ABORT' + arg = yarg['selected'] + if cmd == 'CMD_ABORT': + confirm_and_abort() + cmd = 'XXXINVALIDXXX' + elif cmd == 'CMD_FINISHED': + if not params: + confirm_and_finish() + cmd = 'XXXINVALIDXXX' + return (cmd, arg) + +def confirm_and_abort(): + ans = UI_YesNo(_('Are you sure you want to abandon this set of profile changes and exit?'), 'n') + if ans == 'y': + UI_Info(_('Abandoning all changes.')) + #shutdown_yast() + #for prof in created: + # delete_profile(prof) + sys.exit(0) + +def UI_ShortMessage(title, message): + SendDataToYast({'type': 'short-dialog-message', + 'headline': title, + 'message': message + }) + ypath, yarg = GetDataFromYast() + +def UI_LongMessage(title, message): + SendDataToYast({'type': 'long-dialog-message', + 'headline': title, + 'message': message + }) + ypath, yarg = GetDataFromYast() + +def confirm_and_finish(): + sys.stdout.write(_('FINISHING...\n')) + sys.exit(0) + +def Text_PromptUser(question): + title = question['title'] + explanation = question['explanation'] + headers = question['headers'] + functions = question['functions'] + + default = question['default'] + options = question['options'] + selected = question.get('selected', 0) + helptext = question['helptext'] + if helptext: + functions.append('CMD_HELP') + + menu_items = [] + keys = dict() + + for cmd in functions: + if not CMDS.get(cmd, False): + raise AppArmorException(_('PromptUser: Unknown command %s') % cmd) + + menutext = CMDS[cmd] + + key = get_translated_hotkey(menutext).lower() + # Duplicate hotkey + if keys.get(key, False): + raise AppArmorException(_('PromptUser: Duplicate hotkey for %s: %s ') % (cmd, menutext)) + + keys[key] = cmd + + if default and default == cmd: + menutext = '[%s]' % menutext + + menu_items.append(menutext) + + default_key = 0 + if default and CMDS[default]: + defaulttext = CMDS[default] + defmsg = _('PromptUser: Invalid hotkey in default item') + + default_key = get_translated_hotkey(defaulttext, defmsg).lower() + + if not keys.get(default_key, False): + raise AppArmorException(_('PromptUser: Invalid default %s') % default) + + widest = 0 + header_copy = headers[:] + while header_copy: + header = header_copy.pop(0) + header_copy.pop(0) + if len(header) > widest: + widest = len(header) + widest += 1 + + formatstr = '%-' + str(widest) + 's %s\n' + + function_regexp = '^(' + function_regexp += '|'.join(keys.keys()) + if options: + function_regexp += '|\d' + function_regexp += ')$' + + ans = 'XXXINVALIDXXX' + while not re.search(function_regexp, ans, flags=re.IGNORECASE): + + prompt = '\n' + if title: + prompt += '= %s =\n\n' % title + + if headers: + header_copy = headers[:] + while header_copy: + header = header_copy.pop(0) + value = header_copy.pop(0) + prompt += formatstr % (header + ':', value) + prompt += '\n' + + if explanation: + prompt += explanation + '\n\n' + + if options: + for index, option in enumerate(options): + if selected == index: + format_option = ' [%s - %s]' + else: + format_option = ' %s - %s ' + prompt += format_option % (index + 1, option) + prompt += '\n' + + prompt += ' / '.join(menu_items) + + sys.stdout.write(prompt + '\n') + + ans = getkey().lower() + + if ans: + if ans == 'up': + if options and selected > 0: + selected -= 1 + ans = 'XXXINVALIDXXX' + + elif ans == 'down': + if options and selected < len(options) - 1: + selected += 1 + ans = 'XXXINVALIDXXX' + +# elif keys.get(ans, False) == 'CMD_HELP': +# sys.stdout.write('\n%s\n' %helptext) +# ans = 'XXXINVALIDXXX' + + elif is_number(ans) == 10: + # If they hit return choose default option + ans = default_key + + elif options and re.search('^\d$', ans): + ans = int(ans) + if ans > 0 and ans <= len(options): + selected = ans - 1 + ans = 'XXXINVALIDXXX' + + if keys.get(ans, False) == 'CMD_HELP': + sys.stdout.write('\n%s\n' % helptext) + ans = 'again' + + if keys.get(ans, False): + ans = keys[ans] + + return ans, selected + +def is_number(number): + try: + return int(number) + except: + return False diff --git a/utils/apparmor/yasti.py b/utils/apparmor/yasti.py new file mode 100644 index 000000000..180e7152a --- /dev/null +++ b/utils/apparmor/yasti.py @@ -0,0 +1,106 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import re +import sys +try: + import ycp +except ImportError: + # ycp isn't found everywhere. + ycp = None + +from apparmor.common import error, DebugLogger + +# Set up UI logger for separate messages from YaST module +debug_logger = DebugLogger('YaST') + + +def setup_yast(): + # To-Do + pass + +def shutdown_yast(): + # To-Do + pass + +def yastLog(text): + ycp.y2milestone(text) + +def SendDataToYast(data): + debug_logger.info('SendDataToYast: Waiting for YCP command') + for line in sys.stdin: + ycommand, ypath, yargument = ParseCommand(line) + if ycommand and ycommand == 'Read': + debug_logger.info('SendDataToYast: Sending--%s' % data) + ycp.Return(data) + return True + else: + debug_logger.info('SendDataToYast: Expected \'Read\' but got-- %s' % line) + error('SendDataToYast: didn\'t receive YCP command before connection died') + +def GetDataFromYast(): + debug_logger.inf('GetDataFromYast: Waiting for YCP command') + for line in sys.stdin: + debug_logger.info('GetDataFromYast: YCP: %s' % line) + ycommand, ypath, yarg = ParseCommand(line) + debug_logger.info('GetDataFromYast: Recieved--\n%s' % yarg) + if ycommand and ycommand == 'Write': + ycp.Return('true') + return ypath, yarg + else: + debug_logger.info('GetDataFromYast: Expected Write but got-- %s' % line) + error('GetDataFromYast: didn\'t receive YCP command before connection died') + +def ParseCommand(commands): + term = ParseTerm(commands) + if term: + command = term[0] + term = term[1:] + else: + command = '' + path = '' + pathref = None + if term: + pathref = term[0] + term = term[1:] + if pathref: + if pathref.strip(): + path = pathref.strip() + elif command != 'result': + ycp.y2error('The first arguement is not a path. (%s)' % pathref) + argument = None + if term: + argument = term[0] + if len(term) > 1: + ycp.y2warning('Superfluous command arguments ignored') + return (command, path, argument) + +def ParseTerm(inp): + regex_term = re.compile('^\s*`?(\w*)\s*') + term = regex_term.search(inp) + ret = [] + symbol = None + if term: + symbol = term.groups()[0] + else: + ycp.y2error('No term symbol') + ret.append(symbol) + inp = regex_term.sub('', inp) + if not inp.startswith('('): + ycp.y2error('No term parantheses') + argref, err, rest = ycp.ParseYcpTermBody(inp) + if err: + ycp.y2error('%s (%s)' % (err, rest)) + else: + ret += argref + return ret diff --git a/utils/easyprof/policygroups/networking b/utils/easyprof/policygroups/networking deleted file mode 100644 index c60a4ede2..000000000 --- a/utils/easyprof/policygroups/networking +++ /dev/null @@ -1,2 +0,0 @@ -# Policygroup to allow networking -#include diff --git a/utils/easyprof/templates/default b/utils/easyprof/templates/default index c6d0be497..d19483f21 100644 --- a/utils/easyprof/templates/default +++ b/utils/easyprof/templates/default @@ -13,7 +13,7 @@ ###VAR### -###BINARY### { +###PROFILEATTACH### { #include ###ABSTRACTIONS### diff --git a/utils/easyprof/templates/sandbox b/utils/easyprof/templates/sandbox index acc81f97c..80dfbfc13 100644 --- a/utils/easyprof/templates/sandbox +++ b/utils/easyprof/templates/sandbox @@ -13,7 +13,7 @@ ###VAR### -###BINARY### { +###PROFILEATTACH### { #include / r, /**/ r, diff --git a/utils/easyprof/templates/sandbox-x b/utils/easyprof/templates/sandbox-x index 077cb6049..45fe09830 100644 --- a/utils/easyprof/templates/sandbox-x +++ b/utils/easyprof/templates/sandbox-x @@ -13,7 +13,7 @@ ###VAR### -###BINARY### { +###PROFILEATTACH### { #include #include #include diff --git a/utils/easyprof/templates/user-application b/utils/easyprof/templates/user-application index 766da425e..dd5f3266f 100644 --- a/utils/easyprof/templates/user-application +++ b/utils/easyprof/templates/user-application @@ -16,7 +16,7 @@ ###VAR### -###BINARY### { +###PROFILEATTACH### { #include ###ABSTRACTIONS### diff --git a/utils/logprof.conf b/utils/logprof.conf index b7764ad54..ea6636e7a 100644 --- a/utils/logprof.conf +++ b/utils/logprof.conf @@ -43,14 +43,20 @@ [qualifiers] # things will be painfully broken if bash has a profile /bin/bash = icnu - /bin/ksh = icnu - /bin/dash = icnu + /usr/bin/bash = icnu + /bin/ksh = icnu + /usr/bin/ksh = icnu + /bin/dash = icnu + /usr/bin/dash = icnu # these programs can't function if they're confined /bin/mount = u + /usr/bin/mount = u /etc/init.d/subdomain = u /sbin/cardmgr = u + /usr/sbin/cardmgr = u /sbin/subdomain_parser = u + /usr/sbin/subdomain_parser = u /usr/sbin/genprof = u /usr/sbin/logprof = u /usr/lib/YaST2/servers_non_y2/ag_genprof = u @@ -58,24 +64,43 @@ # these ones shouln't have their own profiles /bin/awk = icn + /usr/bin/awk = icn /bin/cat = icn + /usr/bin/cat = icn /bin/chmod = icn + /usr/bin/chmod = icn /bin/chown = icn + /usr/bin/chown = icn /bin/cp = icn + /usr/bin/cp = icn /bin/gawk = icn + /usr/bin/gawk = icn /bin/grep = icn + /usr/bin/grep = icn /bin/gunzip = icn + /usr/bin/gunzip = icn /bin/gzip = icn + /usr/bin/gzip = icn /bin/kill = icn + /usr/bin/kill = icn /bin/ln = icn + /usr/bin/ln = icn /bin/ls = icn + /usr/bin/ls = icn /bin/mkdir = icn + /usr/bin/mkdir = icn /bin/mv = icn + /usr/bin/mv = icn /bin/readlink = icn + /usr/bin/readlink = icn /bin/rm = icn + /usr/bin/rm = icn /bin/sed = icn + /usr/bin/sed = icn /bin/touch = icn + /usr/bin/touch = icn /sbin/killall5 = icn + /usr/sbin/killall5 = icn /usr/bin/find = icn /usr/bin/killall = icn /usr/bin/nice = icn diff --git a/utils/po/Makefile b/utils/po/Makefile index 974d57ce3..0c5527b8c 100644 --- a/utils/po/Makefile +++ b/utils/po/Makefile @@ -24,4 +24,4 @@ include ../common/Make-po.rules ../common/Make-po.rules: make -C .. common/Make.rules -XGETTEXT_ARGS+=--language=perl +XGETTEXT_ARGS+=--language=perl --language=python diff --git a/utils/po/README b/utils/po/README new file mode 100644 index 000000000..eade5e817 --- /dev/null +++ b/utils/po/README @@ -0,0 +1,10 @@ +GENERATING TRANSLATION MESSAGES + +To generate the messages.pot file: + +Run the following command in Translate. +python pygettext.py ../apparmor/*.py ../Tools/aa* + +It will generate the messages.pot file in the Translate directory. + +You might need to provide the full path to pygettext.py from your python installation. It will typically be in the /path/to/python/libs/Tools/i18n/pygettext.py diff --git a/utils/po/apparmor-utils.pot b/utils/po/apparmor-utils.pot index c157fe7c7..969527a20 100644 --- a/utils/po/apparmor-utils.pot +++ b/utils/po/apparmor-utils.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: apparmor@lists.ubuntu.com\n" -"POT-Creation-Date: 2013-11-13 16:44-0800\n" +"POT-Creation-Date: 2014-02-13 10:57-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,702 +17,333 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: ../aa-genprof:72 ../aa-unconfined:54 +#: ../aa-genprof:52 +msgid "Generate profile for the given program" +msgstr "" + +#: ../aa-genprof:53 ../aa-logprof:25 ../aa-cleanprof:24 ../aa-mergeprof:31 +#: ../aa-autodep:25 ../aa-audit:25 ../aa-complain:24 ../aa-enforce:24 +#: ../aa-disable:24 +msgid "path to profiles" +msgstr "" + +#: ../aa-genprof:54 ../aa-logprof:26 +msgid "path to logfile" +msgstr "" + +#: ../aa-genprof:55 +msgid "name of program to profile" +msgstr "" + +#: ../aa-genprof:65 ../aa-logprof:37 +#, python-format +msgid "The logfile %s does not exist. Please check the path" +msgstr "" + +#: ../aa-genprof:71 ../aa-logprof:43 ../aa-unconfined:34 msgid "" -"AppArmor does not appear to be started. Please enable AppArmor and try again." +"It seems AppArmor was not started. Please enable AppArmor and try again." msgstr "" -#: ../aa-genprof:86 -msgid "Please enter the program to profile: " +#: ../aa-genprof:76 +#, python-format +msgid "%s is not a directory." msgstr "" -#: ../aa-genprof:105 -#, perl-format +#: ../aa-genprof:90 +#, python-format msgid "" -"Can't find %s in the system path list. If the name of the application is " -"correct, please run 'which %s' in the other window in order to find the " -"fully-qualified path." +"Can't find %s in the system path list. If the name of the application\n" +"is correct, please run 'which %s' as a user with correct PATH\n" +"environment set up in order to find the fully-qualified path and\n" +"use the full path as parameter." msgstr "" -#: ../aa-genprof:107 ../aa-autodep:110 ../aa-audit:120 ../aa-complain:119 -#: ../aa-enforce:130 ../aa-disable:140 -#, perl-format -msgid "%s does not exist, please double-check the path." +#: ../aa-genprof:92 +#, python-format +msgid "%s does not exists, please double-check the path." msgstr "" -#: ../aa-genprof:141 +#: ../aa-genprof:120 msgid "" "\n" "Before you begin, you may wish to check if a\n" "profile already exists for the application you\n" "wish to confine. See the following wiki page for\n" -"more information:\n" -"http://wiki.apparmor.net/index.php/Profiles" +"more information:" msgstr "" -#: ../aa-genprof:143 +#: ../aa-genprof:122 msgid "" -"Please start the application to be profiled in \n" +"Please start the application to be profiled in\n" "another window and exercise its functionality now.\n" "\n" -"Once completed, select the \"Scan\" button below in \n" -"order to scan the system logs for AppArmor events. \n" +"Once completed, select the \"Scan\" option below in \n" +"order to scan the system logs for AppArmor events. \n" "\n" -"For each AppArmor event, you will be given the \n" -"opportunity to choose whether the access should be \n" +"For each AppArmor event, you will be given the \n" +"opportunity to choose whether the access should be \n" "allowed or denied." msgstr "" -#: ../aa-genprof:163 +#: ../aa-genprof:143 msgid "Profiling" msgstr "" -#: ../aa-genprof:197 -msgid "Reloaded AppArmor profiles in enforce mode." -msgstr "" - -#: ../aa-genprof:198 +#: ../aa-genprof:161 msgid "" "\n" -"Please consider contributing your new profile! See\n" -"the following wiki page for more information:\n" -"http://wiki.apparmor.net/index.php/Profiles\n" +"Reloaded AppArmor profiles in enforce mode." msgstr "" -#: ../aa-genprof:199 -#, perl-format +#: ../aa-genprof:162 +msgid "" +"\n" +"Please consider contributing your new profile!\n" +"See the following wiki page for more information:" +msgstr "" + +#: ../aa-genprof:163 +#, python-format msgid "Finished generating profile for %s." msgstr "" -#: ../aa-genprof:203 -#, perl-format -msgid "" -"usage: %s [ -d /path/to/profiles ] [ -f /path/to/logfile ] [ program to " -"profile ]" +#: ../aa-logprof:24 +msgid "Process log entries to generate profiles" msgstr "" -#: ../aa-logprof:69 -#, perl-format -msgid "" -"usage: %s [ -d /path/to/profiles ] [ -f /path/to/logfile ] [ -m \"mark in " -"log to start processing after\"" +#: ../aa-logprof:27 +msgid "mark in the log to start processing after" msgstr "" -#: ../aa-autodep:61 -#, perl-format -msgid "Can't find AppArmor profiles in %s." +#: ../aa-cleanprof:23 +msgid "Cleanup the profiles for the given programs" msgstr "" -#: ../aa-autodep:69 -msgid "Please enter the program to create a profile for: " +#: ../aa-cleanprof:25 ../aa-autodep:26 ../aa-audit:27 ../aa-complain:26 +#: ../aa-enforce:26 ../aa-disable:26 +msgid "name of program" msgstr "" -#: ../aa-autodep:93 ../Immunix/AppArmor.pm:6339 -#, perl-format -msgid "" -"%s is currently marked as a program that should not have it's own profile. " -"Usually, programs are marked this way if creating a profile for them is " -"likely to break the rest of the system. If you know what you're doing and " -"are certain you want to create a profile for this program, edit the " -"corresponding entry in the [qualifiers] section in /etc/apparmor/logprof." -"conf." +#: ../aa-cleanprof:26 +msgid "Silently overwrite with a clean profile" msgstr "" -#: ../aa-autodep:100 -#, perl-format -msgid "Profile for %s already exists - skipping." +#: ../aa-mergeprof:27 +msgid "Perform a 3way merge on the given profiles" msgstr "" -#: ../aa-autodep:107 ../aa-audit:117 ../aa-complain:116 ../aa-enforce:127 -#: ../aa-disable:137 -#, perl-format -msgid "" -"Can't find %s in the system path list. If the name of the application is " -"correct, please run 'which %s' as a user with the correct PATH environment " -"set up in order to find the fully-qualified path." +#: ../aa-mergeprof:28 +msgid "your profile" msgstr "" -#: ../aa-audit:104 -#, perl-format -msgid "Setting %s to audit mode." +#: ../aa-mergeprof:29 +msgid "base profile" msgstr "" -#: ../aa-audit:129 -#, perl-format -msgid "usage: %s [ -d /path/to/profiles ] [ program to switch to audit mode ]" +#: ../aa-mergeprof:30 +msgid "other profile" msgstr "" -#: ../aa-complain:61 -msgid "Please enter the program to switch to complain mode: " +#: ../aa-mergeprof:32 +msgid "Automatically merge profiles, exits incase of *x conflicts" msgstr "" -#: ../aa-complain:103 ../Immunix/AppArmor.pm:621 ../Immunix/AppArmor.pm:941 -#, perl-format -msgid "Setting %s to complain mode." +#: ../aa-mergeprof:53 +msgid "The following local profiles were changed. Would you like to save them?" msgstr "" -#: ../aa-complain:128 -#, perl-format -msgid "" -"usage: %s [ -d /path/to/profiles ] [ program to switch to complain mode ]" -msgstr "" - -#: ../aa-enforce:62 -msgid "Please enter the program to switch to enforce mode: " -msgstr "" - -#: ../aa-enforce:103 ../Immunix/AppArmor.pm:634 -#, perl-format -msgid "Setting %s to enforce mode." -msgstr "" - -#: ../aa-enforce:139 -#, perl-format -msgid "" -"usage: %s [ -d /path/to/profiles ] [ program to switch to enforce mode ]" -msgstr "" - -#: ../aa-unconfined:48 -#, perl-format -msgid "Usage: %s [ --paranoid ]\n" -msgstr "" - -#: ../aa-unconfined:59 -msgid "Can't read /proc\n" -msgstr "" - -#: ../aa-unconfined:97 ../aa-unconfined:99 -msgid "not confined\n" -msgstr "" - -#: ../aa-unconfined:108 ../aa-unconfined:110 -msgid "confined by" -msgstr "" - -#: ../aa-disable:69 -msgid "Please enter the program whose profile should be disabled: " -msgstr "" - -#: ../aa-disable:113 -#, perl-format -msgid "Could not find basename for %s." -msgstr "" - -#: ../aa-disable:117 -#, perl-format -msgid "Disabling %s." -msgstr "" - -#: ../aa-disable:123 -#, perl-format -msgid "Could not create %s symlink." -msgstr "" - -#: ../aa-disable:149 -#, perl-format -msgid "usage: %s [ -d /path/to/profiles ] [ program to have profile disabled ]" -msgstr "" - -#: ../Immunix/AppArmor.pm:619 ../Immunix/AppArmor.pm:632 -#, perl-format -msgid "Can't find %s." -msgstr "" - -#: ../Immunix/AppArmor.pm:819 ../Immunix/AppArmor.pm:3326 -msgid "Connecting to repository....." -msgstr "" - -#: ../Immunix/AppArmor.pm:828 -#, perl-format -msgid "" -"WARNING: Error fetching profiles from the repository:\n" -"%s\n" -msgstr "" - -#: ../Immunix/AppArmor.pm:837 -msgid "Inactive local profile for " -msgstr "" - -#: ../Immunix/AppArmor.pm:874 ../Immunix/AppArmor.pm:1900 -#: ../Immunix/AppArmor.pm:2188 ../Immunix/AppArmor.pm:3453 -#: ../Immunix/AppArmor.pm:3486 ../Immunix/AppArmor.pm:3686 -#: ../Immunix/AppArmor.pm:3952 ../Immunix/AppArmor.pm:4004 -msgid "Profile" -msgstr "" - -#: ../Immunix/AppArmor.pm:908 -msgid "Profile submitted by" -msgstr "" - -#: ../Immunix/AppArmor.pm:949 -#, perl-format -msgid "Error activating profiles: %s\n" -msgstr "" - -#: ../Immunix/AppArmor.pm:1098 ../Immunix/AppArmor.pm:1151 -#, perl-format -msgid "" -"WARNING: Error syncronizing profiles with the repository:\n" -"%s\n" -msgstr "" - -#: ../Immunix/AppArmor.pm:1178 -msgid "New profiles" -msgstr "" - -#: ../Immunix/AppArmor.pm:1180 -msgid "" -"Please choose the newly created profiles that you would like\n" -"to store in the repository" -msgstr "" - -#: ../Immunix/AppArmor.pm:1187 -msgid "Submit newly created profiles to the repository" -msgstr "" - -#: ../Immunix/AppArmor.pm:1189 -msgid "Would you like to upload the newly created profiles?" -msgstr "" - -#: ../Immunix/AppArmor.pm:1202 -msgid "" -"Select which of the changed profiles you would like to upload\n" -"to the repository" -msgstr "" - -#: ../Immunix/AppArmor.pm:1204 -msgid "Changed profiles" -msgstr "" - -#: ../Immunix/AppArmor.pm:1210 -msgid "Submit changed profiles to the repository" -msgstr "" - -#: ../Immunix/AppArmor.pm:1212 -msgid "" -"The following profiles from the repository were changed.\n" -"Would you like to upload your changes?" -msgstr "" - -#: ../Immunix/AppArmor.pm:1279 ../Immunix/AppArmor.pm:1359 -#, perl-format -msgid "" -"WARNING: An error occured while uploading the profile %s\n" -"%s\n" -msgstr "" - -#: ../Immunix/AppArmor.pm:1284 -msgid "Uploaded changes to repository." -msgstr "" - -#: ../Immunix/AppArmor.pm:1306 ../Immunix/AppArmor.pm:3185 -#: ../Immunix/AppArmor.pm:3215 -msgid "Repository" -msgstr "" - -#: ../Immunix/AppArmor.pm:1333 -msgid "Changelog Entry: " -msgstr "" - -#: ../Immunix/AppArmor.pm:1354 -#, perl-format -msgid "Uploaded %s to repository." -msgstr "" - -#: ../Immunix/AppArmor.pm:1365 -msgid "" -"Repository Error\n" -"Registration or Signin was unsuccessful. User login\n" -"information is required to upload profiles to the\n" -"repository. These changes have not been sent.\n" -msgstr "" - -#: ../Immunix/AppArmor.pm:1422 ../Immunix/AppArmor.pm:1462 -msgid "(Y)es" -msgstr "" - -#: ../Immunix/AppArmor.pm:1423 ../Immunix/AppArmor.pm:1463 -msgid "(N)o" -msgstr "" - -#: ../Immunix/AppArmor.pm:1426 ../Immunix/AppArmor.pm:1467 -msgid "Invalid hotkey for" -msgstr "" - -#: ../Immunix/AppArmor.pm:1464 -msgid "(C)ancel" -msgstr "" - -#: ../Immunix/AppArmor.pm:1789 -msgid "Are you sure you want to abandon this set of profile changes and exit?" -msgstr "" - -#: ../Immunix/AppArmor.pm:1791 -msgid "Abandoning all changes." -msgstr "" - -#: ../Immunix/AppArmor.pm:1902 -msgid "Default Hat" -msgstr "" - -#: ../Immunix/AppArmor.pm:1904 -msgid "Requested Hat" -msgstr "" - -#: ../Immunix/AppArmor.pm:2190 -msgid "Program" -msgstr "" - -#: ../Immunix/AppArmor.pm:2195 -msgid "Execute" -msgstr "" - -#: ../Immunix/AppArmor.pm:2196 ../Immunix/AppArmor.pm:3455 -#: ../Immunix/AppArmor.pm:3488 ../Immunix/AppArmor.pm:3741 -msgid "Severity" -msgstr "" - -#: ../Immunix/AppArmor.pm:2241 -msgid "Enter profile name to transition to: " -msgstr "" - -#: ../Immunix/AppArmor.pm:2249 -msgid "" -"Should AppArmor sanitize the environment when\n" -"switching profiles?\n" -"\n" -"Sanitizing the environment is more secure,\n" -"but some applications depend on the presence\n" -"of LD_PRELOAD or LD_LIBRARY_PATH." -msgstr "" - -#: ../Immunix/AppArmor.pm:2251 -msgid "" -"Should AppArmor sanitize the environment when\n" -"switching profiles?\n" -"\n" -"Sanitizing the environment is more secure,\n" -"but this application appears to use LD_PRELOAD\n" -"or LD_LIBRARY_PATH and clearing these could\n" -"cause functionality problems." -msgstr "" - -#: ../Immunix/AppArmor.pm:2260 -#, perl-format -msgid "" -"Launching processes in an unconfined state is a very\n" -"dangerous operation and can cause serious security holes.\n" -"\n" -"Are you absolutely certain you wish to remove all\n" -"AppArmor protection when executing %s?" -msgstr "" - -#: ../Immunix/AppArmor.pm:2262 -msgid "" -"Should AppArmor sanitize the environment when\n" -"running this program unconfined?\n" -"\n" -"Not sanitizing the environment when unconfining\n" -"a program opens up significant security holes\n" -"and should be avoided if at all possible." -msgstr "" - -#: ../Immunix/AppArmor.pm:2352 -#, perl-format -msgid "A profile for %s does not exist. Create one?" -msgstr "" - -#: ../Immunix/AppArmor.pm:2379 -#, perl-format -msgid "A local profile for %s does not exist. Create one?" -msgstr "" - -#: ../Immunix/AppArmor.pm:2584 ../Immunix/AppArmor.pm:6733 -#: ../Immunix/AppArmor.pm:6738 -#, perl-format -msgid "Log contains unknown mode %s." -msgstr "" - -#: ../Immunix/AppArmor.pm:3068 -msgid "" -"An updated version of this profile has been found in the profile " -"repository. Would you like to use it?" -msgstr "" - -#: ../Immunix/AppArmor.pm:3098 -#, perl-format -msgid "Updated profile %s to revision %s." -msgstr "" - -#: ../Immunix/AppArmor.pm:3105 -msgid "Error parsing repository profile." -msgstr "" - -#: ../Immunix/AppArmor.pm:3141 -msgid "Create New User?" -msgstr "" - -#: ../Immunix/AppArmor.pm:3142 -msgid "Username: " -msgstr "" - -#: ../Immunix/AppArmor.pm:3143 -msgid "Password: " -msgstr "" - -#: ../Immunix/AppArmor.pm:3144 -msgid "Email Addr: " -msgstr "" - -#: ../Immunix/AppArmor.pm:3146 -msgid "Save Configuration? " -msgstr "" - -#: ../Immunix/AppArmor.pm:3155 -msgid "The Profile Repository server returned the following error:" -msgstr "" - -#: ../Immunix/AppArmor.pm:3157 -msgid "Please re-enter registration information or contact the administrator." -msgstr "" - -#: ../Immunix/AppArmor.pm:3158 -msgid "Login Error\n" -msgstr "" - -#: ../Immunix/AppArmor.pm:3165 -msgid "" -"Login failure\n" -" Please check username and password and try again." -msgstr "" - -#: ../Immunix/AppArmor.pm:3187 -msgid "" -"Would you like to enable access to the\n" -"profile repository?" -msgstr "" - -#: ../Immunix/AppArmor.pm:3218 -msgid "" -"Would you like to upload newly created and changed profiles to\n" -" the profile repository?" -msgstr "" - -#: ../Immunix/AppArmor.pm:3337 -#, perl-format -msgid "" -"WARNING: Profile update check failed\n" -"Error Detail:\n" -"%s" -msgstr "" - -#: ../Immunix/AppArmor.pm:3351 -msgid "Change mode modifiers" -msgstr "" - -#: ../Immunix/AppArmor.pm:3395 -msgid "Complain-mode changes:" -msgstr "" - -#: ../Immunix/AppArmor.pm:3397 -msgid "Enforce-mode changes:" -msgstr "" - -#: ../Immunix/AppArmor.pm:3403 -#, perl-format -msgid "Invalid mode found: %s" -msgstr "" - -#: ../Immunix/AppArmor.pm:3454 ../Immunix/AppArmor.pm:3487 -msgid "Capability" -msgstr "" - -#: ../Immunix/AppArmor.pm:3507 ../Immunix/AppArmor.pm:3781 -#: ../Immunix/AppArmor.pm:4028 -#, perl-format -msgid "Adding #include <%s> to profile." -msgstr "" - -#: ../Immunix/AppArmor.pm:3510 ../Immunix/AppArmor.pm:3782 -#: ../Immunix/AppArmor.pm:3822 ../Immunix/AppArmor.pm:4032 -#, perl-format -msgid "Deleted %s previous matching profile entries." -msgstr "" - -#: ../Immunix/AppArmor.pm:3521 -#, perl-format -msgid "Adding capability %s to profile." -msgstr "" - -#: ../Immunix/AppArmor.pm:3526 -#, perl-format -msgid "Denying capability %s to profile." -msgstr "" - -#: ../Immunix/AppArmor.pm:3687 +#: ../aa-mergeprof:131 ../aa-mergeprof:413 msgid "Path" msgstr "" -#: ../Immunix/AppArmor.pm:3698 ../Immunix/AppArmor.pm:3730 +#: ../aa-mergeprof:132 +msgid "Select the appropriate mode" +msgstr "" + +#: ../aa-mergeprof:149 +msgid "Unknown selection" +msgstr "" + +#: ../aa-mergeprof:166 ../aa-mergeprof:192 +msgid "File includes" +msgstr "" + +#: ../aa-mergeprof:166 ../aa-mergeprof:192 +msgid "Select the ones you wish to add" +msgstr "" + +#: ../aa-mergeprof:178 ../aa-mergeprof:205 +#, python-format +msgid "Adding %s to the file." +msgstr "" + +#: ../aa-mergeprof:182 +msgid "unknown" +msgstr "" + +#: ../aa-mergeprof:207 ../aa-mergeprof:258 ../aa-mergeprof:499 +#: ../aa-mergeprof:541 ../aa-mergeprof:658 +#, python-format +msgid "Deleted %s previous matching profile entries." +msgstr "" + +#: ../aa-mergeprof:227 ../aa-mergeprof:412 ../aa-mergeprof:612 +#: ../aa-mergeprof:639 +msgid "Profile" +msgstr "" + +#: ../aa-mergeprof:228 +msgid "Capability" +msgstr "" + +#: ../aa-mergeprof:229 ../aa-mergeprof:463 +msgid "Severity" +msgstr "" + +#: ../aa-mergeprof:256 ../aa-mergeprof:497 +#, python-format +msgid "Adding %s to profile." +msgstr "" + +#: ../aa-mergeprof:265 +#, python-format +msgid "Adding capability %s to profile." +msgstr "" + +#: ../aa-mergeprof:272 +#, python-format +msgid "Denying capability %s to profile." +msgstr "" + +#: ../aa-mergeprof:422 ../aa-mergeprof:453 msgid "(owner permissions off)" msgstr "" -#: ../Immunix/AppArmor.pm:3704 +#: ../aa-mergeprof:427 msgid "(force new perms to owner)" msgstr "" -#: ../Immunix/AppArmor.pm:3707 +#: ../aa-mergeprof:430 msgid "(force all rule perms to owner)" msgstr "" -#: ../Immunix/AppArmor.pm:3719 +#: ../aa-mergeprof:442 msgid "Old Mode" msgstr "" -#: ../Immunix/AppArmor.pm:3720 +#: ../aa-mergeprof:443 msgid "New Mode" msgstr "" -#: ../Immunix/AppArmor.pm:3736 +#: ../aa-mergeprof:458 msgid "(force perms to owner)" msgstr "" -#: ../Immunix/AppArmor.pm:3739 +#: ../aa-mergeprof:461 msgid "Mode" msgstr "" -#: ../Immunix/AppArmor.pm:3821 -#, perl-format -msgid "Adding %s %s to profile." +#: ../aa-mergeprof:539 +#, python-format +msgid "Adding %s %s to profile" msgstr "" -#: ../Immunix/AppArmor.pm:3837 +#: ../aa-mergeprof:557 msgid "Enter new path: " msgstr "" -#: ../Immunix/AppArmor.pm:3840 -msgid "The specified path does not match this log entry:" -msgstr "" - -#: ../Immunix/AppArmor.pm:3841 -msgid "Log Entry" -msgstr "" - -#: ../Immunix/AppArmor.pm:3842 -msgid "Entered Path" -msgstr "" - -#: ../Immunix/AppArmor.pm:3843 -msgid "Do you really want to use this path?" -msgstr "" - -#: ../Immunix/AppArmor.pm:3955 ../Immunix/AppArmor.pm:4007 +#: ../aa-mergeprof:613 ../aa-mergeprof:640 msgid "Network Family" msgstr "" -#: ../Immunix/AppArmor.pm:3958 ../Immunix/AppArmor.pm:4010 +#: ../aa-mergeprof:614 ../aa-mergeprof:641 msgid "Socket Type" msgstr "" -#: ../Immunix/AppArmor.pm:4058 -#, perl-format +#: ../aa-mergeprof:656 +#, python-format +msgid "Adding %s to profile" +msgstr "" + +#: ../aa-mergeprof:666 +#, python-format msgid "Adding network access %s %s to profile." msgstr "" -#: ../Immunix/AppArmor.pm:4077 -#, perl-format -msgid "Denying network access %s %s to profile." +#: ../aa-mergeprof:672 +#, python-format +msgid "Denying network access %s %s to profile" msgstr "" -#: ../Immunix/AppArmor.pm:4285 -#, perl-format -msgid "Reading log entries from %s." +#: ../aa-autodep:23 +msgid "Generate a basic AppArmor profile by guessing requirements" msgstr "" -#: ../Immunix/AppArmor.pm:4286 -#, perl-format -msgid "Updating AppArmor profiles in %s." +#: ../aa-autodep:24 +msgid "overwrite existing profile" msgstr "" -#: ../Immunix/AppArmor.pm:4290 -msgid "unknown\n" +#: ../aa-audit:24 +msgid "Switch the given programs to audit mode" msgstr "" -#: ../Immunix/AppArmor.pm:4324 -msgid "" -"The profile analyzer has completed processing the log files.\n" -"\n" -"All updated profiles will be reloaded" +#: ../aa-audit:26 +msgid "remove audit mode" msgstr "" -#: ../Immunix/AppArmor.pm:4330 -msgid "No unhandled AppArmor events were found in the system log." +#: ../aa-audit:28 +msgid "Show full trace" msgstr "" -#: ../Immunix/AppArmor.pm:4391 -msgid "" -"Select which profile changes you would like to save to the\n" -"local profile set" +#: ../aa-complain:23 +msgid "Switch the given program to complain mode" msgstr "" -#: ../Immunix/AppArmor.pm:4392 -msgid "Local profile changes" +#: ../aa-complain:25 +msgid "remove complain mode" msgstr "" -#: ../Immunix/AppArmor.pm:4419 -msgid "" -"The following local profiles were changed. Would you like to save them?" +#: ../aa-enforce:23 +msgid "Switch the given program to enforce mode" msgstr "" -#: ../Immunix/AppArmor.pm:4516 -msgid "Profile Changes" +#: ../aa-enforce:25 +msgid "switch to complain mode" msgstr "" -#: ../Immunix/AppArmor.pm:5139 ../Immunix/AppArmor.pm:5155 -#: ../Immunix/AppArmor.pm:5166 ../Immunix/AppArmor.pm:5174 -#: ../Immunix/AppArmor.pm:5195 ../Immunix/AppArmor.pm:5215 -#: ../Immunix/AppArmor.pm:5224 ../Immunix/AppArmor.pm:5256 -#: ../Immunix/AppArmor.pm:5334 ../Immunix/AppArmor.pm:5382 -#, perl-format -msgid "%s contains syntax errors." +#: ../aa-disable:23 +msgid "Disable the profile for the given programs" msgstr "" -#: ../Immunix/AppArmor.pm:5275 -#, perl-format -msgid "Profile %s contains invalid regexp %s." +#: ../aa-disable:25 +msgid "enable the profile for the given programs" msgstr "" -#: ../Immunix/AppArmor.pm:5280 -#, perl-format -msgid "Profile %s contains invalid mode %s." +#: ../aa-unconfined:26 +msgid "Lists unconfined processes having tcp or udp ports" msgstr "" -#: ../Immunix/AppArmor.pm:5430 -#, perl-format -msgid "%s contains syntax errors. Line [%s]" +#: ../aa-unconfined:27 +msgid "scan all processes from /proc" msgstr "" -#: ../Immunix/AppArmor.pm:6022 -#, perl-format -msgid "Writing updated profile for %s." +#: ../aa-unconfined:79 +#, python-format +msgid "%s %s (%s) not confined\n" msgstr "" -#: ../Immunix/AppArmor.pm:6528 -msgid "Unknown command" +#: ../aa-unconfined:83 +#, python-format +msgid "%s %s %snot confined\n" msgstr "" -#: ../Immunix/AppArmor.pm:6536 -msgid "Invalid hotkey in" +#: ../aa-unconfined:88 +#, python-format +msgid "%s %s (%s) confined by '%s'\n" msgstr "" -#: ../Immunix/AppArmor.pm:6546 -msgid "Duplicate hotkey for" -msgstr "" - -#: ../Immunix/AppArmor.pm:6567 -msgid "Invalid hotkey in default item" -msgstr "" - -#: ../Immunix/AppArmor.pm:6576 -msgid "Invalid default" +#: ../aa-unconfined:92 +#, python-format +msgid "%s %s %sconfined by '%s'\n" msgstr "" diff --git a/utils/python-tools-setup.py b/utils/python-tools-setup.py index 1e56f9e1c..5cd5d3659 100644 --- a/utils/python-tools-setup.py +++ b/utils/python-tools-setup.py @@ -81,6 +81,7 @@ setup (name='apparmor', license='GPL-2', cmdclass={'install': Install}, package_dir={'apparmor': 'staging'}, + packages=['apparmor'], py_modules=['apparmor.easyprof'] ) diff --git a/utils/test/aa_test.py b/utils/test/aa_test.py new file mode 100755 index 000000000..fd6315e98 --- /dev/null +++ b/utils/test/aa_test.py @@ -0,0 +1,130 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import unittest +import sys + +sys.path.append('../') +import apparmor.aa +import apparmor.logparser + +class Test(unittest.TestCase): + + def setUp(self): + self.MODE_TEST = {'x': apparmor.aamode.AA_MAY_EXEC, + 'w': apparmor.aamode.AA_MAY_WRITE, + 'r': apparmor.aamode.AA_MAY_READ, + 'a': apparmor.aamode.AA_MAY_APPEND, + 'l': apparmor.aamode.AA_MAY_LINK, + 'k': apparmor.aamode.AA_MAY_LOCK, + 'm': apparmor.aamode.AA_EXEC_MMAP, + 'i': apparmor.aamode.AA_EXEC_INHERIT, + 'u': apparmor.aamode.AA_EXEC_UNCONFINED | apparmor.aamode.AA_EXEC_UNSAFE, + 'U': apparmor.aamode.AA_EXEC_UNCONFINED, + 'p': apparmor.aamode.AA_EXEC_PROFILE | apparmor.aamode.AA_EXEC_UNSAFE, + 'P': apparmor.aamode.AA_EXEC_PROFILE, + 'c': apparmor.aamode.AA_EXEC_CHILD | apparmor.aamode.AA_EXEC_UNSAFE, + 'C': apparmor.aamode.AA_EXEC_CHILD, + } + + def test_loadinclude(self): + apparmor.aa.loadincludes() + + def test_path_globs(self): + globs = { + '/foo/': '/*/', + '/foo': '/*', + '/b*': '/*', + '/*b': '/*', + '/*': '/*', + '/*/': '/*/', + '/*.foo/': '/*/', + '/**.foo/': '/**/', + '/foo/*/': '/**/', + '/usr/foo/*': '/usr/**', + '/usr/foo/**': '/usr/**', + '/usr/foo/bar**': '/usr/foo/**', + '/usr/foo/**bar': '/usr/foo/**', + '/usr/bin/foo**bar': '/usr/bin/**', + '/usr/foo/**/bar': '/usr/foo/**/*', + '/usr/foo/**/*': '/usr/foo/**', + '/usr/foo/*/bar': '/usr/foo/*/*', + '/usr/bin/foo*bar': '/usr/bin/*', + '/usr/bin/*foo*': '/usr/bin/*', + '/usr/foo/*/*': '/usr/foo/**', + '/usr/foo/*/**': '/usr/foo/**', + '/**': '/**', + '/**/': '/**/' + } + for path in globs.keys(): + self.assertEqual(apparmor.aa.glob_path(path), globs[path], 'Unexpected glob generated for path: %s'%path) + + def test_path_withext_globs(self): + globs = { + '/foo/bar': '/foo/bar', + '/foo/**/bar': '/foo/**/bar', + '/foo.bar': '/*.bar', + '/*.foo': '/*.foo' , + '/usr/*.bar': '/**.bar', + '/usr/**.bar': '/**.bar', + '/usr/foo**.bar': '/usr/**.bar', + '/usr/foo*.bar': '/usr/*.bar', + '/usr/fo*oo.bar': '/usr/*.bar', + '/usr/*foo*.bar': '/usr/*.bar', + '/usr/**foo.bar': '/usr/**.bar', + '/usr/*foo.bar': '/usr/*.bar', + '/usr/foo.b*': '/usr/*.b*' + } + for path in globs.keys(): + self.assertEqual(apparmor.aa.glob_path_withext(path), globs[path], 'Unexpected glob generated for path: %s'%path) + + def test_parse_event(self): + parser = apparmor.logparser.ReadLog('', '', '', '', '') + event = 'type=AVC msg=audit(1345027352.096:499): apparmor="ALLOWED" operation="rename_dest" parent=6974 profile="/usr/sbin/httpd2-prefork//vhost_foo" name=2F686F6D652F7777772F666F6F2E6261722E696E2F68747470646F63732F61707061726D6F722F696D616765732F746573742F696D61676520312E6A7067 pid=20143 comm="httpd2-prefork" requested_mask="wc" denied_mask="wc" fsuid=30 ouid=30' + parsed_event = parser.parse_event(event) + self.assertEqual(parsed_event['name'], '/home/www/foo.bar.in/httpdocs/apparmor/images/test/image 1.jpg', 'Incorrectly parsed/decoded name') + self.assertEqual(parsed_event['profile'], '/usr/sbin/httpd2-prefork//vhost_foo', 'Incorrectly parsed/decode profile name') + self.assertEqual(parsed_event['aamode'], 'PERMITTING') + self.assertEqual(parsed_event['request_mask'], set(['w', 'a', '::w', '::a'])) + #print(parsed_event) + + #event = 'type=AVC msg=audit(1322614912.304:857): apparmor="ALLOWED" operation="getattr" parent=16001 profile=74657374207370616365 name=74657374207370616365 pid=17011 comm="bash" requested_mask="r" denied_mask="r" fsuid=0 ouid=0' + #parsed_event = apparmor.aa.parse_event(event) + #print(parsed_event) + + 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' + parsed_event = parser.parse_event(event) + self.assertEqual(parsed_event['name'], '/home/foo/.bash_history', 'Incorrectly parsed/decoded name') + self.assertEqual(parsed_event['profile'], 'foo bar', 'Incorrectly parsed/decode profile name') + self.assertEqual(parsed_event['aamode'], 'PERMITTING') + self.assertEqual(parsed_event['request_mask'], set(['r', 'w', 'a','::r' , '::w', '::a'])) + #print(parsed_event) + + + def test_modes_to_string(self): + + for string in self.MODE_TEST.keys(): + mode = self.MODE_TEST[string] + self.assertEqual(apparmor.aamode.mode_to_str(mode), string, 'mode is %s and string is %s'%(mode, string)) + + def test_string_to_modes(self): + + for string in self.MODE_TEST.keys(): + mode = self.MODE_TEST[string] | apparmor.aamode.AA_OTHER(self.MODE_TEST[string]) + #print("mode: %s string: %s str_to_mode(string): %s" % (mode, string, apparmor.aamode.str_to_mode(string))) + self.assertEqual(mode, apparmor.aamode.str_to_mode(string), 'mode is %s and string is %s'%(mode, string)) + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/utils/test/cleanprof_test.in b/utils/test/cleanprof_test.in new file mode 100644 index 000000000..200652577 --- /dev/null +++ b/utils/test/cleanprof_test.in @@ -0,0 +1,19 @@ +# A simple test comment which will persist +#include + +/usr/bin/a/simple/cleanprof/test/profile { + # Just for the heck of it, this comment wont see the day of light + #include + #Below rule comes from abstractions/base + allow /usr/share/X11/locale/** r, + allow /home/*/** r, + allow /home/foo/bar r, + allow /home/foo/** w, +} + +/usr/bin/other/cleanprof/test/profile { + # This one shouldn't be affected by the processing + # However this comment will be wiped, need to change that + allow /home/*/** rw, + allow /home/foo/bar r, +} \ No newline at end of file diff --git a/utils/test/cleanprof_test.out b/utils/test/cleanprof_test.out new file mode 100644 index 000000000..db102c135 --- /dev/null +++ b/utils/test/cleanprof_test.out @@ -0,0 +1,17 @@ +#include + +# A simple test comment which will persist + + +/usr/bin/a/simple/cleanprof/test/profile { + #include + + /home/*/** r, + /home/foo/** w, + +} +/usr/bin/other/cleanprof/test/profile { + /home/*/** rw, + /home/foo/bar r, + +} diff --git a/utils/test/common_test.py b/utils/test/common_test.py new file mode 100755 index 000000000..1bb9c66bb --- /dev/null +++ b/utils/test/common_test.py @@ -0,0 +1,41 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import unittest +import re +import sys + +sys.path.append('../') +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.") + # self.assertEqual(apparmor.common.readkey().lower(), 'y', 'Error reading key from shell!') + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.test_RegexParser'] + unittest.main() diff --git a/utils/test/config_test.py b/utils/test/config_test.py new file mode 100755 index 000000000..2d5575542 --- /dev/null +++ b/utils/test/config_test.py @@ -0,0 +1,52 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import unittest +import sys + +sys.path.append('../') +import apparmor.config as config + +class Test(unittest.TestCase): + + + def test_IniConfig(self): + ini_config = config.Config('ini') + ini_config.CONF_DIR = '.' + conf = ini_config.read_config('logprof.conf') + logprof_sections = ['settings', 'repository', 'qualifiers', 'required_hats', 'defaulthat', 'globs'] + logprof_sections_options = ['profiledir', 'inactive_profiledir', 'logfiles', 'parser', 'ldd', 'logger', 'default_owner_prompt', 'custom_includes'] + logprof_settings_parser = '/sbin/apparmor_parser /sbin/subdomain_parser' + + self.assertEqual(conf.sections(), logprof_sections) + self.assertEqual(conf.options('settings'), logprof_sections_options) + self.assertEqual(conf['settings']['parser'], logprof_settings_parser) + + def test_ShellConfig(self): + shell_config = config.Config('shell') + shell_config.CONF_DIR = '.' + conf = shell_config.read_config('easyprof.conf') + easyprof_sections = ['POLICYGROUPS_DIR', 'TEMPLATES_DIR'] + easyprof_Policygroup = '/usr/share/apparmor/easyprof/policygroups' + easyprof_Templates = '/usr/share/apparmor/easyprof/templates' + + self.assertEqual(sorted(list(conf[''].keys())), sorted(easyprof_sections)) + self.assertEqual(conf['']['POLICYGROUPS_DIR'], easyprof_Policygroup) + self.assertEqual(conf['']['TEMPLATES_DIR'], easyprof_Templates) + + + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testConfig'] + unittest.main() diff --git a/utils/test/logprof.conf b/utils/test/logprof.conf new file mode 100644 index 000000000..e073eb70a --- /dev/null +++ b/utils/test/logprof.conf @@ -0,0 +1,131 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2004-2006 Novell/SUSE +# +# 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. +# +# ------------------------------------------------------------------ + +[settings] + profiledir = /etc/apparmor.d /etc/subdomain.d + inactive_profiledir = /usr/share/doc/apparmor-profiles/extras + logfiles = /var/log/audit/audit.log /var/log/syslog /var/log/messages + + parser = /sbin/apparmor_parser /sbin/subdomain_parser + ldd = /usr/bin/ldd + logger = /bin/logger /usr/bin/logger + + # customize how file ownership permissions are presented + # 0 - off + # 1 - default of what ever mode the log reported + # 2 - force the new permissions to be user + # 3 - force all perms on the rule to be user + default_owner_prompt = 1 + + # custom directory locations to look for #includes + # + # each name should be a valid directory containing possible #include + # candidate files under the profile dir which by default is /etc/apparmor.d. + # + # So an entry of my-includes will allow /etc/apparmor.d/my-includes to + # be used by the yast UI and profiling tools as a source of #include + # files. + custom_includes = + + +[repository] + distro = ubuntu-intrepid + url = http://apparmor.test.opensuse.org/backend/api + preferred_user = ubuntu + +[qualifiers] + # things will be painfully broken if bash has a profile + /bin/bash = icnu + /bin/ksh = icnu + /bin/dash = icnu + + # these programs can't function if they're confined + /bin/mount = u + /etc/init.d/subdomain = u + /sbin/cardmgr = u + /sbin/subdomain_parser = u + /usr/sbin/genprof = u + /usr/sbin/logprof = u + /usr/lib/YaST2/servers_non_y2/ag_genprof = u + /usr/lib/YaST2/servers_non_y2/ag_logprof = u + + # these ones shouln't have their own profiles + /bin/awk = icn + /bin/cat = icn + /bin/chmod = icn + /bin/chown = icn + /bin/cp = icn + /bin/gawk = icn + /bin/grep = icn + /bin/gunzip = icn + /bin/gzip = icn + /bin/kill = icn + /bin/ln = icn + /bin/ls = icn + /bin/mkdir = icn + /bin/mv = icn + /bin/readlink = icn + /bin/rm = icn + /bin/sed = icn + /bin/touch = icn + /sbin/killall5 = icn + /usr/bin/find = icn + /usr/bin/killall = icn + /usr/bin/nice = icn + /usr/bin/perl = icn + /usr/bin/tr = icn + +[required_hats] + ^.+/apache(|2|2-prefork)$ = DEFAULT_URI HANDLING_UNTRUSTED_INPUT + ^.+/httpd(|2|2-prefork)$ = DEFAULT_URI HANDLING_UNTRUSTED_INPUT + +[defaulthat] + ^.+/apache(|2|2-prefork)$ = DEFAULT_URI + ^.+/httpd(|2|2-prefork)$ = DEFAULT_URI + +[globs] + # /foo/bar/lib/libbaz.so -> /foo/bar/lib/lib* + /lib/lib[^\/]+so[^\/]*$ = /lib/lib*so* + + # strip kernel version numbers from kernel module accesses + ^/lib/modules/[^\/]+\/ = /lib/modules/*/ + + # strip pid numbers from /proc accesses + ^/proc/\d+/ = /proc/*/ + + # if it looks like a home directory, glob out the username + ^/home/[^\/]+ = /home/* + + # if they use any perl modules, grant access to all + ^/usr/lib/perl5/.+$ = /usr/lib/perl5/** + + # locale foo + ^/usr/lib/locale/.+$ = /usr/lib/locale/** + ^/usr/share/locale/.+$ = /usr/share/locale/** + + # timezone fun + ^/usr/share/zoneinfo/.+$ = /usr/share/zoneinfo/** + + # /foobar/fonts/baz -> /foobar/fonts/** + /fonts/.+$ = /fonts/** + + # turn /foo/bar/baz.8907234 into /foo/bar/baz.* + # BUGBUG - this one looked weird because it would suggest a glob for + # BUGBUG - libfoo.so.5.6.0 that looks like libfoo.so.5.6.* + # \.\d+$ = .* + + # some various /etc/security poo -- dunno about these ones... + ^/etc/security/_[^\/]+$ = /etc/security/* + ^/lib/security/pam_filter/[^\/]+$ = /lib/security/pam_filter/* + ^/lib/security/pam_[^\/]+\.so$ = /lib/security/pam_*.so + + ^/etc/pam.d/[^\/]+$ = /etc/pam.d/* + ^/etc/profile.d/[^\/]+\.sh$ = /etc/profile.d/*.sh + diff --git a/utils/test/minitools_test.py b/utils/test/minitools_test.py new file mode 100755 index 000000000..2699ffe5a --- /dev/null +++ b/utils/test/minitools_test.py @@ -0,0 +1,153 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import atexit +import os +import shutil +import subprocess +import sys +import unittest +import filecmp + +import apparmor.aa as apparmor + +# Path for the program +test_path = '/usr/sbin/ntpd' +# Path for the target file containing profile +local_profilename = './profiles/usr.sbin.ntpd' + +python_interpreter = 'python' +if sys.version_info >= (3, 0): + python_interpreter = 'python3' + +class Test(unittest.TestCase): + + def test_audit(self): + #Set ntpd profile to audit mode and check if it was correctly set + str(subprocess.check_output('%s ./../Tools/aa-audit -d ./profiles %s'%(python_interpreter, test_path), shell=True)) + + self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'audit', 'Audit flag could not be set in profile %s'%local_profilename) + + #Remove audit mode from ntpd profile and check if it was correctly removed + subprocess.check_output('%s ./../Tools/aa-audit -d ./profiles -r %s'%(python_interpreter, test_path), shell=True) + + self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), None, 'Complain flag could not be removed in profile %s'%local_profilename) + + + def test_complain(self): + #Set ntpd profile to complain mode and check if it was correctly set + subprocess.check_output('%s ./../Tools/aa-complain -d ./profiles %s'%(python_interpreter, test_path), shell=True) + + self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), True, 'Failed to create a symlink for %s in force-complain'%local_profilename) + self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'complain', 'Complain flag could not be set in profile %s'%local_profilename) + + #Set ntpd profile to enforce mode and check if it was correctly set + subprocess.check_output('%s ./../Tools/aa-complain -d ./profiles -r %s'%(python_interpreter, test_path), shell=True) + + self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from force-complain'%local_profilename) + self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from disable'%local_profilename) + self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), None, 'Complain flag could not be removed in profile %s'%local_profilename) + + # Set audit flag and then complain flag in a profile + subprocess.check_output('%s ./../Tools/aa-audit -d ./profiles %s'%(python_interpreter, test_path), shell=True) + subprocess.check_output('%s ./../Tools/aa-complain -d ./profiles %s'%(python_interpreter, test_path), shell=True) + + self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), True, 'Failed to create a symlink for %s in force-complain'%local_profilename) + self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'audit,complain', 'Complain flag could not be set in profile %s'%local_profilename) + + #Remove complain flag first i.e. set to enforce mode + subprocess.check_output('%s ./../Tools/aa-complain -d ./profiles -r %s'%(python_interpreter, test_path), shell=True) + + self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from force-complain'%local_profilename) + self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from disable'%local_profilename) + self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'audit', 'Complain flag could not be removed in profile %s'%local_profilename) + + #Remove audit flag + subprocess.check_output('%s ./../Tools/aa-audit -d ./profiles -r %s'%(python_interpreter, test_path), shell=True) + + def test_enforce(self): + #Set ntpd profile to complain mode and check if it was correctly set + subprocess.check_output('%s ./../Tools/aa-enforce -d ./profiles -r %s'%(python_interpreter, test_path), shell=True) + + self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), True, 'Failed to create a symlink for %s in force-complain'%local_profilename) + self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'complain', 'Complain flag could not be set in profile %s'%local_profilename) + + + #Set ntpd profile to enforce mode and check if it was correctly set + subprocess.check_output('%s ./../Tools/aa-enforce -d ./profiles %s'%(python_interpreter, test_path), shell=True) + + self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from force-complain'%local_profilename) + self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from disable'%local_profilename) + self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), None, 'Complain flag could not be removed in profile %s'%local_profilename) + + + def test_disable(self): + #Disable the ntpd profile and check if it was correctly disabled + subprocess.check_output('%s ./../Tools/aa-disable -d ./profiles %s'%(python_interpreter, test_path), shell=True) + + self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), True, 'Failed to create a symlink for %s in disable'%local_profilename) + + #Enable the ntpd profile and check if it was correctly re-enabled + subprocess.check_output('%s ./../Tools/aa-disable -d ./profiles -r %s'%(python_interpreter, test_path), shell=True) + + self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), False, 'Failed to remove a symlink for %s from disable'%local_profilename) + + + def test_autodep(self): + pass + + def test_unconfined(self): + output = subprocess.check_output('%s ./../Tools/aa-unconfined'%python_interpreter, shell=True) + + output_force = subprocess.check_output('%s ./../Tools/aa-unconfined --paranoid'%python_interpreter, shell=True) + + self.assertIsNot(output, '', 'Failed to run aa-unconfined') + + self.assertIsNot(output_force, '', 'Failed to run aa-unconfined in paranoid mode') + + + def test_cleanprof(self): + input_file = 'cleanprof_test.in' + output_file = 'cleanprof_test.out' + #We position the local testfile + shutil.copy('./%s'%input_file, './profiles') + #Our silly test program whose profile we wish to clean + cleanprof_test = '/usr/bin/a/simple/cleanprof/test/profile' + + subprocess.check_output('%s ./../Tools/aa-cleanprof -d ./profiles -s %s' % (python_interpreter, cleanprof_test), shell=True) + + #Strip off the first line (#modified line) + subprocess.check_output('sed -i 1d ./profiles/%s'%(input_file), shell=True) + + self.assertEqual(filecmp.cmp('./profiles/%s'%input_file, './%s'%output_file, False), True, 'Failed to cleanup profile properly') + + +def clean_profile_dir(): + #Wipe the local profiles from the test directory + shutil.rmtree('./profiles') + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + + if os.path.exists('./profiles'): + shutil.rmtree('./profiles') + + #copy the local profiles to the test directory + #Should be the set of cleanprofile + shutil.copytree('/etc/apparmor.d', './profiles', symlinks=True) + + apparmor.profile_dir = './profiles' + + atexit.register(clean_profile_dir) + + unittest.main() diff --git a/utils/test/regex_tests.ini b/utils/test/regex_tests.ini new file mode 100644 index 000000000..99c954054 --- /dev/null +++ b/utils/test/regex_tests.ini @@ -0,0 +1,51 @@ +# ---------------------------------------------------------------------- +# 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 \ No newline at end of file diff --git a/utils/test/runtests-py2.sh b/utils/test/runtests-py2.sh new file mode 100755 index 000000000..8c9f77014 --- /dev/null +++ b/utils/test/runtests-py2.sh @@ -0,0 +1 @@ +for file in *.py ; do echo "running $file..." ; python $file; echo; done diff --git a/utils/test/runtests-py3.sh b/utils/test/runtests-py3.sh new file mode 100755 index 000000000..33645bb8b --- /dev/null +++ b/utils/test/runtests-py3.sh @@ -0,0 +1 @@ +for file in *.py ; do echo "running $file..." ; python3 $file; echo; done diff --git a/utils/test/severity.db b/utils/test/severity.db new file mode 100644 index 000000000..f15fae3c9 --- /dev/null +++ b/utils/test/severity.db @@ -0,0 +1,460 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2002-2005 Novell/SUSE +# +# 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. +# +# ------------------------------------------------------------------ + +# Allow this process to 0wn the machine: + CAP_SYS_ADMIN 10 + CAP_SYS_CHROOT 10 + CAP_SYS_MODULE 10 + CAP_SYS_PTRACE 10 + CAP_SYS_RAWIO 10 + CAP_MAC_ADMIN 10 + CAP_MAC_OVERRIDE 10 +# Allow other processes to 0wn the machine: + CAP_SETPCAP 9 + CAP_SETFCAP 9 + CAP_CHOWN 9 + CAP_FSETID 9 + CAP_MKNOD 9 + CAP_LINUX_IMMUTABLE 9 + CAP_DAC_OVERRIDE 9 + CAP_SETGID 9 + CAP_SETUID 9 + CAP_FOWNER 9 +# Denial of service, bypass audit controls, information leak + CAP_SYS_TIME 8 + CAP_NET_ADMIN 8 + CAP_SYS_RESOURCE 8 + CAP_KILL 8 + CAP_IPC_OWNER 8 + CAP_SYS_PACCT 8 + CAP_SYS_BOOT 8 + CAP_NET_BIND_SERVICE 8 + CAP_NET_RAW 8 + CAP_SYS_NICE 8 + CAP_LEASE 8 + CAP_IPC_LOCK 8 + CAP_SYS_TTY_CONFIG 8 + CAP_AUDIT_CONTROL 8 + CAP_AUDIT_WRITE 8 + CAP_SYSLOG 8 + CAP_WAKE_ALARM 8 + CAP_BLOCK_SUSPEND 8 + CAP_DAC_READ_SEARCH 7 +# unused + CAP_NET_BROADCAST 0 + +# filename r w x +# 'hard drives' are generally 4 10 0 +/**/lost+found/** 5 5 0 +/boot/** 7 10 0 +/etc/passwd* 4 8 0 +/etc/group* 4 8 0 +/etc/shadow* 7 9 0 +/etc/shadow* 7 9 0 +/home/*/.ssh/** 7 9 0 +/home/*/.gnupg/** 5 7 0 +/home/** 4 6 0 +/srv/** 4 6 0 +/proc/** 6 9 0 +/proc/sys/kernel/hotplug 2 10 0 +/proc/sys/kernel/modprobe 2 10 0 +/proc/kallsyms 7 0 0 +/sys/** 4 8 0 +/sys/power/state 2 8 0 +/sys/firmware/** 2 10 0 +/dev/pts/* 8 9 0 +/dev/ptmx 8 9 0 +/dev/pty* 8 9 0 +/dev/null 0 0 0 +/dev/adbmouse 3 8 0 +/dev/ataraid 9 10 0 +/dev/zero 0 0 0 +/dev/agpgart* 8 10 0 +/dev/aio 3 3 0 +/dev/cbd/* 5 5 0 +/dev/cciss/* 4 10 0 +/dev/capi* 4 6 0 +/dev/cfs0 4 10 0 +/dev/compaq/* 4 10 0 +/dev/cdouble* 4 8 0 +/dev/cpu** 5 5 0 +/dev/cpu**microcode 1 10 0 +/dev/double* 4 8 0 +/dev/hd* 4 10 0 +/dev/sd* 4 10 0 +/dev/ida/* 4 10 0 +/dev/input/* 4 8 0 +/dev/mapper/control 4 10 0 +/dev/*mem 8 10 0 +/dev/loop* 4 10 0 +/dev/lp* 0 4 0 +/dev/md* 4 10 0 +/dev/msr 4 10 0 +/dev/nb* 4 10 0 +/dev/ram* 8 10 0 +/dev/rd/* 4 10 0 +/dev/*random 3 1 0 +/dev/sbpcd* 4 0 0 +/dev/rtc 6 0 0 +/dev/sd* 4 10 0 +/dev/sc* 4 10 0 +/dev/sg* 4 10 0 +/dev/st* 4 10 0 +/dev/snd/* 3 8 0 +/dev/usb/mouse* 4 6 0 +/dev/usb/hid* 4 6 0 +/dev/usb/tty* 4 6 0 +/dev/tty* 8 9 0 +/dev/stderr 0 0 0 +/dev/stdin 0 0 0 +/dev/stdout 0 0 0 +/dev/ubd* 4 10 0 +/dev/usbmouse* 4 6 0 +/dev/userdma 8 10 0 +/dev/vcs* 8 9 0 +/dev/xta* 4 10 0 +/dev/zero 0 0 0 +/dev/inittcl 8 10 0 +/dev/log 5 7 0 +/etc/fstab 3 8 0 +/etc/mtab 3 5 0 +/etc/SuSEconfig/* 1 8 0 +/etc/X11/* 2 7 0 +/etc/X11/xinit/* 2 8 0 +/etc/SuSE-release 1 5 0 +/etc/issue* 1 3 0 +/etc/motd 1 3 0 +/etc/aliases.d/* 1 7 0 +/etc/cron* 1 9 0 +/etc/cups/* 2 7 0 +/etc/default/* 3 8 0 +/etc/init.d/* 1 10 0 +/etc/permissions.d/* 1 8 0 +/etc/ppp/* 2 6 0 +/etc/ppp/*secrets 8 6 0 +/etc/profile.d/* 1 8 0 +/etc/skel/* 0 7 0 +/etc/sysconfig/* 4 10 0 +/etc/xinetd.d/* 1 9 0 +/etc/termcap/* 1 4 0 +/etc/ld.so.* 1 9 0 +/etc/pam.d/* 3 9 0 +/etc/udev/* 3 9 0 +/etc/insserv.conf 3 6 0 +/etc/security/* 1 9 0 +/etc/securetty 0 7 0 +/etc/sudoers 4 9 0 +/etc/hotplug/* 2 10 0 +/etc/xinitd.conf 1 9 0 +/etc/gpm/* 2 10 0 +/etc/ssl/** 2 7 0 +/etc/shadow* 5 9 0 +/etc/bash.bashrc 1 9 0 +/etc/csh.cshrc 1 9 0 +/etc/csh.login 1 9 0 +/etc/inittab 1 10 0 +/etc/profile* 1 9 0 +/etc/shells 1 5 0 +/etc/alternatives 1 6 0 +/etc/sysctl.conf 3 7 0 +/etc/dev.d/* 1 8 0 +/etc/manpath.config 1 6 0 +/etc/permissions* 1 8 0 +/etc/evms.conf 3 8 0 +/etc/exports 3 8 0 +/etc/samba/* 5 8 0 +/etc/ssh/* 3 8 0 +/etc/ssh/ssh_host_*key 8 8 0 +/etc/krb5.conf 4 8 0 +/etc/ntp.conf 3 8 0 +/etc/auto.* 3 8 0 +/etc/postfix/* 3 7 0 +/etc/postfix/*passwd* 6 7 0 +/etc/postfix/*cert* 6 7 0 +/etc/foomatic/* 3 5 0 +/etc/printcap 3 5 0 +/etc/youservers 4 9 0 +/etc/grub.conf 7 10 0 +/etc/modules.conf 4 10 0 +/etc/resolv.conf 2 7 0 +/etc/apache2/** 3 7 0 +/etc/apache2/**ssl** 7 7 0 +/etc/subdomain.d/** 6 10 0 +/etc/apparmor.d/** 6 10 0 +/etc/apparmor/** 6 10 0 +/var/log/** 3 8 0 +/var/adm/SuSEconfig/** 3 8 0 +/var/adm/** 3 7 0 +/var/lib/rpm/** 4 8 0 +/var/run/nscd/* 3 3 0 +/var/run/.nscd_socket 3 3 0 +/usr/share/doc/** 1 1 0 +/usr/share/man/** 3 5 0 +/usr/X11/man/** 3 5 0 +/usr/share/info/** 2 4 0 +/usr/share/java/** 2 5 0 +/usr/share/locale/** 2 4 0 +/usr/share/sgml/** 2 4 0 +/usr/share/YaST2/** 3 9 0 +/usr/share/ghostscript/** 3 5 0 +/usr/share/terminfo/** 1 8 0 +/usr/share/latex2html/** 2 4 0 +/usr/share/cups/** 5 6 0 +/usr/share/susehelp/** 2 6 0 +/usr/share/susehelp/cgi-bin/** 3 7 7 +/usr/share/zoneinfo/** 2 7 0 +/usr/share/zsh/** 3 6 0 +/usr/share/vim/** 3 8 0 +/usr/share/groff/** 3 7 0 +/usr/share/vnc/** 3 8 0 +/usr/share/wallpapers/** 2 4 0 +/usr/X11** 3 8 5 +/usr/X11*/bin/XFree86 3 8 8 +/usr/X11*/bin/Xorg 3 8 8 +/usr/X11*/bin/sux 3 8 8 +/usr/X11*/bin/xconsole 3 7 7 +/usr/X11*/bin/xhost 3 7 7 +/usr/X11*/bin/xauth 3 7 7 +/usr/X11*/bin/ethereal 3 6 8 +/usr/lib/ooo-** 3 6 5 +/usr/lib/lsb/** 2 8 8 +/usr/lib/pt_chwon 2 8 5 +/usr/lib/tcl** 2 5 3 +/usr/lib/lib*so* 3 8 4 +/usr/lib/iptables/* 2 8 2 +/usr/lib/perl5/** 4 10 6 +/usr/lib/gconv/* 4 7 4 +/usr/lib/locale/** 4 8 0 +/usr/lib/jvm/** 5 7 5 +/usr/lib/sasl*/** 5 8 4 +/usr/lib/jvm-exports/** 5 7 5 +/usr/lib/jvm-private/** 5 7 5 +/usr/lib/python*/** 5 7 5 +/usr/lib/libkrb5* 4 8 4 +/usr/lib/postfix/* 4 7 4 +/usr/lib/rpm/** 4 8 6 +/usr/lib/rpm/gnupg/** 4 9 0 +/usr/lib/apache2** 4 7 4 +/usr/lib/mailman/** 4 6 4 +/usr/bin/ldd 1 7 4 +/usr/bin/netcat 5 7 8 +/usr/bin/clear 2 6 3 +/usr/bin/reset 2 6 3 +/usr/bin/tput 2 6 3 +/usr/bin/tset 2 6 3 +/usr/bin/file 2 6 3 +/usr/bin/ftp 3 7 5 +/usr/bin/busybox 4 8 6 +/usr/bin/rbash 4 8 5 +/usr/bin/screen 3 6 5 +/usr/bin/getfacl 3 7 4 +/usr/bin/setfacl 3 7 9 +/usr/bin/*awk* 3 7 7 +/usr/bin/sudo 2 9 10 +/usr/bin/lsattr 2 6 5 +/usr/bin/chattr 2 7 8 +/usr/bin/sed 3 7 6 +/usr/bin/grep 2 7 2 +/usr/bin/chroot 2 6 10 +/usr/bin/dircolors 2 9 3 +/usr/bin/cut 2 7 2 +/usr/bin/du 2 7 3 +/usr/bin/env 2 7 2 +/usr/bin/head 2 7 2 +/usr/bin/tail 2 7 2 +/usr/bin/install 2 8 4 +/usr/bin/link 2 6 4 +/usr/bin/logname 2 6 2 +/usr/bin/md5sum 2 8 3 +/usr/bin/mkfifo 2 6 10 +/usr/bin/nice 2 7 7 +/usr/bin/nohup 2 7 7 +/usr/bin/printf 2 7 1 +/usr/bin/readlink 2 7 3 +/usr/bin/seq 2 7 1 +/usr/bin/sha1sum 2 8 3 +/usr/bin/shred 2 7 3 +/usr/bin/sort 2 7 3 +/usr/bin/split 2 7 3 +/usr/bin/stat 2 7 4 +/usr/bin/sum 2 8 3 +/usr/bin/tac 2 7 3 +/usr/bin/tail 3 8 4 +/usr/bin/tee 2 7 3 +/usr/bin/test 2 8 4 +/usr/bin/touch 2 7 3 +/usr/bin/tr 2 8 3 +/usr/bin/tsort 2 7 3 +/usr/bin/tty 2 7 3 +/usr/bin/unexpand 2 7 3 +/usr/bin/uniq 2 7 3 +/usr/bin/unlink 2 8 4 +/usr/bin/uptime 2 7 3 +/usr/bin/users 2 8 4 +/usr/bin/vdir 2 8 4 +/usr/bin/wc 2 7 3 +/usr/bin/who 2 8 4 +/usr/bin/whoami 2 8 4 +/usr/bin/yes 1 6 1 +/usr/bin/ed 2 7 5 +/usr/bin/red 2 7 4 +/usr/bin/find 2 8 5 +/usr/bin/xargs 2 7 5 +/usr/bin/ispell 2 7 4 +/usr/bin/a2p 2 7 5 +/usr/bin/perlcc 2 7 5 +/usr/bin/perldoc 2 7 5 +/usr/bin/pod2* 2 7 5 +/usr/bin/prove 2 7 5 +/usr/bin/perl 2 10 7 +/usr/bin/perl* 2 10 7 +/usr/bin/suidperl 2 8 8 +/usr/bin/csh 2 8 8 +/usr/bin/tcsh 2 8 8 +/usr/bin/tree 2 6 5 +/usr/bin/last 2 7 5 +/usr/bin/lastb 2 7 5 +/usr/bin/utmpdump 2 6 5 +/usr/bin/alsamixer 2 6 8 +/usr/bin/amixer 2 6 8 +/usr/bin/amidi 2 6 8 +/usr/bin/aoss 2 6 8 +/usr/bin/aplay 2 6 8 +/usr/bin/aplaymidi 2 6 8 +/usr/bin/arecord 2 6 8 +/usr/bin/arecordmidi 2 6 8 +/usr/bin/aseqnet 2 6 8 +/usr/bin/aserver 2 6 8 +/usr/bin/iecset 2 6 8 +/usr/bin/rview 2 6 5 +/usr/bin/ex 2 7 5 +/usr/bin/enscript 2 6 5 +/usr/bin/genscript 2 6 5 +/usr/bin/xdelta 2 6 5 +/usr/bin/edit 2 6 5 +/usr/bin/vimtutor 2 6 5 +/usr/bin/rvim 2 6 5 +/usr/bin/vim 2 8 7 +/usr/bin/vimdiff 2 8 7 +/usr/bin/aspell 2 6 5 +/usr/bin/xxd 2 6 5 +/usr/bin/spell 2 6 5 +/usr/bin/eqn 2 6 5 +/usr/bin/eqn2graph 2 6 5 +/usr/bin/word-list-compress 2 6 4 +/usr/bin/afmtodit 2 6 4 +/usr/bin/hpf2dit 2 6 4 +/usr/bin/geqn 2 6 4 +/usr/bin/grn 2 6 4 +/usr/bin/grodvi 2 6 4 +/usr/bin/groff 2 6 5 +/usr/bin/groffer 2 6 4 +/usr/bin/grolj4 2 6 4 +/usr/bin/grotty 2 6 4 +/usr/bin/gtbl 2 6 4 +/usr/bin/pic2graph 2 6 4 +/usr/bin/indxbib 2 6 4 +/usr/bin/lkbib 2 6 4 +/usr/bin/lookbib 2 6 4 +/usr/bin/mmroff 2 6 4 +/usr/bin/neqn 2 6 4 +/usr/bin/pfbtops 2 6 4 +/usr/bin/pic 2 6 4 +/usr/bin/tfmtodit 2 6 4 +/usr/bin/tbl 2 6 4 +/usr/bin/post-grohtml 2 6 4 +/usr/bin/pre-grohtml 2 6 4 +/usr/bin/refer 2 6 4 +/usr/bin/soelim 2 6 4 +/usr/bin/disable-paste 2 6 6 +/usr/bin/troff 2 6 4 +/usr/bin/strace-graph 2 6 4 +/usr/bin/gpm-root 2 6 7 +/usr/bin/hltest 2 6 7 +/usr/bin/mev 2 6 6 +/usr/bin/mouse-test 2 6 6 +/usr/bin/strace 2 8 9 +/usr/bin/scsiformat 2 7 10 +/usr/bin/lsscsi 2 7 7 +/usr/bin/scsiinfo 2 7 7 +/usr/bin/sg_* 2 7 7 +/usr/bin/build-classpath 2 6 6 +/usr/bin/build-classpath-directory 2 6 6 +/usr/bin/build-jar-repository 2 6 6 +/usr/bin/diff-jars 2 6 6 +/usr/bin/jvmjar 2 6 6 +/usr/bin/rebuild-jar-repository 2 6 6 +/usr/bin/scriptreplay 2 6 5 +/usr/bin/cal 2 6 3 +/usr/bin/chkdupexe 2 6 5 +/usr/bin/col 2 6 4 +/usr/bin/colcrt 2 6 4 +/usr/bin/colrm 2 6 3 +/usr/bin/column 2 6 4 +/usr/bin/cytune 2 6 6 +/usr/bin/ddate 2 6 3 +/usr/bin/fdformat 2 6 6 +/usr/bin/getopt 2 8 6 +/usr/bin/hexdump 2 6 4 +/usr/bin/hostid 2 6 4 +/usr/bin/ipcrm 2 7 7 +/usr/bin/ipcs 2 7 6 +/usr/bin/isosize 2 6 4 +/usr/bin/line 2 6 4 +/usr/bin/look 2 6 5 +/usr/bin/mcookie 2 7 5 +/usr/bin/mesg 2 6 4 +/usr/bin/namei 2 6 5 +/usr/bin/rename 2 6 5 +/usr/bin/renice 2 6 7 +/usr/bin/rev 2 6 5 +/usr/bin/script 2 6 6 +/usr/bin/ChangeSymlinks 2 8 8 +/usr/bin/setfdprm 2 6 7 +/usr/bin/setsid 2 6 3 +/usr/bin/setterm 2 6 5 +/usr/bin/tailf 2 6 4 +/usr/bin/time 2 6 4 +/usr/bin/ul 2 6 4 +/usr/bin/wall 2 6 5 +/usr/bin/whereis 2 6 4 +/usr/bin/which 2 6 3 +/usr/bin/c_rehash 2 7 6 +/usr/bin/openssl 2 8 6 +/usr/bin/lsdev 2 6 5 +/usr/bin/procinfo 2 6 5 +/usr/bin/socklist 2 6 5 +/usr/bin/filesize 2 6 3 +/usr/bin/linkto 2 6 3 +/usr/bin/mkinfodir 2 6 5 +/usr/bin/old 2 6 4 +/usr/bin/rpmlocate 2 6 5 +/usr/bin/safe-rm 2 8 6 +/usr/bin/safe-rmdir 2 8 6 +/usr/bin/setJava 2 6 1 +/usr/bin/vmstat 2 6 4 +/usr/bin/top 2 6 6 +/usr/bin/pinentry* 2 7 6 +/usr/bin/free 2 8 4 +/usr/bin/pmap 2 6 5 +/usr/bin/slabtop 2 6 4 +/usr/bin/tload 2 6 4 +/usr/bin/watch 2 6 3 +/usr/bin/w 2 6 4 +/usr/bin/pstree.x11 2 6 4 +/usr/bin/pstree 2 6 4 +/usr/bin/snice 2 6 6 +/usr/bin/skill 2 6 7 +/usr/bin/pgrep 2 6 4 +/usr/bin/killall 2 6 7 +/usr/bin/curl 2 7 7 +/usr/bin/slptool 2 7 8 +/usr/bin/ldap* 2 7 7 +/usr/bin/whatis 2 7 5 diff --git a/utils/test/severity_broken.db b/utils/test/severity_broken.db new file mode 100644 index 000000000..417945324 --- /dev/null +++ b/utils/test/severity_broken.db @@ -0,0 +1,460 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2002-2005 Novell/SUSE +# +# 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. +# +# ------------------------------------------------------------------ + +# Allow this process to 0wn the machine: + CAP_SYS_ADMIN 10 + CAP_SYS_CHROOT 10 + CAP_SYS_MODULE + CAP_SYS_PTRACE 10 + CAP_SYS_RAWIO 10 + CAP_MAC_ADMIN 10 + CAP_MAC_OVERRIDE 10 +# Allow other processes to 0wn the machine: + CAP_SETPCAP 9 + CAP_SETFCAP 9 + CAP_CHOWN 9 + CAP_FSETID 9 + CAP_MKNOD 9 + CAP_LINUX_IMMUTABLE 9 + CAP_DAC_OVERRIDE 9 + CAP_SETGID 9 + CAP_SETUID 9 + CAP_FOWNER 9 +# Denial of service, bypass audit controls, information leak + CAP_SYS_TIME 8 + CAP_NET_ADMIN 8 + CAP_SYS_RESOURCE 8 + CAP_KILL 8 + CAP_IPC_OWNER 8 + CAP_SYS_PACCT 8 + CAP_SYS_BOOT 8 + CAP_NET_BIND_SERVICE 8 + CAP_NET_RAW 8 + CAP_SYS_NICE 8 + CAP_LEASE 8 + CAP_IPC_LOCK 8 + CAP_SYS_TTY_CONFIG 8 + CAP_AUDIT_CONTROL 8 + CAP_AUDIT_WRITE 8 + CAP_SYSLOG 8 + CAP_WAKE_ALARM 8 + CAP_BLOCK_SUSPEND 8 + CAP_DAC_READ_SEARCH 7 +# unused + CAP_NET_BROADCAST 0 + +# filename r w x +# 'hard drives' are generally 4 10 0 +/**/lost+found/** 5 5 0 +/boot/** 7 10 0 +/etc/passwd* 4 8 0 +/etc/group* 4 8 0 +/etc/shadow* 7 9 0 +/etc/shadow* 7 9 0 +/home/*/.ssh/** 7 9 0 +/home/*/.gnupg/** 5 7 0 +/home/** 4 6 0 +/srv/** 4 6 0 +/proc/** 6 9 0 +/proc/sys/kernel/hotplug 2 10 0 +/proc/sys/kernel/modprobe 2 10 0 +/proc/kallsyms 7 0 0 +/sys/** 4 8 0 +/sys/power/state 2 8 0 +/sys/firmware/** 2 10 0 +/dev/pts/* 8 9 0 +/dev/ptmx 8 9 0 +/dev/pty* 8 9 0 +/dev/null 0 0 0 +/dev/adbmouse 3 8 0 +/dev/ataraid 9 10 0 +/dev/zero 0 0 0 +/dev/agpgart* 8 10 0 +/dev/aio 3 3 0 +/dev/cbd/* 5 5 0 +/dev/cciss/* 4 10 0 +/dev/capi* 4 6 0 +/dev/cfs0 4 10 0 +/dev/compaq/* 4 10 0 +/dev/cdouble* 4 8 0 +/dev/cpu** 5 5 0 +/dev/cpu**microcode 1 10 0 +/dev/double* 4 8 0 +/dev/hd* 4 10 0 +/dev/sd* 4 10 0 +/dev/ida/* 4 10 0 +/dev/input/* 4 8 0 +/dev/mapper/control 4 10 0 +/dev/*mem 8 10 0 +/dev/loop* 4 10 0 +/dev/lp* 0 4 0 +/dev/md* 4 10 0 +/dev/msr 4 10 0 +/dev/nb* 4 10 0 +/dev/ram* 8 10 0 +/dev/rd/* 4 10 0 +/dev/*random 3 1 0 +/dev/sbpcd* 4 0 0 +/dev/rtc 6 0 0 +/dev/sd* 4 10 0 +/dev/sc* 4 10 0 +/dev/sg* 4 10 0 +/dev/st* 4 10 0 +/dev/snd/* 3 8 0 +/dev/usb/mouse* 4 6 0 +/dev/usb/hid* 4 6 0 +/dev/usb/tty* 4 6 0 +/dev/tty* 8 9 0 +/dev/stderr 0 0 0 +/dev/stdin 0 0 0 +/dev/stdout 0 0 0 +/dev/ubd* 4 10 0 +/dev/usbmouse* 4 6 0 +/dev/userdma 8 10 0 +/dev/vcs* 8 9 0 +/dev/xta* 4 10 0 +/dev/zero 0 0 0 +/dev/inittcl 8 10 0 +/dev/log 5 7 0 +/etc/fstab 3 8 0 +/etc/mtab 3 5 0 +/etc/SuSEconfig/* 1 8 0 +/etc/X11/* 2 7 0 +/etc/X11/xinit/* 2 8 0 +/etc/SuSE-release 1 5 0 +/etc/issue* 1 3 0 +/etc/motd 1 3 0 +/etc/aliases.d/* 1 7 0 +/etc/cron* 1 9 0 +/etc/cups/* 2 7 0 +/etc/default/* 3 8 0 +/etc/init.d/* 1 10 0 +/etc/permissions.d/* 1 8 0 +/etc/ppp/* 2 6 0 +/etc/ppp/*secrets 8 6 0 +/etc/profile.d/* 1 8 0 +/etc/skel/* 0 7 0 +/etc/sysconfig/* 4 10 0 +/etc/xinetd.d/* 1 9 0 +/etc/termcap/* 1 4 0 +/etc/ld.so.* 1 9 0 +/etc/pam.d/* 3 9 0 +/etc/udev/* 3 9 0 +/etc/insserv.conf 3 6 0 +/etc/security/* 1 9 0 +/etc/securetty 0 7 0 +/etc/sudoers 4 9 0 +/etc/hotplug/* 2 10 0 +/etc/xinitd.conf 1 9 0 +/etc/gpm/* 2 10 0 +/etc/ssl/** 2 7 0 +/etc/shadow* 5 9 0 +/etc/bash.bashrc 1 9 0 +/etc/csh.cshrc 1 9 0 +/etc/csh.login 1 9 0 +/etc/inittab 1 10 0 +/etc/profile* 1 9 0 +/etc/shells 1 5 0 +/etc/alternatives 1 6 0 +/etc/sysctl.conf 3 7 0 +/etc/dev.d/* 1 8 0 +/etc/manpath.config 1 6 0 +/etc/permissions* 1 8 0 +/etc/evms.conf 3 8 0 +/etc/exports 3 8 0 +/etc/samba/* 5 8 0 +/etc/ssh/* 3 8 0 +/etc/ssh/ssh_host_*key 8 8 0 +/etc/krb5.conf 4 8 0 +/etc/ntp.conf 3 8 0 +/etc/auto.* 3 8 0 +/etc/postfix/* 3 7 0 +/etc/postfix/*passwd* 6 7 0 +/etc/postfix/*cert* 6 7 0 +/etc/foomatic/* 3 5 0 +/etc/printcap 3 5 0 +/etc/youservers 4 9 0 +/etc/grub.conf 7 10 0 +/etc/modules.conf 4 10 0 +/etc/resolv.conf 2 7 0 +/etc/apache2/** 3 7 0 +/etc/apache2/**ssl** 7 7 0 +/etc/subdomain.d/** 6 10 0 +/etc/apparmor.d/** 6 10 0 +/etc/apparmor/** 6 10 0 +/var/log/** 3 8 0 +/var/adm/SuSEconfig/** 3 8 0 +/var/adm/** 3 7 0 +/var/lib/rpm/** 4 8 0 +/var/run/nscd/* 3 3 0 +/var/run/.nscd_socket 3 3 0 +/usr/share/doc/** 1 1 0 +/usr/share/man/** 3 5 0 +/usr/X11/man/** 3 5 0 +/usr/share/info/** 2 4 0 +/usr/share/java/** 2 5 0 +/usr/share/locale/** 2 4 0 +/usr/share/sgml/** 2 4 0 +/usr/share/YaST2/** 3 9 0 +/usr/share/ghostscript/** 3 5 0 +/usr/share/terminfo/** 1 8 0 +/usr/share/latex2html/** 2 4 0 +/usr/share/cups/** 5 6 0 +/usr/share/susehelp/** 2 6 0 +/usr/share/susehelp/cgi-bin/** 3 7 7 +/usr/share/zoneinfo/** 2 7 0 +/usr/share/zsh/** 3 6 0 +/usr/share/vim/** 3 8 0 +/usr/share/groff/** 3 7 0 +/usr/share/vnc/** 3 8 0 +/usr/share/wallpapers/** 2 4 0 +/usr/X11** 3 8 5 +/usr/X11*/bin/XFree86 3 8 8 +/usr/X11*/bin/Xorg 3 8 8 +/usr/X11*/bin/sux 3 8 8 +/usr/X11*/bin/xconsole 3 7 7 +/usr/X11*/bin/xhost 3 7 7 +/usr/X11*/bin/xauth 3 7 7 +/usr/X11*/bin/ethereal 3 6 8 +/usr/lib/ooo-** 3 6 5 +/usr/lib/lsb/** 2 8 8 +/usr/lib/pt_chwon 2 8 5 +/usr/lib/tcl** 2 5 3 +/usr/lib/lib*so* 3 8 4 +/usr/lib/iptables/* 2 8 2 +/usr/lib/perl5/** 4 10 6 +/usr/lib/gconv/* 4 7 4 +/usr/lib/locale/** 4 8 0 +/usr/lib/jvm/** 5 7 5 +/usr/lib/sasl*/** 5 8 4 +/usr/lib/jvm-exports/** 5 7 5 +/usr/lib/jvm-private/** 5 7 5 +/usr/lib/python*/** 5 7 5 +/usr/lib/libkrb5* 4 8 4 +/usr/lib/postfix/* 4 7 4 +/usr/lib/rpm/** 4 8 6 +/usr/lib/rpm/gnupg/** 4 9 0 +/usr/lib/apache2** 4 7 4 +/usr/lib/mailman/** 4 6 4 +/usr/bin/ldd 1 7 4 +/usr/bin/netcat 5 7 8 +/usr/bin/clear 2 6 3 +/usr/bin/reset 2 6 3 +/usr/bin/tput 2 6 3 +/usr/bin/tset 2 6 3 +/usr/bin/file 2 6 3 +/usr/bin/ftp 3 7 5 +/usr/bin/busybox 4 8 6 +/usr/bin/rbash 4 8 5 +/usr/bin/screen 3 6 5 +/usr/bin/getfacl 3 7 4 +/usr/bin/setfacl 3 7 9 +/usr/bin/*awk* 3 7 7 +/usr/bin/sudo 2 9 10 +/usr/bin/lsattr 2 6 5 +/usr/bin/chattr 2 7 8 +/usr/bin/sed 3 7 6 +/usr/bin/grep 2 7 2 +/usr/bin/chroot 2 6 10 +/usr/bin/dircolors 2 9 3 +/usr/bin/cut 2 7 2 +/usr/bin/du 2 7 3 +/usr/bin/env 2 7 2 +/usr/bin/head 2 7 2 +/usr/bin/tail 2 7 2 +/usr/bin/install 2 8 4 +/usr/bin/link 2 6 4 +/usr/bin/logname 2 6 2 +/usr/bin/md5sum 2 8 3 +/usr/bin/mkfifo 2 6 10 +/usr/bin/nice 2 7 7 +/usr/bin/nohup 2 7 7 +/usr/bin/printf 2 7 1 +/usr/bin/readlink 2 7 3 +/usr/bin/seq 2 7 1 +/usr/bin/sha1sum 2 8 3 +/usr/bin/shred 2 7 3 +/usr/bin/sort 2 7 3 +/usr/bin/split 2 7 3 +/usr/bin/stat 2 7 4 +/usr/bin/sum 2 8 3 +/usr/bin/tac 2 7 3 +/usr/bin/tail 3 8 4 +/usr/bin/tee 2 7 3 +/usr/bin/test 2 8 4 +/usr/bin/touch 2 7 3 +/usr/bin/tr 2 8 3 +/usr/bin/tsort 2 7 3 +/usr/bin/tty 2 7 3 +/usr/bin/unexpand 2 7 3 +/usr/bin/uniq 2 7 3 +/usr/bin/unlink 2 8 4 +/usr/bin/uptime 2 7 3 +/usr/bin/users 2 8 4 +/usr/bin/vdir 2 8 4 +/usr/bin/wc 2 7 3 +/usr/bin/who 2 8 4 +/usr/bin/whoami 2 8 4 +/usr/bin/yes 1 6 1 +/usr/bin/ed 2 7 5 +/usr/bin/red 2 7 4 +/usr/bin/find 2 8 5 +/usr/bin/xargs 2 7 5 +/usr/bin/ispell 2 7 4 +/usr/bin/a2p 2 7 5 +/usr/bin/perlcc 2 7 5 +/usr/bin/perldoc 2 7 5 +/usr/bin/pod2* 2 7 5 +/usr/bin/prove 2 7 5 +/usr/bin/perl 2 10 7 +/usr/bin/perl* 2 10 7 +/usr/bin/suidperl 2 8 8 +/usr/bin/csh 2 8 8 +/usr/bin/tcsh 2 8 8 +/usr/bin/tree 2 6 5 +/usr/bin/last 2 7 5 +/usr/bin/lastb 2 7 5 +/usr/bin/utmpdump 2 6 5 +/usr/bin/alsamixer 2 6 8 +/usr/bin/amixer 2 6 8 +/usr/bin/amidi 2 6 8 +/usr/bin/aoss 2 6 8 +/usr/bin/aplay 2 6 8 +/usr/bin/aplaymidi 2 6 8 +/usr/bin/arecord 2 6 8 +/usr/bin/arecordmidi 2 6 8 +/usr/bin/aseqnet 2 6 8 +/usr/bin/aserver 2 6 8 +/usr/bin/iecset 2 6 8 +/usr/bin/rview 2 6 5 +/usr/bin/ex 2 7 5 +/usr/bin/enscript 2 6 5 +/usr/bin/genscript 2 6 5 +/usr/bin/xdelta 2 6 5 +/usr/bin/edit 2 6 5 +/usr/bin/vimtutor 2 6 5 +/usr/bin/rvim 2 6 5 +/usr/bin/vim 2 8 7 +/usr/bin/vimdiff 2 8 7 +/usr/bin/aspell 2 6 5 +/usr/bin/xxd 2 6 5 +/usr/bin/spell 2 6 5 +/usr/bin/eqn 2 6 5 +/usr/bin/eqn2graph 2 6 5 +/usr/bin/word-list-compress 2 6 4 +/usr/bin/afmtodit 2 6 4 +/usr/bin/hpf2dit 2 6 4 +/usr/bin/geqn 2 6 4 +/usr/bin/grn 2 6 4 +/usr/bin/grodvi 2 6 4 +/usr/bin/groff 2 6 5 +/usr/bin/groffer 2 6 4 +/usr/bin/grolj4 2 6 4 +/usr/bin/grotty 2 6 4 +/usr/bin/gtbl 2 6 4 +/usr/bin/pic2graph 2 6 4 +/usr/bin/indxbib 2 6 4 +/usr/bin/lkbib 2 6 4 +/usr/bin/lookbib 2 6 4 +/usr/bin/mmroff 2 6 4 +/usr/bin/neqn 2 6 4 +/usr/bin/pfbtops 2 6 4 +/usr/bin/pic 2 6 4 +/usr/bin/tfmtodit 2 6 4 +/usr/bin/tbl 2 6 4 +/usr/bin/post-grohtml 2 6 4 +/usr/bin/pre-grohtml 2 6 4 +/usr/bin/refer 2 6 4 +/usr/bin/soelim 2 6 4 +/usr/bin/disable-paste 2 6 6 +/usr/bin/troff 2 6 4 +/usr/bin/strace-graph 2 6 4 +/usr/bin/gpm-root 2 6 7 +/usr/bin/hltest 2 6 7 +/usr/bin/mev 2 6 6 +/usr/bin/mouse-test 2 6 6 +/usr/bin/strace 2 8 9 +/usr/bin/scsiformat 2 7 10 +/usr/bin/lsscsi 2 7 7 +/usr/bin/scsiinfo 2 7 7 +/usr/bin/sg_* 2 7 7 +/usr/bin/build-classpath 2 6 6 +/usr/bin/build-classpath-directory 2 6 6 +/usr/bin/build-jar-repository 2 6 6 +/usr/bin/diff-jars 2 6 6 +/usr/bin/jvmjar 2 6 6 +/usr/bin/rebuild-jar-repository 2 6 6 +/usr/bin/scriptreplay 2 6 5 +/usr/bin/cal 2 6 3 +/usr/bin/chkdupexe 2 6 5 +/usr/bin/col 2 6 4 +/usr/bin/colcrt 2 6 4 +/usr/bin/colrm 2 6 3 +/usr/bin/column 2 6 4 +/usr/bin/cytune 2 6 6 +/usr/bin/ddate 2 6 3 +/usr/bin/fdformat 2 6 6 +/usr/bin/getopt 2 8 6 +/usr/bin/hexdump 2 6 4 +/usr/bin/hostid 2 6 4 +/usr/bin/ipcrm 2 7 7 +/usr/bin/ipcs 2 7 6 +/usr/bin/isosize 2 6 4 +/usr/bin/line 2 6 4 +/usr/bin/look 2 6 5 +/usr/bin/mcookie 2 7 5 +/usr/bin/mesg 2 6 4 +/usr/bin/namei 2 6 5 +/usr/bin/rename 2 6 5 +/usr/bin/renice 2 6 7 +/usr/bin/rev 2 6 5 +/usr/bin/script 2 6 6 +/usr/bin/ChangeSymlinks 2 8 8 +/usr/bin/setfdprm 2 6 7 +/usr/bin/setsid 2 6 3 +/usr/bin/setterm 2 6 5 +/usr/bin/tailf 2 6 4 +/usr/bin/time 2 6 4 +/usr/bin/ul 2 6 4 +/usr/bin/wall 2 6 5 +/usr/bin/whereis 2 6 4 +/usr/bin/which 2 6 3 +/usr/bin/c_rehash 2 7 6 +/usr/bin/openssl 2 8 6 +/usr/bin/lsdev 2 6 5 +/usr/bin/procinfo 2 6 5 +/usr/bin/socklist 2 6 5 +/usr/bin/filesize 2 6 3 +/usr/bin/linkto 2 6 3 +/usr/bin/mkinfodir 2 6 5 +/usr/bin/old 2 6 4 +/usr/bin/rpmlocate 2 6 5 +/usr/bin/safe-rm 2 8 6 +/usr/bin/safe-rmdir 2 8 6 +/usr/bin/setJava 2 6 1 +/usr/bin/vmstat 2 6 4 +/usr/bin/top 2 6 6 +/usr/bin/pinentry* 2 7 6 +/usr/bin/free 2 8 4 +/usr/bin/pmap 2 6 5 +/usr/bin/slabtop 2 6 4 +/usr/bin/tload 2 6 4 +/usr/bin/watch 2 6 3 +/usr/bin/w 2 6 4 +/usr/bin/pstree.x11 2 6 4 +/usr/bin/pstree 2 6 4 +/usr/bin/snice 2 6 6 +/usr/bin/skill 2 6 7 +/usr/bin/pgrep 2 6 4 +/usr/bin/killall 2 6 7 +/usr/bin/curl 2 7 7 +/usr/bin/slptool 2 7 8 +/usr/bin/ldap* 2 7 7 +/usr/bin/whatis 2 7 5 diff --git a/utils/test/severity_test.py b/utils/test/severity_test.py new file mode 100755 index 000000000..1abfa2847 --- /dev/null +++ b/utils/test/severity_test.py @@ -0,0 +1,90 @@ +# ---------------------------------------------------------------------- +# 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. +# +# ---------------------------------------------------------------------- +import os +import shutil +import sys +import unittest + +sys.path.append('../') + +import apparmor.severity as severity +from apparmor.common import AppArmorException + +class Test(unittest.TestCase): + + def setUp(self): + #copy the local profiles to the test directory + if os.path.exists('./profiles'): + shutil.rmtree('./profiles') + shutil.copytree('/etc/apparmor.d/', './profiles/', symlinks=True) + + def tearDown(self): + #Wipe the local profiles from the test directory + shutil.rmtree('./profiles') + + def testRank_Test(self): + sev_db = severity.Severity('severity.db') + rank = sev_db.rank('/usr/bin/whatis', 'x') + self.assertEqual(rank, 5, 'Wrong rank') + rank = sev_db.rank('/etc', 'x') + self.assertEqual(rank, 10, 'Wrong rank') + rank = sev_db.rank('/dev/doublehit', 'x') + self.assertEqual(rank, 0, 'Wrong rank') + rank = sev_db.rank('/dev/doublehit', 'rx') + self.assertEqual(rank, 4, 'Wrong rank') + rank = sev_db.rank('/dev/doublehit', 'rwx') + self.assertEqual(rank, 8, 'Wrong rank') + rank = sev_db.rank('/dev/tty10', 'rwx') + self.assertEqual(rank, 9, 'Wrong rank') + rank = sev_db.rank('/var/adm/foo/**', 'rx') + self.assertEqual(rank, 3, 'Wrong rank') + rank = sev_db.rank('CAP_KILL') + self.assertEqual(rank, 8, 'Wrong rank') + rank = sev_db.rank('CAP_SETPCAP') + self.assertEqual(rank, 9, 'Wrong rank') + self.assertEqual(sev_db.rank('/etc/apparmor/**', 'r') , 6, 'Invalid Rank') + self.assertEqual(sev_db.rank('/etc/**', 'r') , 10, 'Invalid Rank') + + # Load all variables for /sbin/klogd and test them + sev_db.load_variables('profiles/sbin.klogd') + self.assertEqual(sev_db.rank('@{PROC}/sys/vm/overcommit_memory', 'r'), 6, 'Invalid Rank') + self.assertEqual(sev_db.rank('@{HOME}/sys/@{PROC}/overcommit_memory', 'r'), 10, 'Invalid Rank') + self.assertEqual(sev_db.rank('/overco@{multiarch}mmit_memory', 'r'), 10, 'Invalid Rank') + + sev_db.unload_variables() + + sev_db.load_variables('profiles/usr.sbin.dnsmasq') + self.assertEqual(sev_db.rank('@{PROC}/sys/@{TFTP_DIR}/overcommit_memory', 'r'), 6, 'Invalid Rank') + self.assertEqual(sev_db.rank('@{PROC}/sys/vm/overcommit_memory', 'r'), 6, 'Invalid Rank') + self.assertEqual(sev_db.rank('@{HOME}/sys/@{PROC}/overcommit_memory', 'r'), 10, 'Invalid Rank') + self.assertEqual(sev_db.rank('/overco@{multiarch}mmit_memory', 'r'), 10, 'Invalid Rank') + + #self.assertEqual(sev_db.rank('/proc/@{PID}/maps', 'rw'), 9, 'Invalid Rank') + + def testInvalid(self): + sev_db = severity.Severity('severity.db') + rank = sev_db.rank('/dev/doublehit', 'i') + self.assertEqual(rank, 10, 'Wrong') + try: + severity.Severity('severity_broken.db') + except AppArmorException: + pass + rank = sev_db.rank('CAP_UNKOWN') + rank = sev_db.rank('CAP_K*') + + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/utils/test/test-aa-easyprof.py b/utils/test/test-aa-easyprof.py index e59c13860..5607ab7fd 100755 --- a/utils/test/test-aa-easyprof.py +++ b/utils/test/test-aa-easyprof.py @@ -1,7 +1,7 @@ #! /usr/bin/env python # ------------------------------------------------------------------ # -# Copyright (C) 2011-2012 Canonical Ltd. +# 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 @@ -10,6 +10,8 @@ # ------------------------------------------------------------------ import glob +import json +import optparse import os import shutil import sys @@ -31,6 +33,69 @@ def recursive_rm(dirPath, contents_only=False): if contents_only == False: os.rmdir(dirPath) +# From Lib/test/test_optparse.py from python 2.7.4 +class InterceptedError(Exception): + def __init__(self, + error_message=None, + exit_status=None, + exit_message=None): + self.error_message = error_message + self.exit_status = exit_status + self.exit_message = exit_message + + def __str__(self): + return self.error_message or self.exit_message or "intercepted error" + + +class InterceptingOptionParser(optparse.OptionParser): + def exit(self, status=0, msg=None): + raise InterceptedError(exit_status=status, exit_message=msg) + + def error(self, msg): + raise InterceptedError(error_message=msg) + + +class Manifest(object): + def __init__(self, profile_name): + self.security = dict() + self.security['profiles'] = dict() + self.profile_name = profile_name + self.security['profiles'][self.profile_name] = dict() + + def add_policygroups(self, policy_list): + self.security['profiles'][self.profile_name]['policy_groups'] = policy_list.split(",") + + def add_author(self, author): + self.security['profiles'][self.profile_name]['author'] = author + + def add_copyright(self, copyright): + self.security['profiles'][self.profile_name]['copyright'] = copyright + + def add_comment(self, comment): + self.security['profiles'][self.profile_name]['comment'] = comment + + def add_binary(self, binary): + self.security['profiles'][self.profile_name]['binary'] = binary + + def add_template(self, template): + self.security['profiles'][self.profile_name]['template'] = template + + def add_template_variable(self, name, value): + if not 'template_variables' in self.security['profiles'][self.profile_name]: + self.security['profiles'][self.profile_name]['template_variables'] = dict() + + self.security['profiles'][self.profile_name]['template_variables'][name] = value + + def emit_json(self, use_security_prefix=True): + manifest = dict() + manifest['security'] = self.security + if use_security_prefix: + dumpee = manifest + else: + dumpee = self.security + + return json.dumps(dumpee, indent=2) + # # Our test class # @@ -58,7 +123,7 @@ class T(unittest.TestCase): ###VAR### -###BINARY### { +###PROFILEATTACH### { #include ###ABSTRACTIONS### @@ -101,7 +166,8 @@ TEMPLATES_DIR="%s/templates" def tearDown(self): '''Teardown for tests''' if os.path.exists(self.tmpdir): - sys.stdout.write("%s\n" % self.tmpdir) + if debugging: + sys.stdout.write("%s\n" % self.tmpdir) recursive_rm(self.tmpdir) # @@ -206,6 +272,30 @@ TEMPLATES_DIR="%s/templates" self.assertTrue(easyp.dirs['policygroups'] == valid, "Not using specified --policy-groups-dir") self.assertFalse(easyp.get_policy_groups() == None, "Could not find policy-groups") + def test_policygroups_dir_valid_with_vendor(self): + '''Test --policy-groups-dir (valid DIR with vendor)''' + os.chdir(self.tmpdir) + valid = os.path.join(self.tmpdir, 'valid') + os.mkdir(valid) + shutil.copy(os.path.join(self.tmpdir, 'policygroups', self.test_policygroup), os.path.join(valid, self.test_policygroup)) + + vendor = "ubuntu" + version = "1.0" + valid_distro = os.path.join(valid, vendor, version) + os.mkdir(os.path.join(valid, vendor)) + os.mkdir(valid_distro) + shutil.copy(os.path.join(self.tmpdir, 'policygroups', self.test_policygroup), valid_distro) + + args = self.full_args + args += ['--policy-groups-dir', valid, '--show-policy-group', '--policy-groups=%s' % self.test_policygroup] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + self.assertTrue(easyp.dirs['policygroups'] == valid, "Not using specified --policy-groups-dir") + self.assertFalse(easyp.get_policy_groups() == None, "Could not find policy-groups") + for f in easyp.get_policy_groups(): + self.assertFalse(os.path.basename(f) == vendor, "Found '%s' in %s" % (vendor, f)) + def test_configuration_file_t_invalid(self): '''Test config parsing (invalid TEMPLATES_DIR)''' contents = ''' @@ -305,13 +395,51 @@ POLICYGROUPS_DIR="%s/templates" self.assertTrue(easyp.dirs['templates'] == valid, "Not using specified --template-dir") self.assertFalse(easyp.get_templates() == None, "Could not find templates") + def test_templates_dir_valid_with_vendor(self): + '''Test --templates-dir (valid DIR with vendor)''' + os.chdir(self.tmpdir) + valid = os.path.join(self.tmpdir, 'valid') + os.mkdir(valid) + shutil.copy(os.path.join(self.tmpdir, 'templates', self.test_template), os.path.join(valid, self.test_template)) + + vendor = "ubuntu" + version = "1.0" + valid_distro = os.path.join(valid, vendor, version) + os.mkdir(os.path.join(valid, vendor)) + os.mkdir(valid_distro) + shutil.copy(os.path.join(self.tmpdir, 'templates', self.test_template), valid_distro) + + args = self.full_args + args += ['--templates-dir', valid, '--show-template', '--template=%s' % self.test_template] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + self.assertTrue(easyp.dirs['templates'] == valid, "Not using specified --template-dir") + self.assertFalse(easyp.get_templates() == None, "Could not find templates") + for f in easyp.get_templates(): + self.assertFalse(os.path.basename(f) == vendor, "Found '%s' in %s" % (vendor, f)) + # # Binary file tests # - def test_binary(self): - '''Test binary''' + def test_binary_without_profile_name(self): + '''Test binary ( { })''' easyprof.AppArmorEasyProfile('/bin/ls', self.options) + def test_binary_with_profile_name(self): + '''Test binary (profile { })''' + args = self.full_args + args += ['--profile-name=some-profile-name'] + (self.options, self.args) = easyprof.parse_args(args) + easyprof.AppArmorEasyProfile('/bin/ls', self.options) + + def test_binary_omitted_with_profile_name(self): + '''Test binary (profile { })''' + args = self.full_args + args += ['--profile-name=some-profile-name'] + (self.options, self.args) = easyprof.parse_args(args) + easyprof.AppArmorEasyProfile(None, self.options) + def test_binary_nonexistent(self): '''Test binary (nonexistent)''' easyprof.AppArmorEasyProfile(os.path.join(self.tmpdir, 'nonexistent'), self.options) @@ -399,9 +527,109 @@ POLICYGROUPS_DIR="%s/templates" self.assertTrue(os.path.exists(path), "Could not find '%s'" % path) open(path).read() +# +# Manifest file argument tests +# + def test_manifest_argument(self): + '''Test manifest argument''' + + # setup our manifest + self.manifest = os.path.join(self.tmpdir, 'manifest.json') + contents = ''' +{"security": {"domain.reverse.appname": {"name": "simple-app"}}} +''' + open(self.manifest, 'w').write(contents) + + args = self.full_args + args.extend(['--manifest', self.manifest]) + easyprof.parse_args(args) + + def _manifest_conflicts(self, opt, value): + '''Helper for conflicts tests''' + # setup our manifest + self.manifest = os.path.join(self.tmpdir, 'manifest.json') + contents = ''' +{"security": {"domain.reverse.appname": {"binary": /nonexistent"}}} +''' + open(self.manifest, 'w').write(contents) + + # opt first + args = self.full_args + args.extend([opt, value, '--manifest', self.manifest]) + raised = False + try: + easyprof.parse_args(args, InterceptingOptionParser()) + except InterceptedError: + raised = True + + self.assertTrue(raised, msg="%s and manifest arguments did not " \ + "raise a parse error" % opt) + + # manifest first + args = self.full_args + args.extend(['--manifest', self.manifest, opt, value]) + raised = False + try: + easyprof.parse_args(args, InterceptingOptionParser()) + except InterceptedError: + raised = True + + self.assertTrue(raised, msg="%s and manifest arguments did not " \ + "raise a parse error" % opt) + + def test_manifest_conflicts_profilename(self): + '''Test manifest arg conflicts with profile_name arg''' + self._manifest_conflicts("--profile-name", "simple-app") + + def test_manifest_conflicts_copyright(self): + '''Test manifest arg conflicts with copyright arg''' + self._manifest_conflicts("--copyright", "2013-01-01") + + def test_manifest_conflicts_author(self): + '''Test manifest arg conflicts with author arg''' + self._manifest_conflicts("--author", "Foo Bar") + + def test_manifest_conflicts_comment(self): + '''Test manifest arg conflicts with comment arg''' + self._manifest_conflicts("--comment", "some comment") + + def test_manifest_conflicts_abstractions(self): + '''Test manifest arg conflicts with abstractions arg''' + self._manifest_conflicts("--abstractions", "base") + + def test_manifest_conflicts_read_path(self): + '''Test manifest arg conflicts with read-path arg''' + self._manifest_conflicts("--read-path", "/etc/passwd") + + def test_manifest_conflicts_write_path(self): + '''Test manifest arg conflicts with write-path arg''' + self._manifest_conflicts("--write-path", "/tmp/foo") + + def test_manifest_conflicts_policy_groups(self): + '''Test manifest arg conflicts with policy-groups arg''' + self._manifest_conflicts("--policy-groups", "opt-application") + + def test_manifest_conflicts_name(self): + '''Test manifest arg conflicts with name arg''' + self._manifest_conflicts("--name", "foo") + + def test_manifest_conflicts_template_var(self): + '''Test manifest arg conflicts with template-var arg''' + self._manifest_conflicts("--template-var", "foo") + + def test_manifest_conflicts_policy_version(self): + '''Test manifest arg conflicts with policy-version arg''' + self._manifest_conflicts("--policy-version", "1.0") + + def test_manifest_conflicts_policy_vendor(self): + '''Test manifest arg conflicts with policy-vendor arg''' + self._manifest_conflicts("--policy-vendor", "somevendor") + + # # Test genpolicy # + def _gen_policy(self, name=None, template=None, extra_args=[]): '''Generate a policy''' # Build up our args @@ -446,6 +674,40 @@ POLICYGROUPS_DIR="%s/templates" return p + def _gen_manifest_policy(self, manifest, use_security_prefix=True): + # Build up our args + args = self.full_args + args.append("--manifest=/dev/null") + + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(manifest.emit_json(use_security_prefix), self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, self.options) + params = easyprof.gen_policy_params(binary, self.options) + p = easyp.gen_policy(**params) + + # ###NAME### should be replaced with self.binary or 'name'. Check for that + inv_s = '###NAME###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + if debugging: + sys.stdout.write("%s\n" % p) + + return p + + def test__is_safe(self): + '''Test _is_safe()''' + bad = [ + "/../../../../etc/passwd", + "abstraction with spaces", + "semicolon;bad", + "bad\x00baz", + "foo/bar", + "foo'bar", + 'foo"bar', + ] + for s in bad: + self.assertFalse(easyprof._is_safe(s), "'%s' should be bad" %s) + def test_genpolicy_templates_abspath(self): '''Test genpolicy (abspath to template)''' # create a new template @@ -521,6 +783,54 @@ POLICYGROUPS_DIR="%s/templates" inv_s = '###ABSTRACTIONS###' self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + def test_genpolicy_abstractions_bad(self): + '''Test genpolicy (abstractions - bad values)''' + bad = [ + "nonexistent", + "/../../../../etc/passwd", + "abstraction with spaces", + ] + for s in bad: + try: + self._gen_policy(extra_args=['--abstractions=%s' % s]) + except easyprof.AppArmorException: + continue + except Exception: + raise + raise Exception ("abstraction '%s' should be invalid" % s) + + def test_genpolicy_profile_name_bad(self): + '''Test genpolicy (profile name - bad values)''' + bad = [ + "/../../../../etc/passwd", + "../../../../etc/passwd", + "profile name with spaces", + ] + for s in bad: + try: + self._gen_policy(extra_args=['--profile-name=%s' % s]) + except easyprof.AppArmorException: + continue + except Exception: + raise + raise Exception ("profile_name '%s' should be invalid" % s) + + def test_genpolicy_policy_group_bad(self): + '''Test genpolicy (policy group - bad values)''' + bad = [ + "/../../../../etc/passwd", + "../../../../etc/passwd", + "profile name with spaces", + ] + for s in bad: + try: + self._gen_policy(extra_args=['--policy-groups=%s' % s]) + except easyprof.AppArmorException: + continue + except Exception: + raise + raise Exception ("policy group '%s' should be invalid" % s) + def test_genpolicy_policygroups(self): '''Test genpolicy (single policygroup)''' groups = self.test_policygroup @@ -566,7 +876,7 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (read-path file)''' s = "/opt/test-foo" p = self._gen_policy(extra_args=['--read-path=%s' % s]) - search = "%s r," % s + search = "%s rk," % s self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) @@ -575,7 +885,7 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (read-path file in /home)''' s = "/home/*/test-foo" p = self._gen_policy(extra_args=['--read-path=%s' % s]) - search = "owner %s r," % s + search = "owner %s rk," % s self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) @@ -584,7 +894,7 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (read-path file in @{HOME})''' s = "@{HOME}/test-foo" p = self._gen_policy(extra_args=['--read-path=%s' % s]) - search = "owner %s r," % s + search = "owner %s rk," % s self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) @@ -593,7 +903,7 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (read-path file in @{HOMEDIRS})''' s = "@{HOMEDIRS}/test-foo" p = self._gen_policy(extra_args=['--read-path=%s' % s]) - search = "owner %s r," % s + search = "owner %s rk," % s self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) @@ -602,7 +912,7 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (read-path directory/)''' s = "/opt/test-foo-dir/" p = self._gen_policy(extra_args=['--read-path=%s' % s]) - search_terms = ["%s r," % s, "%s** r," % s] + search_terms = ["%s rk," % s, "%s** rk," % s] for search in search_terms: self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' @@ -612,7 +922,7 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (read-path directory/*)''' s = "/opt/test-foo-dir/*" p = self._gen_policy(extra_args=['--read-path=%s' % s]) - search_terms = ["%s r," % os.path.dirname(s), "%s r," % s] + search_terms = ["%s rk," % os.path.dirname(s), "%s rk," % s] for search in search_terms: self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' @@ -622,7 +932,7 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (read-path directory/**)''' s = "/opt/test-foo-dir/**" p = self._gen_policy(extra_args=['--read-path=%s' % s]) - search_terms = ["%s r," % os.path.dirname(s), "%s r," % s] + search_terms = ["%s rk," % os.path.dirname(s), "%s rk," % s] for search in search_terms: self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' @@ -646,13 +956,13 @@ POLICYGROUPS_DIR="%s/templates" if s.startswith('/home/') or s.startswith("@{HOME"): owner = "owner " if s.endswith('/'): - search_terms.append("%s r," % (s)) - search_terms.append("%s%s** r," % (owner, s)) + search_terms.append("%s rk," % (s)) + search_terms.append("%s%s** rk," % (owner, s)) elif s.endswith('/**') or s.endswith('/*'): - search_terms.append("%s r," % (os.path.dirname(s))) - search_terms.append("%s%s r," % (owner, s)) + search_terms.append("%s rk," % (os.path.dirname(s))) + search_terms.append("%s%s rk," % (owner, s)) else: - search_terms.append("%s%s r," % (owner, s)) + search_terms.append("%s%s rk," % (owner, s)) p = self._gen_policy(extra_args=args) for search in search_terms: @@ -784,33 +1094,48 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (template-var single)''' s = "@{FOO}=bar" p = self._gen_policy(extra_args=['--template-var=%s' % s]) + k, v = s.split('=') + s = '%s="%s"' % (k, v) self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) inv_s = '###TEMPLATEVAR###' self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) def test_genpolicy_templatevar_multiple(self): '''Test genpolicy (template-var multiple)''' - variables = ["@{FOO}=bar", "@{BAR}=baz"] + variables = ['@{FOO}=bar', '@{BAR}=baz'] args = [] for s in variables: args.append('--template-var=%s' % s) p = self._gen_policy(extra_args=args) for s in variables: + k, v = s.split('=') + s = '%s="%s"' % (k, v) self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) inv_s = '###TEMPLATEVAR###' self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) def test_genpolicy_templatevar_bad(self): - '''Test genpolicy (template-var bad)''' - s = "{FOO}=bar" - try: - self._gen_policy(extra_args=['--template-var=%s' % s]) - except easyprof.AppArmorException: - return - except Exception: - raise - raise Exception ("template-var should be invalid") + '''Test genpolicy (template-var - bad values)''' + bad = [ + "{FOO}=bar", + "@FOO}=bar", + "@{FOO=bar", + "FOO=bar", + "@FOO=bar", + "@{FOO}=/../../../etc/passwd", + "@{FOO}=bar=foo", + "@{FOO;BAZ}=bar", + '@{FOO}=bar"baz', + ] + for s in bad: + try: + self._gen_policy(extra_args=['--template-var=%s' % s]) + except easyprof.AppArmorException: + continue + except Exception: + raise + raise Exception ("template-var should be invalid") def test_genpolicy_invalid_template_policy(self): '''Test genpolicy (invalid template policy)''' @@ -835,6 +1160,1291 @@ POLICYGROUPS_DIR="%s/templates" raise raise Exception ("policy should be invalid") + def test_genpolicy_no_binary_without_profile_name(self): + '''Test genpolicy (no binary with no profile name)''' + try: + easyprof.gen_policy_params(None, self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("No binary or profile name should have been invalid") + + def test_genpolicy_with_binary_with_profile_name(self): + '''Test genpolicy (binary with profile name)''' + profile_name = "some-profile-name" + p = self._gen_policy(extra_args=['--profile-name=%s' % profile_name]) + s = 'profile "%s" "%s" {' % (profile_name, self.binary) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###PROFILEATTACH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_with_binary_without_profile_name(self): + '''Test genpolicy (binary without profile name)''' + p = self._gen_policy() + s = '"%s" {' % (self.binary) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###PROFILEATTACH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_without_binary_with_profile_name(self): + '''Test genpolicy (no binary with profile name)''' + profile_name = "some-profile-name" + args = self.full_args + args.append('--profile-name=%s' % profile_name) + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(None, self.options) + params = easyprof.gen_policy_params(None, self.options) + p = easyp.gen_policy(**params) + s = 'profile "%s" {' % (profile_name) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###PROFILEATTACH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + +# manifest tests + + def test_gen_manifest_policy_with_binary_with_profile_name(self): + '''Test gen_manifest_policy (binary with profile name)''' + m = Manifest("test_gen_manifest_policy") + m.add_binary('/bin/ls') + self._gen_manifest_policy(m) + + def test_gen_manifest_policy_without_binary_with_profile_name(self): + '''Test gen_manifest_policy (no binary with profile name)''' + m = Manifest("test_gen_manifest_policy") + self._gen_manifest_policy(m) + + def test_gen_manifest_policy_templates_system(self): + '''Test gen_manifest_policy (system template)''' + m = Manifest("test_gen_manifest_policy") + m.add_template(self.test_template) + self._gen_manifest_policy(m) + + def test_gen_manifest_policy_templates_system_noprefix(self): + '''Test gen_manifest_policy (system template, no security prefix)''' + m = Manifest("test_gen_manifest_policy") + m.add_template(self.test_template) + self._gen_manifest_policy(m, use_security_prefix=False) + + def test_gen_manifest_abs_path_template(self): + '''Test gen_manifest_policy (abs path template)''' + m = Manifest("test_gen_manifest_policy") + m.add_template("/etc/shadow") + try: + self._gen_manifest_policy(m) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("abs path template name should be invalid") + + def test_gen_manifest_escape_path_templates(self): + '''Test gen_manifest_policy (esc path template)''' + m = Manifest("test_gen_manifest_policy") + m.add_template("../../../../../../../../etc/shadow") + try: + self._gen_manifest_policy(m) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("../ template name should be invalid") + + def test_gen_manifest_policy_templates_nonexistent(self): + '''Test gen manifest policy (nonexistent template)''' + m = Manifest("test_gen_manifest_policy") + m.add_template("nonexistent") + try: + self._gen_manifest_policy(m) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("template should be invalid") + + def test_gen_manifest_policy_comment(self): + '''Test gen manifest policy (comment)''' + s = "test comment" + m = Manifest("test_gen_manifest_policy") + m.add_comment(s) + p = self._gen_manifest_policy(m) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###COMMENT###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_gen_manifest_policy_author(self): + '''Test gen manifest policy (author)''' + s = "Archibald Poindexter" + m = Manifest("test_gen_manifest_policy") + m.add_author(s) + p = self._gen_manifest_policy(m) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###AUTHOR###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_gen_manifest_policy_copyright(self): + '''Test genpolicy (copyright)''' + s = "2112/01/01" + m = Manifest("test_gen_manifest_policy") + m.add_copyright(s) + p = self._gen_manifest_policy(m) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###COPYRIGHT###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_gen_manifest_policy_policygroups(self): + '''Test gen manifest policy (single policygroup)''' + groups = self.test_policygroup + m = Manifest("test_gen_manifest_policy") + m.add_policygroups(groups) + p = self._gen_manifest_policy(m) + + for s in ['#include ', '#include ']: + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###POLICYGROUPS###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_gen_manifest_policy_policygroups_multiple(self): + '''Test genpolicy (multiple policygroups)''' + test_policygroup2 = "test-policygroup2" + contents = ''' + # %s + #include + #include +''' % (self.test_policygroup) + open(os.path.join(self.tmpdir, 'policygroups', test_policygroup2), 'w').write(contents) + + groups = "%s,%s" % (self.test_policygroup, test_policygroup2) + m = Manifest("test_gen_manifest_policy") + m.add_policygroups(groups) + p = self._gen_manifest_policy(m) + + for s in ['#include ', + '#include ', + '#include ', + '#include ']: + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###POLICYGROUPS###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_gen_manifest_policy_policygroups_nonexistent(self): + '''Test gen manifest policy (nonexistent policygroup)''' + groups = "nonexistent" + m = Manifest("test_gen_manifest_policy") + m.add_policygroups(groups) + try: + self._gen_manifest_policy(m) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("policygroup should be invalid") + + def test_gen_manifest_policy_templatevar(self): + '''Test gen manifest policy (template-var single)''' + m = Manifest("test_gen_manifest_policy") + m.add_template_variable("FOO", "bar") + p = self._gen_manifest_policy(m) + s = '@{FOO}="bar"' + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###TEMPLATEVAR###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_gen_manifest_policy_templatevar_multiple(self): + '''Test gen manifest policy (template-var multiple)''' + variables = [["FOO", "bar"], ["BAR", "baz"]] + m = Manifest("test_gen_manifest_policy") + for s in variables: + m.add_template_variable(s[0], s[1]) + + p = self._gen_manifest_policy(m) + for s in variables: + str_s = '@{%s}="%s"' % (s[0], s[1]) + self.assertTrue(str_s in p, "Could not find '%s' in:\n%s" % (str_s, p)) + inv_s = '###TEMPLATEVAR###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_gen_manifest_policy_invalid_keys(self): + '''Test gen manifest policy (invalid keys)''' + keys = ['config_file', + 'debug', + 'help', + 'list-templates', + 'list_templates', + 'show-template', + 'show_template', + 'list-policy-groups', + 'list_policy_groups', + 'show-policy-group', + 'show_policy_group', + 'templates-dir', + 'templates_dir', + 'policy-groups-dir', + 'policy_groups_dir', + 'nonexistent', + 'no_verify', + ] + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + for k in keys: + security = dict() + security["profile_name"] = "test-app" + security[k] = "bad" + j = json.dumps(security, indent=2) + try: + easyprof.parse_manifest(j, self.options) + except easyprof.AppArmorException: + continue + raise Exception ("'%s' should be invalid" % k) + + def test_gen_manifest(self): + '''Test gen_manifest''' + # this should come from manpage + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "opt-application", + "user-application" + ], + "policy_vendor": "somevendor", + "policy_version": 1.0, + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + } + } + } +}''' + + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, "somevendor/1.0")) + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, self.options) + params = easyprof.gen_policy_params(binary, self.options) + + # verify we get the same manifest back + man_new = easyp.gen_manifest(params) + self.assertEquals(m, man_new) + + def test_gen_manifest_ubuntu(self): + '''Test gen_manifest (ubuntu)''' + # this should be based on the manpage (but use existing policy_groups + # and template + m = '''{ + "security": { + "profiles": { + "com.ubuntu.developer.myusername.MyCoolApp": { + "name": "MyCoolApp", + "policy_groups": [ + "opt-application", + "user-application" + ], + "policy_vendor": "ubuntu", + "policy_version": 1.0, + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } +}''' + + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, "ubuntu/1.0")) + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, self.options) + params = easyprof.gen_policy_params(binary, self.options) + + # verify we get the same manifest back + man_new = easyp.gen_manifest(params) + self.assertEquals(m, man_new) + + def test_parse_manifest_no_version(self): + '''Test parse_manifest (vendor with no version)''' + # this should come from manpage + m = '''{ + "security": { + "profiles": { + "com.ubuntu.developer.myusername.MyCoolApp": { + "policy_groups": [ + "opt-application", + "user-application" + ], + "policy_vendor": "ubuntu", + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } +}''' + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + try: + easyprof.AppArmorEasyProfile(binary, self.options) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed on missing version") + + def test_parse_manifest_no_vendor(self): + '''Test parse_manifest (version with no vendor)''' + # this should come from manpage + m = '''{ + "security": { + "profiles": { + "com.ubuntu.developer.myusername.MyCoolApp": { + "policy_groups": [ + "opt-application", + "user-application" + ], + "policy_version": 1.0, + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } +}''' + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + try: + easyprof.AppArmorEasyProfile(binary, self.options) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed on missing vendor") + + def test_parse_manifest_multiple(self): + '''Test parse_manifest_multiple''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "opt-application", + "user-application" + ], + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + }, + "com.ubuntu.developer.myusername.MyCoolApp": { + "policy_groups": [ + "opt-application" + ], + "policy_vendor": "ubuntu", + "policy_version": 1.0, + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } +}''' + + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, "ubuntu/1.0")) + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + profiles = easyprof.parse_manifest(m, self.options) + for (binary, options) in profiles: + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + easyp.gen_manifest(params) + easyp.gen_policy(**params) + + +# verify manifest tests + def _verify_manifest(self, m, expected, invalid=False): + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + try: + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + except easyprof.AppArmorException: + if invalid: + return + raise + params = easyprof.gen_policy_params(binary, options) + if expected: + self.assertTrue(easyprof.verify_manifest(params), "params=%s\nmanifest=%s" % (params,m)) + else: + self.assertFalse(easyprof.verify_manifest(params), "params=%s\nmanifest=%s" % (params,m)) + + def test_verify_manifest_full(self): + '''Test verify_manifest (full)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "abstractions": [ + "base" + ], + "author": "Your Name", + "binary": "/opt/com.example/foo/**", + "comment": "some free-form single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "foo", + "policy_groups": [ + "user-application", + "opt-application" + ], + "template": "user-application", + "template_variables": { + "OK1": "foo", + "OK2": "com.example.foo" + } + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_full_bad(self): + '''Test verify_manifest (full bad)''' + m = '''{ + "security": { + "profiles": { + "/com.example.foo": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/usr/foo/**", + "comment": "some free-form single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "foo", + "policy_groups": [ + "user-application", + "opt-application" + ], + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "VAR1": "f*o", + "VAR2": "*foo", + "VAR3": "fo*", + "VAR4": "b{ar", + "VAR5": "b{a,r}", + "VAR6": "b}ar", + "VAR7": "bar[0-9]", + "VAR8": "b{ar", + "VAR9": "/tmp/../etc/passwd" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + } + } + } +}''' + + self._verify_manifest(m, expected=False, invalid=True) + + def test_verify_manifest_binary(self): + '''Test verify_manifest (binary in /usr)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/usr/foo/**", + "template": "user-application" + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_profile_profile_name_bad(self): + '''Test verify_manifest (bad profile_name)''' + m = '''{ + "security": { + "profiles": { + "/foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' + self._verify_manifest(m, expected=False, invalid=True) + + m = '''{ + "security": { + "profiles": { + "bin/*": { + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' + self._verify_manifest(m, expected=False) + + def test_verify_manifest_profile_profile_name(self): + '''Test verify_manifest (profile_name)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_profile_abstractions(self): + '''Test verify_manifest (abstractions)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "base" + ] + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_profile_abstractions_bad(self): + '''Test verify_manifest (bad abstractions)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "user-tmp" + ] + } + } + } +}''' + self._verify_manifest(m, expected=False) + + def test_verify_manifest_profile_template_var(self): + '''Test verify_manifest (good template_var)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/something with spaces/**", + "template": "user-application", + "template_variables": { + "OK1": "foo", + "OK2": "com.example.foo", + "OK3": "something with spaces" + } + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_profile_template_var_bad(self): + '''Test verify_manifest (bad template_var)''' + for v in ['"VAR1": "f*o"', + '"VAR2": "*foo"', + '"VAR3": "fo*"', + '"VAR4": "b{ar"', + '"VAR5": "b{a,r}"', + '"VAR6": "b}ar"', + '"VAR7": "bar[0-9]"', + '"VAR8": "b{ar"', + '"VAR9": "foo/bar"' # this is valid, but potentially unsafe + ]: + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "template_variables": { + %s + } + } + } + } +}''' % v + self._verify_manifest(m, expected=False) + + def test_manifest_invalid(self): + '''Test invalid manifest (parse error)''' + m = '''{ + "security": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "base" + ] +}''' + self._verify_manifest(m, expected=False, invalid=True) + + def test_manifest_invalid2(self): + '''Test invalid manifest (profile_name is not key)''' + m = '''{ + "security": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "base" + ] + } +}''' + self._verify_manifest(m, expected=False, invalid=True) + + def test_manifest_invalid3(self): + '''Test invalid manifest (profile_name in dict)''' + m = '''{ + "security": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "base" + ], + "profile_name": "com.example.foo" + } +}''' + self._verify_manifest(m, expected=False, invalid=True) + + def test_manifest_invalid4(self): + '''Test invalid manifest (bad path in template var)''' + for v in ['"VAR1": "/tmp/../etc/passwd"', + '"VAR2": "./"', + '"VAR3": "foo\"bar"', + '"VAR4": "foo//bar"', + ]: + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "template_variables": { + %s + } + } + } + } +}''' % v + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + params = easyprof.gen_policy_params(binary, options) + try: + easyprof.verify_manifest(params) + except easyprof.AppArmorException: + return + + raise Exception ("Should have failed with invalid variable declaration") + + +# policy version tests + def test_policy_vendor_manifest_nonexistent(self): + '''Test policy vendor via manifest (nonexistent)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "policy_vendor": "nonexistent", + "policy_version": 1.0, + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' + # Build up our args + args = self.full_args + args.append("--manifest=/dev/null") + + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + try: + easyprof.AppArmorEasyProfile(binary, self.options) + except easyprof.AppArmorException: + return + + raise Exception ("Should have failed with non-existent directory") + + def test_policy_version_manifest(self): + '''Test policy version via manifest (good)''' + policy_vendor = "somevendor" + policy_version = "1.0" + policy_subdir = "%s/%s" % (policy_vendor, policy_version) + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "policy_vendor": "%s", + "policy_version": %s, + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' % (policy_vendor, policy_version) + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, policy_subdir)) + + # Build up our args + args = self.full_args + args.append("--manifest=/dev/null") + + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, self.options) + + tdir = os.path.join(self.tmpdir, 'templates', policy_subdir) + for t in easyp.get_templates(): + self.assertTrue(t.startswith(tdir)) + + pdir = os.path.join(self.tmpdir, 'policygroups', policy_subdir) + for p in easyp.get_policy_groups(): + self.assertTrue(p.startswith(pdir)) + + params = easyprof.gen_policy_params(binary, self.options) + easyp.gen_policy(**params) + + def test_policy_vendor_version_args(self): + '''Test policy vendor and version via command line args (good)''' + policy_version = "1.0" + policy_vendor = "somevendor" + policy_subdir = "%s/%s" % (policy_vendor, policy_version) + + # Create the directories + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, policy_subdir)) + + # Build up our args + args = self.full_args + args.append("--policy-version=%s" % policy_version) + args.append("--policy-vendor=%s" % policy_vendor) + + (self.options, self.args) = easyprof.parse_args(args) + (self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary]) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + tdir = os.path.join(self.tmpdir, 'templates', policy_subdir) + for t in easyp.get_templates(): + self.assertTrue(t.startswith(tdir), \ + "'%s' does not start with '%s'" % (t, tdir)) + + pdir = os.path.join(self.tmpdir, 'policygroups', policy_subdir) + for p in easyp.get_policy_groups(): + self.assertTrue(p.startswith(pdir), \ + "'%s' does not start with '%s'" % (p, pdir)) + + params = easyprof.gen_policy_params(self.binary, self.options) + easyp.gen_policy(**params) + + def test_policy_vendor_args_nonexistent(self): + '''Test policy vendor via command line args (nonexistent)''' + policy_vendor = "nonexistent" + policy_version = "1.0" + args = self.full_args + args.append("--policy-version=%s" % policy_version) + args.append("--policy-vendor=%s" % policy_vendor) + + (self.options, self.args) = easyprof.parse_args(args) + (self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary]) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + return + + raise Exception ("Should have failed with non-existent directory") + + def test_policy_version_args_bad(self): + '''Test policy version via command line args (bad)''' + bad = [ + "../../../../../../etc", + "notanumber", + "v1.0a", + "-1", + ] + for policy_version in bad: + args = self.full_args + args.append("--policy-version=%s" % policy_version) + args.append("--policy-vendor=somevendor") + + (self.options, self.args) = easyprof.parse_args(args) + (self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary]) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + continue + + raise Exception ("Should have failed with bad version") + + def test_policy_vendor_args_bad(self): + '''Test policy vendor via command line args (bad)''' + bad = [ + "../../../../../../etc", + "vendor with space", + "semicolon;isbad", + ] + for policy_vendor in bad: + args = self.full_args + args.append("--policy-vendor=%s" % policy_vendor) + args.append("--policy-version=1.0") + + (self.options, self.args) = easyprof.parse_args(args) + (self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary]) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + continue + + raise Exception ("Should have failed with bad vendor") + +# output_directory tests + def test_output_directory_multiple(self): + '''Test output_directory (multiple)''' + files = dict() + files["com.example.foo"] = "com.example.foo" + files["com.ubuntu.developer.myusername.MyCoolApp"] = "com.ubuntu.developer.myusername.MyCoolApp" + files["usr.bin.baz"] = "/usr/bin/baz" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "opt-application", + "user-application" + ], + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + }, + "%s": { + "policy_groups": [ + "opt-application", + "user-application" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + }, + "%s": { + "abstractions": [ + "gnome" + ], + "policy_groups": [ + "user-application" + ], + "template_variables": { + "APPNAME": "baz" + } + } + } + } +}''' % (files["com.example.foo"], + files["com.ubuntu.developer.myusername.MyCoolApp"], + files["usr.bin.baz"]) + + out_dir = os.path.join(self.tmpdir, "output") + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + profiles = easyprof.parse_manifest(m, self.options) + for (binary, options) in profiles: + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + easyp.output_policy(params, dir=out_dir) + + for fn in files: + f = os.path.join(out_dir, fn) + self.assertTrue(os.path.exists(f), "Could not find '%s'" % f) + + def test_output_directory_single(self): + '''Test output_directory (single)''' + files = dict() + files["com.example.foo"] = "com.example.foo" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "opt-application", + "user-application" + ], + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + } + } + } +}''' % (files["com.example.foo"]) + + out_dir = os.path.join(self.tmpdir, "output") + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + profiles = easyprof.parse_manifest(m, self.options) + for (binary, options) in profiles: + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + easyp.output_policy(params, dir=out_dir) + + for fn in files: + f = os.path.join(out_dir, fn) + self.assertTrue(os.path.exists(f), "Could not find '%s'" % f) + + + + + def test_output_directory_invalid(self): + '''Test output_directory (output directory exists as file)''' + files = dict() + files["usr.bin.baz"] = "/usr/bin/baz" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "gnome" + ], + "policy_groups": [ + "user-application" + ], + "template_variables": { + "APPNAME": "baz" + } + } + } + } +}''' % files["usr.bin.baz"] + + + out_dir = os.path.join(self.tmpdir, "output") + open(out_dir, 'w').close() + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + try: + easyp.output_policy(params, dir=out_dir) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed with 'is not a directory'") + + def test_output_directory_invalid_params(self): + '''Test output_directory (no binary or profile_name)''' + files = dict() + files["usr.bin.baz"] = "/usr/bin/baz" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "gnome" + ], + "policy_groups": [ + "user-application" + ], + "template_variables": { + "APPNAME": "baz" + } + } + } + } +}''' % files["usr.bin.baz"] + + out_dir = os.path.join(self.tmpdir, "output") + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + del params['binary'] + try: + easyp.output_policy(params, dir=out_dir) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed with 'Must specify binary and/or profile name'") + + def test_output_directory_invalid2(self): + '''Test output_directory (profile exists)''' + files = dict() + files["usr.bin.baz"] = "/usr/bin/baz" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "gnome" + ], + "policy_groups": [ + "user-application" + ], + "template_variables": { + "APPNAME": "baz" + } + } + } + } +}''' % files["usr.bin.baz"] + + out_dir = os.path.join(self.tmpdir, "output") + os.mkdir(out_dir) + open(os.path.join(out_dir, "usr.bin.baz"), 'w').close() + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + try: + easyp.output_policy(params, dir=out_dir) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed with 'already exists'") + + def test_output_directory_args(self): + '''Test output_directory (args)''' + files = dict() + files["usr.bin.baz"] = "/usr/bin/baz" + + # Build up our args + args = self.full_args + args.append('--template=%s' % self.test_template) + args.append('--name=%s' % 'foo') + args.append(files["usr.bin.baz"]) + + out_dir = os.path.join(self.tmpdir, "output") + + # Now parse our args + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(files["usr.bin.baz"], self.options) + params = easyprof.gen_policy_params(files["usr.bin.baz"], self.options) + easyp.output_policy(params, dir=out_dir) + + for fn in files: + f = os.path.join(out_dir, fn) + self.assertTrue(os.path.exists(f), "Could not find '%s'" % f) + +# +# utility classes +# + def test_valid_profile_name(self): + '''Test valid_profile_name''' + names = ['foo', + 'com.example.foo', + '/usr/bin/foo', + 'com.example.app_myapp_1:2.3+ab12~foo', + ] + for n in names: + self.assertTrue(easyprof.valid_profile_name(n), "'%s' should be valid" % n) + + def test_valid_profile_name_invalid(self): + '''Test valid_profile_name (invalid)''' + names = ['fo/o', + '/../../etc/passwd', + '../../etc/passwd', + './../etc/passwd', + './etc/passwd', + '/usr/bin//foo', + '/usr/bin/./foo', + 'foo`', + 'foo!', + 'foo@', + 'foo$', + 'foo#', + 'foo%', + 'foo^', + 'foo&', + 'foo*', + 'foo(', + 'foo)', + 'foo=', + 'foo{', + 'foo}', + 'foo[', + 'foo]', + 'foo|', + 'foo/', + 'foo\\', + 'foo;', + 'foo\'', + 'foo"', + 'foo<', + 'foo>', + 'foo?', + 'foo\/', + 'foo,', + '_foo', + ] + for n in names: + self.assertFalse(easyprof.valid_profile_name(n), "'%s' should be invalid" % n) + + def test_valid_path(self): + '''Test valid_path''' + names = ['/bin/bar', + '/etc/apparmor.d/com.example.app_myapp_1:2.3+ab12~foo', + ] + names_rel = ['bin/bar', + 'apparmor.d/com.example.app_myapp_1:2.3+ab12~foo', + 'com.example.app_myapp_1:2.3+ab12~foo', + ] + for n in names: + self.assertTrue(easyprof.valid_path(n), "'%s' should be valid" % n) + for n in names_rel: + self.assertTrue(easyprof.valid_path(n, relative_ok=True), "'%s' should be valid" % n) + + def test_zz_valid_path_invalid(self): + '''Test valid_path (invalid)''' + names = ['/bin//bar', + 'bin/bar', + '/../etc/passwd', + './bin/bar', + './', + ] + names_rel = ['bin/../bar', + 'apparmor.d/../passwd', + 'com.example.app_"myapp_1:2.3+ab12~foo', + ] + for n in names: + self.assertFalse(easyprof.valid_path(n, relative_ok=False), "'%s' should be invalid" % n) + for n in names_rel: + self.assertFalse(easyprof.valid_path(n, relative_ok=True), "'%s' should be invalid" % n) + # # End test class diff --git a/utils/vim/apparmor.vim.in b/utils/vim/apparmor.vim.in index 00df1c993..f03970f37 100644 --- a/utils/vim/apparmor.vim.in +++ b/utils/vim/apparmor.vim.in @@ -160,7 +160,8 @@ syn match sdRLimit /\v^\s*set\s+rlimit\s+(locks|sigpending)\s+\<\=\s+[0-9]+@@EOL syn match sdRLimit /\v^\s*set\s+rlimit\s+(fsize|data|stack|core|rss|as|memlock|msgqueue)\s+\<\=\s+[0-9]+([KMG]B)?@@EOL@@/ contains=sdComment syn match sdRLimit /\v^\s*set\s+rlimit\s+nice\s+\<\=\s+(-1?[0-9]|-20|1?[0-9])@@EOL@@/ contains=sdComment syn match sdRLimit /\v^\s*set\s+rlimit\s+cpu\s+\<\=\s+[0-9]+(seconds|minutes|hours|days)?@@EOL@@/ contains=sdComment -syn match sdRLimit /\v^\s*set\s+rlimit\s+(cpu|nofile|nproc|rtprio|locks|sigpending|fsize|data|stack|core|rss|as|memlock|msgqueue|nice)\s+\<\=\s+infinity@@EOL@@/ contains=sdComment +syn match sdRLimit /\v^\s*set\s+rlimit\s+rttime\s+\<\=\s+[0-9]+(ms|seconds|minutes)?@@EOL@@/ contains=sdComment +syn match sdRLimit /\v^\s*set\s+rlimit\s+(cpu|rttime|nofile|nproc|rtprio|locks|sigpending|fsize|data|stack|core|rss|as|memlock|msgqueue|nice)\s+\<\=\s+infinity@@EOL@@/ contains=sdComment " link rules syn match sdEntryW /\v^\s+@@auditdenyowner@@link\s+(subset\s+)?@@FILENAME@@\s+-\>\s+@@FILENAME@@@@EOL@@/ contains=sdGlob diff --git a/utils/vim/create-apparmor.vim.py b/utils/vim/create-apparmor.vim.py index dc10ffb2c..5cd253dc7 100644 --- a/utils/vim/create-apparmor.vim.py +++ b/utils/vim/create-apparmor.vim.py @@ -15,16 +15,17 @@ import subprocess import sys # dangerous capabilities -danger_caps=["audit_control", - "audit_write", - "mac_override", - "mac_admin", - "set_fcap", - "sys_admin", - "sys_module", - "sys_rawio"] +danger_caps = ["audit_control", + "audit_write", + "mac_override", + "mac_admin", + "set_fcap", + "sys_admin", + "sys_module", + "sys_rawio"] -def cmd(command, input = None, stderr = subprocess.STDOUT, stdout = subprocess.PIPE, stdin = None, timeout = None): + +def cmd(command, input=None, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, stdin=None, timeout=None): '''Try to execute given command (array) and return its stdout, or return a textual error if it failed.''' @@ -36,12 +37,12 @@ def cmd(command, input = None, stderr = subprocess.STDOUT, stdout = subprocess.P out, outerr = sp.communicate(input) # Handle redirection of stdout - if out == None: + if out is None: out = '' # Handle redirection of stderr - if outerr == None: + if outerr is None: outerr = '' - return [sp.returncode,out+outerr] + return [sp.returncode, out + outerr] # get capabilities list (rc, output) = cmd(['make', '-s', '--no-print-directory', 'list_capabilities']) @@ -50,7 +51,7 @@ if rc != 0: exit(rc) capabilities = re.sub('CAP_', '', output.strip()).lower().split(" ") -benign_caps =[] +benign_caps = [] for cap in capabilities: if cap not in danger_caps: benign_caps.append(cap) @@ -73,28 +74,28 @@ for af_pair in af_pairs: # but not in aa_flags... # -> currently (2011-01-11) not, but might come back -aa_network_types=r'\s+tcp|\s+udp|\s+icmp' +aa_network_types = r'\s+tcp|\s+udp|\s+icmp' -aa_flags=['complain', - 'audit', - 'attach_disconnect', - 'no_attach_disconnected', - 'chroot_attach', - 'chroot_no_attach', - 'chroot_relative', - 'namespace_relative'] +aa_flags = ['complain', + 'audit', + 'attach_disconnected', + 'no_attach_disconnected', + 'chroot_attach', + 'chroot_no_attach', + 'chroot_relative', + 'namespace_relative'] -filename=r'(\/|\@\{\S*\})\S*' +filename = r'(\/|\@\{\S*\})\S*' aa_regex_map = { 'FILENAME': filename, - 'FILE': r'\v^\s*(audit\s+)?(deny\s+|allow\s+)?(owner\s+)?' + filename + r'\s+', # Start of a file rule + 'FILE': r'\v^\s*(audit\s+)?(deny\s+|allow\s+)?(owner\s+|other\s+)?' + filename + r'\s+', # Start of a file rule # (whitespace_+_, owner etc. flag_?_, filename pattern, whitespace_+_) - 'DENYFILE': r'\v^\s*(audit\s+)?deny\s+(owner\s+)?' + filename + r'\s+', # deny, otherwise like FILE - 'auditdenyowner': r'(audit\s+)?(deny\s+|allow\s+)?(owner\s+)?', - 'audit_DENY_owner': r'(audit\s+)?deny\s+(owner\s+)?', # must include "deny", otherwise like auditdenyowner + 'DENYFILE': r'\v^\s*(audit\s+)?deny\s+(owner\s+|other\s+)?' + filename + r'\s+', # deny, otherwise like FILE + 'auditdenyowner': r'(audit\s+)?(deny\s+|allow\s+)?(owner\s+|other\s+)?', + 'audit_DENY_owner': r'(audit\s+)?deny\s+(owner\s+|other\s+)?', # must include "deny", otherwise like auditdenyowner 'auditdeny': r'(audit\s+)?(deny\s+|allow\s+)?', - 'EOL': r'\s*,(\s*$|(\s*#.*$)\@=)', # End of a line (whitespace_?_, comma, whitespace_?_ comment.*) + 'EOL': r'\s*,(\s*$|(\s*#.*$)\@=)', # End of a line (whitespace_?_, comma, whitespace_?_ comment.*) 'TRANSITION': r'(\s+-\>\s+\S+)?', 'sdKapKey': " ".join(benign_caps), 'sdKapKeyDanger': " ".join(danger_caps), @@ -104,6 +105,7 @@ aa_regex_map = { 'flags': r'((flags\s*\=\s*)?\(\s*(' + '|'.join(aa_flags) + r')(\s*,\s*(' + '|'.join(aa_flags) + r'))*\s*\)\s+)', } + def my_repl(matchobj): matchobj.group(1) if matchobj.group(1) in aa_regex_map: @@ -112,48 +114,48 @@ def my_repl(matchobj): return matchobj.group(0) -def create_file_rule (highlighting, permissions, comment, denyrule = 0): +def create_file_rule(highlighting, permissions, comment, denyrule=0): - if denyrule == 0: - keywords = '@@auditdenyowner@@' - else: - keywords = '@@audit_DENY_owner@@' # TODO: not defined yet, will be '(audit\s+)?deny\s+(owner\s+)?' + if denyrule == 0: + keywords = '@@auditdenyowner@@' + else: + keywords = '@@audit_DENY_owner@@' # TODO: not defined yet, will be '(audit\s+)?deny\s+(owner\s+)?' - sniplet = '' - sniplet = sniplet + "\n" + '" ' + comment + "\n" + sniplet = '' + sniplet = sniplet + "\n" + '" ' + comment + "\n" - prefix = r'syn match ' + highlighting + r' /\v^\s*' + keywords - suffix = r'@@EOL@@/ contains=sdGlob,sdComment nextgroup=@sdEntry,sdComment,sdError,sdInclude' + "\n" - # filename without quotes - sniplet = sniplet + prefix + r'@@FILENAME@@\s+' + permissions + suffix - # filename with quotes - sniplet = sniplet + prefix + r'"@@FILENAME@@"\s+' + permissions + suffix - # filename without quotes, reverse syntax - sniplet = sniplet + prefix + permissions + r'\s+@@FILENAME@@' + suffix - # filename with quotes, reverse syntax - sniplet = sniplet + prefix + permissions + r'\s+"@@FILENAME@@"+' + suffix + prefix = r'syn match ' + highlighting + r' /\v^\s*' + keywords + suffix = r'@@EOL@@/ contains=sdGlob,sdComment nextgroup=@sdEntry,sdComment,sdError,sdInclude' + "\n" + # filename without quotes + sniplet = sniplet + prefix + r'@@FILENAME@@\s+' + permissions + suffix + # filename with quotes + sniplet = sniplet + prefix + r'"@@FILENAME@@"\s+' + permissions + suffix + # filename without quotes, reverse syntax + sniplet = sniplet + prefix + permissions + r'\s+@@FILENAME@@' + suffix + # filename with quotes, reverse syntax + sniplet = sniplet + prefix + permissions + r'\s+"@@FILENAME@@"+' + suffix - return sniplet + return sniplet filerule = '' -filerule = filerule + create_file_rule ( 'sdEntryWriteExec ', r'(l|r|w|a|m|k|[iuUpPcC]x)+@@TRANSITION@@', 'write + exec/mmap - danger! (known bug: accepts aw to keep things simple)' ) -filerule = filerule + create_file_rule ( 'sdEntryUX', r'(r|m|k|ux|pux)+@@TRANSITION@@', 'ux(mr) - unconstrained entry, flag the line red. also includes pux which is unconstrained if no profile exists' ) -filerule = filerule + create_file_rule ( 'sdEntryUXe', r'(r|m|k|Ux|PUx)+@@TRANSITION@@', 'Ux(mr) and PUx(mr) - like ux + clean environment' ) -filerule = filerule + create_file_rule ( 'sdEntryPX', r'(r|m|k|px|cx|pix|cix)+@@TRANSITION@@', 'px/cx/pix/cix(mrk) - standard exec entry, flag the line blue' ) -filerule = filerule + create_file_rule ( 'sdEntryPXe', r'(r|m|k|Px|Cx|Pix|Cix)+@@TRANSITION@@', 'Px/Cx/Pix/Cix(mrk) - like px/cx + clean environment' ) -filerule = filerule + create_file_rule ( 'sdEntryIX', r'(r|m|k|ix)+', 'ix(mr) - standard exec entry, flag the line green' ) -filerule = filerule + create_file_rule ( 'sdEntryM', r'(r|m|k)+', 'mr - mmap with PROT_EXEC' ) +filerule = filerule + create_file_rule('sdEntryWriteExec ', r'(l|r|w|a|m|k|[iuUpPcC]x)+@@TRANSITION@@', 'write + exec/mmap - danger! (known bug: accepts aw to keep things simple)') +filerule = filerule + create_file_rule('sdEntryUX', r'(r|m|k|ux|pux)+@@TRANSITION@@', 'ux(mr) - unconstrained entry, flag the line red. also includes pux which is unconstrained if no profile exists') +filerule = filerule + create_file_rule('sdEntryUXe', r'(r|m|k|Ux|PUx)+@@TRANSITION@@', 'Ux(mr) and PUx(mr) - like ux + clean environment') +filerule = filerule + create_file_rule('sdEntryPX', r'(r|m|k|px|cx|pix|cix)+@@TRANSITION@@', 'px/cx/pix/cix(mrk) - standard exec entry, flag the line blue') +filerule = filerule + create_file_rule('sdEntryPXe', r'(r|m|k|Px|Cx|Pix|Cix)+@@TRANSITION@@', 'Px/Cx/Pix/Cix(mrk) - like px/cx + clean environment') +filerule = filerule + create_file_rule('sdEntryIX', r'(r|m|k|ix)+', 'ix(mr) - standard exec entry, flag the line green') +filerule = filerule + create_file_rule('sdEntryM', r'(r|m|k)+', 'mr - mmap with PROT_EXEC') -filerule = filerule + create_file_rule ( 'sdEntryM', r'(r|m|k|x)+', 'special case: deny x is allowed (does not need to be ix, px, ux or cx)', 1) +filerule = filerule + create_file_rule('sdEntryM', r'(r|m|k|x)+', 'special case: deny x is allowed (does not need to be ix, px, ux or cx)', 1) #syn match sdEntryM /@@DENYFILE@@(r|m|k|x)+@@EOL@@/ contains=sdGlob,sdComment nextgroup=@sdEntry,sdComment,sdError,sdInclude -filerule = filerule + create_file_rule ( 'sdError', r'\S*(w\S*a|a\S*w)\S*', 'write + append is an error' ) -filerule = filerule + create_file_rule ( 'sdEntryW', r'(l|r|w|k)+', 'write entry, flag the line yellow' ) -filerule = filerule + create_file_rule ( 'sdEntryW', r'(l|r|a|k)+', 'append entry, flag the line yellow' ) -filerule = filerule + create_file_rule ( 'sdEntryK', r'[rlk]+', 'read entry + locking, currently no highlighting' ) -filerule = filerule + create_file_rule ( 'sdEntryR', r'[rl]+', 'read entry, no highlighting' ) +filerule = filerule + create_file_rule('sdError', r'\S*(w\S*a|a\S*w)\S*', 'write + append is an error') +filerule = filerule + create_file_rule('sdEntryW', r'(l|r|w|k)+', 'write entry, flag the line yellow') +filerule = filerule + create_file_rule('sdEntryW', r'(l|r|a|k)+', 'append entry, flag the line yellow') +filerule = filerule + create_file_rule('sdEntryK', r'[rlk]+', 'read entry + locking, currently no highlighting') +filerule = filerule + create_file_rule('sdEntryR', r'[rl]+', 'read entry, no highlighting') # " special case: deny x is allowed (doesn't need to be ix, px, ux or cx) # syn match sdEntryM /@@DENYFILE@@(r|m|k|x)+@@EOL@@/ contains=sdGlob,sdComment nextgroup=@sdEntry,sdComment,sdError,sdInclude @@ -174,5 +176,4 @@ with open("apparmor.vim.in") as template: sys.stdout.write("\n\n\n\n") sys.stdout.write('" file rules added with create_file_rule()\n') -sys.stdout.write(re.sub(regex, my_repl, filerule)+'\n') - +sys.stdout.write(re.sub(regex, my_repl, filerule) + '\n')