mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 16:35:02 +01:00

of a mix of symlinks to non-prefixed comands, and "apparmor_" prefixed commands. This also refactors the manpage generation slightly since we no longer need special cases for the manpages, and drops aa-eventd from the default list of tools to install (it also lacks a manpage).
602 lines
17 KiB
Perl
Executable file
602 lines
17 KiB
Perl
Executable file
#!/usr/bin/perl
|
|
# ------------------------------------------------------------------
|
|
#
|
|
# Copyright (C) 2009-2010 Canonical Ltd.
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of version 2 of the GNU General Public
|
|
# License published by the Free Software Foundation.
|
|
#
|
|
# ------------------------------------------------------------------
|
|
#
|
|
# /etc/apparmor/notify.conf:
|
|
# # set to 'yes' to enable AppArmor DENIED notifications
|
|
# show_notifications="yes"
|
|
#
|
|
# # only people in use_group can run this script
|
|
# use_group="admin"
|
|
#
|
|
# $HOME/.apparmor/notify.conf can have:
|
|
# # set to 'yes' to enable AppArmor DENIED notifications
|
|
# show_notifications="yes"
|
|
#
|
|
|
|
use strict;
|
|
use warnings;
|
|
no warnings qw( once );
|
|
|
|
require LibAppArmor;
|
|
require POSIX;
|
|
require Time::Local;
|
|
require File::Basename;
|
|
|
|
use Getopt::Long;
|
|
|
|
my %prefs;
|
|
my $conf = "/etc/apparmor/notify.conf";
|
|
my $user_conf = "$ENV{HOME}/.apparmor/notify.conf";
|
|
my $notify_exe = "/usr/bin/notify-send";
|
|
my $last_exe = "/usr/bin/last";
|
|
my $ps_exe = "/bin/ps";
|
|
my $url = "https://wiki.ubuntu.com/DebuggingApparmor";
|
|
my $nobody_user = "nobody";
|
|
my $nobody_group = "nogroup";
|
|
|
|
sub readconf;
|
|
sub parse_message;
|
|
sub format_message;
|
|
sub format_stats;
|
|
sub kill_running_daemons;
|
|
sub do_notify;
|
|
sub show_since;
|
|
sub do_last;
|
|
sub do_show_messages;
|
|
sub _error;
|
|
sub _warn;
|
|
sub _debug;
|
|
sub exitscript;
|
|
sub usage;
|
|
|
|
#
|
|
# Main script
|
|
#
|
|
|
|
# Clean environment
|
|
$ENV{PATH} = "/bin:/usr/bin";
|
|
$ENV{SHELL} = "/bin/sh";
|
|
defined($ENV{IFS}) and $ENV{IFS} = ' \t\n';
|
|
|
|
my $prog = File::Basename::basename($0);
|
|
|
|
if ($prog !~ /^[a-zA-Z0-9_\-]+$/) {
|
|
print STDERR "ERROR: bad programe name '$prog'\n";
|
|
exitscript(1);
|
|
}
|
|
|
|
$> == $< or die "Cannot be suid\n";
|
|
$) == $( or die "Cannot be sgid\n";
|
|
|
|
my $login;
|
|
our $orig_euid = $>;
|
|
|
|
my $opt_d = '';
|
|
my $opt_h = '';
|
|
my $opt_l = '';
|
|
my $opt_p = '';
|
|
my $opt_v = '';
|
|
my $opt_f = '';
|
|
my $opt_s = 0;
|
|
my $opt_u = '';
|
|
my $opt_w = 0;
|
|
GetOptions(
|
|
'debug|d' => \$opt_d,
|
|
'help|h' => \$opt_h,
|
|
'since-last|l' => \$opt_l,
|
|
'poll|p' => \$opt_p,
|
|
'verbose|v' => \$opt_v,
|
|
'file|f=s' => \$opt_f,
|
|
'since-days|s=n' => \$opt_s,
|
|
'user|u=s' => \$opt_u,
|
|
'wait|w=n' => \$opt_w,
|
|
);
|
|
if ($opt_h) {
|
|
usage;
|
|
exitscript(0);
|
|
}
|
|
|
|
# monitor file specified with -f, else use audit.log if auditd is running,
|
|
# otherwise kern.log
|
|
our $logfile = "/var/log/kern.log";
|
|
if ($opt_f) {
|
|
-f $opt_f or die "'$opt_f' does not exist. Aborting\n";
|
|
$logfile = $opt_f;
|
|
} else {
|
|
-e "/var/run/auditd.pid" and $logfile = "/var/log/audit/audit.log";
|
|
}
|
|
|
|
-r $logfile or die "Cannot read '$logfile'\n";
|
|
our $logfile_inode = get_logfile_inode($logfile);
|
|
our $logfile_size = get_logfile_size($logfile);
|
|
open (LOGFILE, "<$logfile") or die "Could not open '$logfile'\n";
|
|
# Drop priviliges, if running as root
|
|
if ($< == 0) {
|
|
$login = "root";
|
|
if (defined($ENV{SUDO_UID}) and defined($ENV{SUDO_GID})) {
|
|
POSIX::setgid($ENV{SUDO_GID}) or _error("Could not change gid");
|
|
$> = $ENV{SUDO_UID} or _error("Could not change euid");
|
|
defined($ENV{SUDO_USER}) and $login = $ENV{SUDO_USER};
|
|
} else {
|
|
my $drop_to = $nobody_user;
|
|
if ($opt_u) {
|
|
$drop_to = $opt_u;
|
|
}
|
|
# nobody/nogroup
|
|
POSIX::setgid(scalar(getgrnam($nobody_group))) or _error("Could not change gid to '$nobody_group'");
|
|
$> = scalar(getpwnam($drop_to)) or _error("Could not change euid to '$drop_to'");
|
|
}
|
|
} else {
|
|
$login = getlogin();
|
|
defined $login or $login = $ENV{'USER'};
|
|
}
|
|
|
|
if (-s $conf) {
|
|
readconf($conf);
|
|
if (defined($prefs{use_group})) {
|
|
my ($name, $passwd, $gid, $members) = getgrnam($prefs{use_group});
|
|
if (not defined($members) or not defined($login) or (not grep { $_ eq $login } split(/ /, $members) and $login ne "root")) {
|
|
_error("'$login' must be in '$prefs{use_group}' group. Aborting");
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($opt_p) {
|
|
-x "$notify_exe" or _error("Could not find '$notify_exe'. Please install libnotify-bin. Aborting");
|
|
} elsif ($opt_l) {
|
|
-x "$last_exe" or _error("Could not find '$last_exe'. Aborting");
|
|
}
|
|
if ($opt_s and not $opt_l) {
|
|
$opt_s =~ /^[0-9]+$/ or _error("-s requires a number");
|
|
}
|
|
|
|
if ($opt_w) {
|
|
$opt_w =~ /^[0-9]+$/ or _error("-w requires a number");
|
|
}
|
|
|
|
if ($opt_p or $opt_l) {
|
|
if (-s $user_conf) {
|
|
readconf($user_conf);
|
|
}
|
|
|
|
if (defined($prefs{show_notifications}) and $prefs{show_notifications} ne "yes") {
|
|
_debug("'show_notifications' is disabled. Exiting");
|
|
exitscript(0);
|
|
}
|
|
}
|
|
|
|
my $now = time();
|
|
if ($opt_p) {
|
|
do_notify();
|
|
} elsif ($opt_l) {
|
|
do_last();
|
|
} elsif ($opt_s and not $opt_p) {
|
|
do_show_messages($opt_s);
|
|
} else {
|
|
usage;
|
|
exitscript(1);
|
|
}
|
|
|
|
exitscript(0);
|
|
|
|
#
|
|
# Subroutines
|
|
#
|
|
sub readconf {
|
|
my $cfg = $_[0];
|
|
-r $cfg or die "'$cfg' does not exist\n";
|
|
|
|
open (CFG, "<$cfg") or die "Could not open '$cfg'\n";
|
|
while (<CFG>) {
|
|
chomp;
|
|
s/#.*//; # no comments
|
|
s/^\s+//; # no leading white
|
|
s/\s+$//; # no trailing white
|
|
next unless length; # anything left?
|
|
my ($var, $value) = split(/\s*=\s*/, $_, 2);
|
|
if ($var eq "show_notifications" or $var eq "use_group") {
|
|
$value =~ s/^"(.*)"$/$1/g;
|
|
$prefs{$var} = $value;
|
|
}
|
|
}
|
|
close(CFG);
|
|
}
|
|
|
|
sub parse_message {
|
|
my @params = @_;
|
|
my $msg = $params[0];
|
|
|
|
chomp($msg);
|
|
#_debug("processing: $msg");
|
|
|
|
my ($test) = LibAppArmorc::parse_record($msg);
|
|
|
|
# Don't show logs before certain date
|
|
my $date = LibAppArmor::aa_log_record::swig_epoch_get($test);
|
|
my $since = 0;
|
|
if (defined($date) and $#params > 0 and $params[1] =~ /^[0-9]+$/) {
|
|
$since = int($params[1]);
|
|
int($date) >= $since or goto err;
|
|
}
|
|
|
|
# ignore all but status and denied messages
|
|
my $type = LibAppArmor::aa_log_record::swig_event_get($test);
|
|
|
|
$type == $LibAppArmor::AA_RECORD_DENIED or goto err;
|
|
|
|
my $profile = LibAppArmor::aa_log_record::swig_profile_get($test);
|
|
my $operation = LibAppArmor::aa_log_record::swig_operation_get($test);
|
|
my $name = LibAppArmor::aa_log_record::swig_name_get($test);
|
|
my $denied = LibAppArmor::aa_log_record::swig_denied_mask_get($test);
|
|
my $family = LibAppArmor::aa_log_record::swig_net_family_get($test);
|
|
my $sock_type = LibAppArmor::aa_log_record::swig_net_sock_type_get($test);
|
|
LibAppArmorc::free_record($test);
|
|
|
|
return ($profile, $operation, $name, $denied, $family, $sock_type, $date);
|
|
|
|
err:
|
|
LibAppArmorc::free_record($test);
|
|
return ();
|
|
}
|
|
|
|
sub format_message {
|
|
my ($profile, $operation, $name, $denied, $family, $sock_type, $date) = @_;
|
|
|
|
my $formatted = "";
|
|
defined($profile) and $formatted .= "Profile: $profile\n";
|
|
defined($operation) and $formatted .= "Operation: $operation\n";
|
|
defined($name) and $formatted .= "Name: $name\n";
|
|
defined($denied) and $formatted .= "Denied: $denied\n";
|
|
defined($family) and defined ($sock_type) and $formatted .= "Family: $family\nSocket type: $sock_type\n";
|
|
$formatted .= "Logfile: $logfile\n";
|
|
|
|
return $formatted;
|
|
}
|
|
|
|
sub format_stats {
|
|
my $num = $_[0];
|
|
my $time = $_[1];
|
|
if ($num > 0) {
|
|
print "AppArmor denial";
|
|
$num > 1 and print "s";
|
|
print ": $num (since " . scalar(localtime($time)) . ")\n";
|
|
$opt_v and print "For more information, please see: $url\n";
|
|
}
|
|
}
|
|
|
|
sub kill_running_daemons {
|
|
# Look for other daemon instances of this script and kill them. This
|
|
# can happen on logout and back in (in which case $notify_exe fails
|
|
# anyway). 'ps xw' should output something like:
|
|
# 9987 ? Ss 0:01 /usr/bin/perl ./bin/aa-notify -p
|
|
# 10170 ? Ss 0:00 /usr/bin/perl ./bin/aa-notify -p
|
|
open(PS,"$ps_exe xw|") or die "Unable to run '$ps_exe':$!\n";
|
|
while(<PS>) {
|
|
chomp;
|
|
/$prog -[ps]/ or next;
|
|
s/^\s+//;
|
|
my @line = split(/\s+/, $_);
|
|
if ($line[5] =~ /$prog$/ and ($line[6] eq "-p" or $line[6] eq "-s")) {
|
|
if ($line[0] != $$) {
|
|
_warn("killing old daemon '$line[0]'");
|
|
kill 15, ($line[0]);
|
|
}
|
|
}
|
|
}
|
|
close(PS);
|
|
}
|
|
|
|
sub send_message {
|
|
my $msg = $_[0];
|
|
|
|
my $pid = fork();
|
|
if ($pid == 0) { # child
|
|
# notify-send needs $< to be the unprivileged user
|
|
$< = $>;
|
|
|
|
# 'system' uses execvp() so no shell metacharacters here.
|
|
# $notify_exe is an absolute path so execvp won't search PATH.
|
|
system "$notify_exe", "-i", "gtk-dialog-warning", "-u", "critical", "--", "AppArmor Message", "$msg";
|
|
my $exit_code = $? >> 8;
|
|
exit($exit_code);
|
|
}
|
|
|
|
# parent
|
|
waitpid($pid, 0);
|
|
return $?;
|
|
}
|
|
|
|
sub do_notify {
|
|
my %seen;
|
|
my $seconds = 5;
|
|
our $time_to_die = 0;
|
|
|
|
print "Starting aa-notify\n";
|
|
kill_running_daemons();
|
|
|
|
# Daemonize, but not if in debug mode
|
|
if (not $opt_d) {
|
|
chdir('/') or die "Can't chdir to /: $!";
|
|
umask 0;
|
|
open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
|
|
open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
|
|
#open STDERR, '>/dev/null' or die "Can't write to /dev/null: $!";
|
|
my $pid = fork();
|
|
exit if $pid;
|
|
die "Couldn't fork: $!" unless defined($pid);
|
|
POSIX::setsid() or die "Can't start a new session: $!";
|
|
}
|
|
|
|
sub signal_handler {
|
|
$time_to_die = 1;
|
|
}
|
|
$SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&signal_handler;
|
|
$SIG{'PIPE'} = 'IGNORE';
|
|
|
|
if ($opt_w) {
|
|
sleep($opt_w);
|
|
}
|
|
|
|
my $count = 0;
|
|
my $footer = "For more information, please see:\n$url";
|
|
my $first_run = 1;
|
|
my $since = $now;
|
|
if ($opt_s and int($opt_s) > 0) {
|
|
$since = $since - (int($opt_s) * 60 * 60 * 24);
|
|
}
|
|
for (my $i=0; $time_to_die == 0; $i++) {
|
|
if ($logfile_inode != get_logfile_inode($logfile)) {
|
|
_warn("$logfile changed inodes, reopening");
|
|
reopen_logfile();
|
|
} elsif (get_logfile_size($logfile) < $logfile_size) {
|
|
_warn("$logfile is smaller, reopening");
|
|
reopen_logfile();
|
|
}
|
|
while(my $msg = <LOGFILE>) {
|
|
my @attrib;
|
|
if ($first_run == 1) {
|
|
if ($since != $now) {
|
|
@attrib = parse_message($msg, $since);
|
|
}
|
|
} else {
|
|
@attrib = parse_message($msg);
|
|
}
|
|
$#attrib > 0 or next;
|
|
if ($first_run == 1) {
|
|
$count++;
|
|
next;
|
|
}
|
|
|
|
my ($profile, $operation, $name, $denied, $family, $sock_type, $date) = @attrib;
|
|
|
|
# Rate limit messages by creating a hash whose keys are:
|
|
# - for files: $profile|$name|$denied|
|
|
# - for everything else: $profile|$operation|$name|$denied|$family|$sock_type| (as available)
|
|
# The value for the key is a timestamp (epoch) and we won't show
|
|
# messages whose key has a timestamp from less than 5 seconds afo
|
|
my $k = "";
|
|
defined($profile) and $k .= "$profile|";
|
|
if (defined($name) and defined($denied)) {
|
|
$k .= "$name|$denied|"; # for file access, don't worry about operation
|
|
} else {
|
|
defined($operation) and $k .= "$operation|";
|
|
defined($name) and $k .= "$name|";
|
|
defined($denied) and $k .= "$denied|";
|
|
defined($family) and defined ($sock_type) and $k .= "$family|$sock_type|";
|
|
}
|
|
|
|
# don't display same message if seen in last 5 seconds
|
|
if (not defined($seen{$k})) {
|
|
$seen{$k} = time();
|
|
} else {
|
|
my $now = time();
|
|
$now - $seen{$k} < $seconds and next;
|
|
$seen{$k} = $now;
|
|
}
|
|
|
|
my $m = format_message(@attrib);
|
|
$m ne "" or next;
|
|
|
|
$m .= $footer;
|
|
|
|
my $rc = send_message($m);
|
|
if ($rc != 0) {
|
|
_warn("'$notify_exe' exited with error '$rc'");
|
|
$time_to_die = 1;
|
|
last;
|
|
}
|
|
}
|
|
# from seek() in Programming Perl
|
|
seek(LOGFILE, 0, 1);
|
|
sleep(1);
|
|
|
|
if ($first_run) {
|
|
if ($count > 0) {
|
|
my $m = "$logfile contains $count denied message";
|
|
$count > 1 and $m .= "s";
|
|
if ($opt_s) {
|
|
$m .= " in the last ";
|
|
if ($opt_s > 1) {
|
|
$m .= "$opt_s days";
|
|
} else {
|
|
$m .= "day";
|
|
}
|
|
}
|
|
$m .= ". ";
|
|
$m .= $footer;
|
|
send_message($m);
|
|
}
|
|
$first_run = 0;
|
|
}
|
|
|
|
# clean out the %seen database every 30 seconds
|
|
if ($i > 30) {
|
|
foreach my $k (keys %seen) {
|
|
my $now = time();
|
|
$now - $seen{$k} > $seconds and delete $seen{$k} and _debug("deleted $k");
|
|
}
|
|
$i = 0;
|
|
_debug("done purging");
|
|
foreach my $k (keys %seen) {
|
|
_debug("remaining key: $k: $seen{$k}");
|
|
}
|
|
}
|
|
}
|
|
print STDERR "Stopping aa-notify\n";
|
|
}
|
|
|
|
sub show_since {
|
|
my %msg_hash;
|
|
my %last_date;
|
|
my @msg_list;
|
|
my $count = 0;
|
|
while(my $msg = <LOGFILE>) {
|
|
my @attrib = parse_message($msg, $_[0]);
|
|
$#attrib > 0 or next;
|
|
|
|
my $m = format_message(@attrib);
|
|
$m ne "" or next;
|
|
my $date = $attrib[6];
|
|
if ($opt_v) {
|
|
if (exists($msg_hash{$m})) {
|
|
$msg_hash{$m}++;
|
|
defined($date) and $last_date{$m} = scalar(localtime($date));
|
|
} else {
|
|
$msg_hash{$m} = 1;
|
|
push(@msg_list, $m);
|
|
}
|
|
}
|
|
$count++;
|
|
}
|
|
if ($opt_v) {
|
|
foreach my $m (@msg_list) {
|
|
print "$m";
|
|
if ($msg_hash{$m} gt 1) {
|
|
print "($msg_hash{$m} found";
|
|
if (exists($last_date{$m})) {
|
|
print ", most recent from '$last_date{$m}'";
|
|
}
|
|
print ")\n";
|
|
}
|
|
print "\n";
|
|
}
|
|
}
|
|
return $count;
|
|
}
|
|
|
|
sub do_last {
|
|
open(LAST,"$last_exe -F -a $login|") or die "Unable to run $last_exe:$!\n";
|
|
my $time = 0;
|
|
while(my $line = <LAST>) {
|
|
_debug("Checking '$line'");
|
|
$line =~ /^$login/ or next;
|
|
$line !~ /^$login\s+pts.*\s+:[0-9]+\.[0-9]+$/ or next; # ignore xterm and friends
|
|
my @entry = split(/\s+/, $line);
|
|
my ($hour, $min, $sec) = (split(/:/, $entry[5]))[0,1,2];
|
|
$time = Time::Local::timelocal($sec, $min, $hour, $entry[4], $entry[3], $entry[6]);
|
|
last;
|
|
}
|
|
close(LAST);
|
|
$time > 0 or _error("Couldn't find last login");
|
|
|
|
format_stats(show_since($time), $time);
|
|
}
|
|
|
|
sub do_show_messages {
|
|
my $since = $now - (int($_[0]) * 60 * 60 * 24);
|
|
format_stats(show_since($since), $since);
|
|
}
|
|
|
|
sub _warn {
|
|
my $msg = $_[0];
|
|
print STDERR "aa-notify: WARN: $msg\n";
|
|
}
|
|
sub _error {
|
|
my $msg = $_[0];
|
|
print STDERR "aa-notify: ERROR: $msg\n";
|
|
exitscript(1);
|
|
}
|
|
|
|
sub _debug {
|
|
$opt_d or return;
|
|
my $msg = $_[0];
|
|
print STDERR "aa-notify: DEBUG: $msg\n";
|
|
}
|
|
|
|
sub exitscript {
|
|
my $rc = $_[0];
|
|
close(LOGFILE);
|
|
exit $rc;
|
|
}
|
|
|
|
sub usage {
|
|
my $s = <<'EOF';
|
|
USAGE: aa-notify [OPTIONS]
|
|
|
|
Display AppArmor notifications or messages for DENIED entries.
|
|
|
|
OPTIONS:
|
|
-p, --poll poll AppArmor logs and display notifications
|
|
-f FILE, --file=FILE search FILE for AppArmor messages
|
|
-l, --since-last display stats since last login
|
|
-s NUM, --since-days=NUM show stats for last NUM days (can be used alone
|
|
or with -p)
|
|
-v, --verbose show messages with stats
|
|
-h, --help display this help
|
|
-u USER, --user=USER user to drop privileges to when not using sudo
|
|
-w NUM, --wait=NUM wait NUM seconds before displaying
|
|
notifications (with -p)
|
|
EOF
|
|
print $s;
|
|
}
|
|
|
|
sub reopen_logfile {
|
|
# reopen the logfile, temporarily switching back to starting euid for
|
|
# file permissions.
|
|
close(LOGFILE);
|
|
|
|
my $old_euid = $>;
|
|
my $change_euid = 0;
|
|
if ($> != $<) {
|
|
_debug("raising privileges to '$orig_euid' in reopen_logfile()");
|
|
$change_euid = 1;
|
|
$> = $orig_euid;
|
|
$> == $orig_euid or die "Could not raise privileges\n";
|
|
}
|
|
|
|
$logfile_inode = get_logfile_inode($logfile);
|
|
$logfile_size = get_logfile_size($logfile);
|
|
open (LOGFILE, "<$logfile") or die "Could not open '$logfile'\n";
|
|
|
|
if ($change_euid) {
|
|
_debug("dropping privileges to '$old_euid' in reopen_logfile()");
|
|
$> = $old_euid;
|
|
$> == $old_euid or die "Could not drop privileges\n";
|
|
}
|
|
}
|
|
|
|
sub get_logfile_size {
|
|
my $fn = $_[0];
|
|
my $size;
|
|
defined(($size = (stat($fn))[7])) or (sleep(10) and defined(($size = (stat($fn))[7])) or die "'$fn' disappeared. Aborting\n");
|
|
return $size;
|
|
}
|
|
|
|
sub get_logfile_inode {
|
|
my $fn = $_[0];
|
|
my $inode;
|
|
defined(($inode = (stat($fn))[1])) or (sleep(10) and defined(($inode = (stat($fn))[1])) or die "'$fn' disappeared. Aborting\n");
|
|
return $inode;
|
|
}
|
|
|
|
#
|
|
# end Subroutines
|
|
#
|