2
# ------------------------------------------------------------------
4
# Copyright (C) 2009-2010 Canonical Ltd.
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of version 2 of the GNU General Public
8
# License published by the Free Software Foundation.
10
# ------------------------------------------------------------------
12
# /etc/apparmor/notify.conf:
13
# # set to 'yes' to enable AppArmor DENIED notifications
14
# show_notifications="yes"
16
# # only people in use_group can run this script
19
# $HOME/.apparmor/notify.conf can have:
20
# # set to 'yes' to enable AppArmor DENIED notifications
21
# show_notifications="yes"
26
no warnings qw( once );
31
require File::Basename;
36
my $conf = "/etc/apparmor/notify.conf";
37
my $user_conf = "$ENV{HOME}/.apparmor/notify.conf";
38
my $notify_exe = "/usr/bin/notify-send";
39
my $last_exe = "/usr/bin/last";
40
my $ps_exe = "/bin/ps";
41
my $url = "https://wiki.ubuntu.com/DebuggingApparmor";
42
my $nobody_user = "nobody";
43
my $nobody_group = "nogroup";
49
sub kill_running_daemons;
65
$ENV{PATH} = "/bin:/usr/bin";
66
$ENV{SHELL} = "/bin/sh";
67
defined($ENV{IFS}) and $ENV{IFS} = ' \t\n';
69
my $prog = File::Basename::basename($0);
71
if ($prog !~ /^[a-zA-Z0-9_\-]+$/) {
72
print STDERR "ERROR: bad programe name '$prog'\n";
76
$> == $< or die "Cannot be suid\n";
77
$) == $( or die "Cannot be sgid\n";
94
'since-last|l' => \$opt_l,
96
'verbose|v' => \$opt_v,
97
'file|f=s' => \$opt_f,
98
'since-days|s=n' => \$opt_s,
99
'user|u=s' => \$opt_u,
100
'wait|w=n' => \$opt_w,
107
# monitor file specified with -f, else use audit.log if auditd is running,
109
our $logfile = "/var/log/kern.log";
111
-f $opt_f or die "'$opt_f' does not exist. Aborting\n";
114
-e "/var/run/auditd.pid" and $logfile = "/var/log/audit/audit.log";
117
-r $logfile or die "Cannot read '$logfile'\n";
118
our $logfile_inode = get_logfile_inode($logfile);
119
our $logfile_size = get_logfile_size($logfile);
120
open (LOGFILE, "<$logfile") or die "Could not open '$logfile'\n";
121
# Drop priviliges, if running as root
124
if (defined($ENV{SUDO_UID}) and defined($ENV{SUDO_GID})) {
125
POSIX::setgid($ENV{SUDO_GID}) or _error("Could not change gid");
126
$> = $ENV{SUDO_UID} or _error("Could not change euid");
127
defined($ENV{SUDO_USER}) and $login = $ENV{SUDO_USER};
129
my $drop_to = $nobody_user;
134
POSIX::setgid(scalar(getgrnam($nobody_group))) or _error("Could not change gid to '$nobody_group'");
135
$> = scalar(getpwnam($drop_to)) or _error("Could not change euid to '$drop_to'");
139
defined $login or $login = $ENV{'USER'};
144
if (defined($prefs{use_group})) {
145
my ($name, $passwd, $gid, $members) = getgrnam($prefs{use_group});
146
if (not defined($members) or not defined($login) or (not grep { $_ eq $login } split(/ /, $members) and $login ne "root")) {
147
_error("'$login' must be in '$prefs{use_group}' group. Aborting");
153
-x "$notify_exe" or _error("Could not find '$notify_exe'. Please install libnotify-bin. Aborting");
155
-x "$last_exe" or _error("Could not find '$last_exe'. Aborting");
157
if ($opt_s and not $opt_l) {
158
$opt_s =~ /^[0-9]+$/ or _error("-s requires a number");
162
$opt_w =~ /^[0-9]+$/ or _error("-w requires a number");
165
if ($opt_p or $opt_l) {
167
readconf($user_conf);
170
if (defined($prefs{show_notifications}) and $prefs{show_notifications} ne "yes") {
171
_debug("'show_notifications' is disabled. Exiting");
181
} elsif ($opt_s and not $opt_p) {
182
do_show_messages($opt_s);
195
-r $cfg or die "'$cfg' does not exist\n";
197
open (CFG, "<$cfg") or die "Could not open '$cfg'\n";
200
s/#.*//; # no comments
201
s/^\s+//; # no leading white
202
s/\s+$//; # no trailing white
203
next unless length; # anything left?
204
my ($var, $value) = split(/\s*=\s*/, $_, 2);
205
if ($var eq "show_notifications" or $var eq "use_group") {
206
$value =~ s/^"(.*)"$/$1/g;
207
$prefs{$var} = $value;
215
my $msg = $params[0];
218
#_debug("processing: $msg");
220
my ($test) = LibAppArmorc::parse_record($msg);
222
# Don't show logs before certain date
223
my $date = LibAppArmor::aa_log_record::swig_epoch_get($test);
225
if (defined($date) and $#params > 0 and $params[1] =~ /^[0-9]+$/) {
226
$since = int($params[1]);
227
int($date) >= $since or goto err;
230
# ignore all but status and denied messages
231
my $type = LibAppArmor::aa_log_record::swig_event_get($test);
233
$type == $LibAppArmor::AA_RECORD_DENIED or goto err;
235
my $profile = LibAppArmor::aa_log_record::swig_profile_get($test);
236
my $operation = LibAppArmor::aa_log_record::swig_operation_get($test);
237
my $name = LibAppArmor::aa_log_record::swig_name_get($test);
238
my $denied = LibAppArmor::aa_log_record::swig_denied_mask_get($test);
239
my $family = LibAppArmor::aa_log_record::swig_net_family_get($test);
240
my $sock_type = LibAppArmor::aa_log_record::swig_net_sock_type_get($test);
241
LibAppArmorc::free_record($test);
243
return ($profile, $operation, $name, $denied, $family, $sock_type, $date);
246
LibAppArmorc::free_record($test);
251
my ($profile, $operation, $name, $denied, $family, $sock_type, $date) = @_;
254
defined($profile) and $formatted .= "Profile: $profile\n";
255
defined($operation) and $formatted .= "Operation: $operation\n";
256
defined($name) and $formatted .= "Name: $name\n";
257
defined($denied) and $formatted .= "Denied: $denied\n";
258
defined($family) and defined ($sock_type) and $formatted .= "Family: $family\nSocket type: $sock_type\n";
259
$formatted .= "Logfile: $logfile\n";
268
print "AppArmor denial";
269
$num > 1 and print "s";
270
print ": $num (since " . scalar(localtime($time)) . ")\n";
271
$opt_v and print "For more information, please see: $url\n";
275
sub kill_running_daemons {
276
# Look for other daemon instances of this script and kill them. This
277
# can happen on logout and back in (in which case $notify_exe fails
278
# anyway). 'ps xw' should output something like:
279
# 9987 ? Ss 0:01 /usr/bin/perl ./bin/aa-notify -p
280
# 10170 ? Ss 0:00 /usr/bin/perl ./bin/aa-notify -p
281
open(PS,"$ps_exe xw|") or die "Unable to run '$ps_exe':$!\n";
284
/$prog -[ps]/ or next;
286
my @line = split(/\s+/, $_);
287
if ($line[5] =~ /$prog$/ and ($line[6] eq "-p" or $line[6] eq "-s")) {
288
if ($line[0] != $$) {
289
_warn("killing old daemon '$line[0]'");
301
if ($pid == 0) { # child
302
# notify-send needs $< to be the unprivileged user
305
# 'system' uses execvp() so no shell metacharacters here.
306
# $notify_exe is an absolute path so execvp won't search PATH.
307
system "$notify_exe", "-i", "gtk-dialog-warning", "-u", "critical", "--", "AppArmor Message", "$msg";
308
my $exit_code = $? >> 8;
320
our $time_to_die = 0;
322
print "Starting aa-notify\n";
323
kill_running_daemons();
325
# Daemonize, but not if in debug mode
327
chdir('/') or die "Can't chdir to /: $!";
329
open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
330
open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
331
#open STDERR, '>/dev/null' or die "Can't write to /dev/null: $!";
334
die "Couldn't fork: $!" unless defined($pid);
335
POSIX::setsid() or die "Can't start a new session: $!";
341
$SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&signal_handler;
342
$SIG{'PIPE'} = 'IGNORE';
349
my $footer = "For more information, please see:\n$url";
352
if ($opt_s and int($opt_s) > 0) {
353
$since = $since - (int($opt_s) * 60 * 60 * 24);
355
for (my $i=0; $time_to_die == 0; $i++) {
356
if ($logfile_inode != get_logfile_inode($logfile)) {
357
_warn("$logfile changed inodes, reopening");
359
} elsif (get_logfile_size($logfile) < $logfile_size) {
360
_warn("$logfile is smaller, reopening");
363
while(my $msg = <LOGFILE>) {
365
if ($first_run == 1) {
366
if ($since != $now) {
367
@attrib = parse_message($msg, $since);
370
@attrib = parse_message($msg);
372
$#attrib > 0 or next;
373
if ($first_run == 1) {
378
my ($profile, $operation, $name, $denied, $family, $sock_type, $date) = @attrib;
380
# Rate limit messages by creating a hash whose keys are:
381
# - for files: $profile|$name|$denied|
382
# - for everything else: $profile|$operation|$name|$denied|$family|$sock_type| (as available)
383
# The value for the key is a timestamp (epoch) and we won't show
384
# messages whose key has a timestamp from less than 5 seconds afo
386
defined($profile) and $k .= "$profile|";
387
if (defined($name) and defined($denied)) {
388
$k .= "$name|$denied|"; # for file access, don't worry about operation
390
defined($operation) and $k .= "$operation|";
391
defined($name) and $k .= "$name|";
392
defined($denied) and $k .= "$denied|";
393
defined($family) and defined ($sock_type) and $k .= "$family|$sock_type|";
396
# don't display same message if seen in last 5 seconds
397
if (not defined($seen{$k})) {
401
$now - $seen{$k} < $seconds and next;
405
my $m = format_message(@attrib);
410
my $rc = send_message($m);
412
_warn("'$notify_exe' exited with error '$rc'");
417
# from seek() in Programming Perl
423
my $m = "$logfile contains $count denied message";
424
$count > 1 and $m .= "s";
426
$m .= " in the last ";
440
# clean out the %seen database every 30 seconds
442
foreach my $k (keys %seen) {
444
$now - $seen{$k} > $seconds and delete $seen{$k} and _debug("deleted $k");
447
_debug("done purging");
448
foreach my $k (keys %seen) {
449
_debug("remaining key: $k: $seen{$k}");
453
print STDERR "Stopping aa-notify\n";
461
while(my $msg = <LOGFILE>) {
462
my @attrib = parse_message($msg, $_[0]);
463
$#attrib > 0 or next;
465
my $m = format_message(@attrib);
467
my $date = $attrib[6];
469
if (exists($msg_hash{$m})) {
471
defined($date) and $last_date{$m} = scalar(localtime($date));
480
foreach my $m (@msg_list) {
482
if ($msg_hash{$m} gt 1) {
483
print "($msg_hash{$m} found";
484
if (exists($last_date{$m})) {
485
print ", most recent from '$last_date{$m}'";
496
open(LAST,"$last_exe -F -a $login|") or die "Unable to run $last_exe:$!\n";
498
while(my $line = <LAST>) {
499
_debug("Checking '$line'");
500
$line =~ /^$login/ or next;
501
$line !~ /^$login\s+pts.*\s+:[0-9]+\.[0-9]+$/ or next; # ignore xterm and friends
502
my @entry = split(/\s+/, $line);
503
my ($hour, $min, $sec) = (split(/:/, $entry[5]))[0,1,2];
504
$time = Time::Local::timelocal($sec, $min, $hour, $entry[4], $entry[3], $entry[6]);
508
$time > 0 or _error("Couldn't find last login");
510
format_stats(show_since($time), $time);
513
sub do_show_messages {
514
my $since = $now - (int($_[0]) * 60 * 60 * 24);
515
format_stats(show_since($since), $since);
520
print STDERR "aa-notify: WARN: $msg\n";
524
print STDERR "aa-notify: ERROR: $msg\n";
531
print STDERR "aa-notify: DEBUG: $msg\n";
542
USAGE: aa-notify [OPTIONS]
544
Display AppArmor notifications or messages for DENIED entries.
547
-p, --poll poll AppArmor logs and display notifications
548
-f FILE, --file=FILE search FILE for AppArmor messages
549
-l, --since-last display stats since last login
550
-s NUM, --since-days=NUM show stats for last NUM days (can be used alone
552
-v, --verbose show messages with stats
553
-h, --help display this help
554
-u USER, --user=USER user to drop privileges to when not using sudo
555
-w NUM, --wait=NUM wait NUM seconds before displaying
556
notifications (with -p)
562
# reopen the logfile, temporarily switching back to starting euid for
569
_debug("raising privileges to '$orig_euid' in reopen_logfile()");
572
$> == $orig_euid or die "Could not raise privileges\n";
575
$logfile_inode = get_logfile_inode($logfile);
576
$logfile_size = get_logfile_size($logfile);
577
open (LOGFILE, "<$logfile") or die "Could not open '$logfile'\n";
580
_debug("dropping privileges to '$old_euid' in reopen_logfile()");
582
$> == $old_euid or die "Could not drop privileges\n";
586
sub get_logfile_size {
589
defined(($size = (stat($fn))[7])) or (sleep(10) and defined(($size = (stat($fn))[7])) or die "'$fn' disappeared. Aborting\n");
593
sub get_logfile_inode {
596
defined(($inode = (stat($fn))[1])) or (sleep(10) and defined(($inode = (stat($fn))[1])) or die "'$fn' disappeared. Aborting\n");