1
# ----------------------------------------------------------------------
2
# Copyright (c) 2006 Novell, Inc. All Rights Reserved.
3
# Copyright (c) 2010 Canonical, Ltd.
5
# This program is free software; you can redistribute it and/or
6
# modify it under the terms of version 2 of the GNU General Public
7
# License as published by the Free Software Foundation.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, contact Novell, Inc.
17
# To contact Novell about this file by physical or electronic mail,
18
# you may find current contact information at www.novell.com.
19
# ----------------------------------------------------------------------
21
package Immunix::AppArmor;
27
use Cwd qw(cwd realpath);
29
use File::Temp qw/ tempfile tempdir /;
34
use Storable qw(dclone);
38
use Immunix::Severity;
39
use Immunix::Repository;
44
our @ISA = qw(Exporter);
56
$running_under_genprof
93
activate_repo_profiles
110
our $confdir = "/etc/apparmor";
112
our $running_under_genprof = 0;
116
our $unimplemented_warning = 0;
118
# keep track of if we're running under yast or not - default to text mode
119
our $UI_Mode = "text";
123
# initialize Term::ReadLine if it's available
126
require Term::ReadLine;
127
import Term::ReadLine;
128
$term = new Term::ReadLine 'AppArmor';
131
# initialize the local poo
132
setlocale(LC_MESSAGES, "")
133
unless defined(LC_MESSAGES);
134
textdomain("apparmor-utils");
136
# where do we get our log messages from?
146
our $extraprofiledir;
148
# we keep track of the included profile fragments with %include
151
my %existing_profiles;
156
# these are globs that the user specifically entered. we'll keep track of
157
# them so that if one later matches, we'll suggest it again.
160
### THESE VARIABLES ARE USED WITHIN LOGPROF
163
our %sd; # we keep track of the original profiles in %sd
165
our %extras; # inactive profiles from extras
177
our %helpers; # we want to preserve this one between passes
179
### THESE VARIABLES ARE USED WITHIN LOGPROF
181
my %filelist; # file level stuff including variables in config files
184
my $AA_MAY_WRITE = 2;
186
my $AA_MAY_APPEND = 8;
187
my $AA_MAY_LINK = 16;
188
my $AA_MAY_LOCK = 32;
189
my $AA_EXEC_MMAP = 64;
190
my $AA_EXEC_UNSAFE = 128;
191
my $AA_EXEC_INHERIT = 256;
192
my $AA_EXEC_UNCONFINED = 512;
193
my $AA_EXEC_PROFILE = 1024;
194
my $AA_EXEC_CHILD = 2048;
195
my $AA_EXEC_NT = 4096;
196
my $AA_LINK_SUBSET = 8192;
198
my $AA_OTHER_SHIFT = 14;
199
my $AA_USER_MASK = 16384 -1;
201
my $AA_EXEC_TYPE = $AA_MAY_EXEC | $AA_EXEC_UNSAFE | $AA_EXEC_INHERIT |
202
$AA_EXEC_UNCONFINED | $AA_EXEC_PROFILE | $AA_EXEC_CHILD | $AA_EXEC_NT;
204
my $ALL_AA_EXEC_TYPE = $AA_EXEC_TYPE;
222
i => $AA_EXEC_INHERIT,
223
I => $AA_EXEC_INHERIT,
224
u => $AA_EXEC_UNCONFINED + $AA_EXEC_UNSAFE, # U + Unsafe
225
U => $AA_EXEC_UNCONFINED,
226
p => $AA_EXEC_PROFILE + $AA_EXEC_UNSAFE, # P + Unsafe
227
P => $AA_EXEC_PROFILE,
228
c => $AA_EXEC_CHILD + $AA_EXEC_UNSAFE,
230
n => $AA_EXEC_NT + $AA_EXEC_UNSAFE,
235
# Currently only used by netdomain but there's no reason it couldn't
236
# be extended to support other types.
237
my %operation_types = (
240
"socket_create", => "net",
241
"socket_post_create" => "net",
242
"socket_bind" => "net",
243
"socket_connect" => "net",
244
"socket_listen" => "net",
245
"socket_accept" => "net",
246
"socket_sendmsg" => "net",
247
"socket_recvmsg" => "net",
248
"socket_getsockname" => "net",
249
"socket_getpeername" => "net",
250
"socket_getsockopt" => "net",
251
"socket_setsockopt" => "net",
252
"socket_shutdown" => "net",
256
"post_create" => "net",
263
"getsockname" => "net",
264
"getpeername" => "net",
265
"getsockopt" => "net",
266
"setsockopt" => "net",
267
"sock_shutdown" => "net",
272
my $type = $operation_types{$op};
274
return "unknown" if !defined($type);
282
print DEBUG "$message\n" if $DEBUGGING;
285
my %arrows = ( A => "UP", B => "DOWN", C => "RIGHT", D => "LEFT" );
291
my $key = ReadKey(0);
293
# decode arrow key control sequences
294
if ($key eq "\x1B") {
299
$key = $arrows{$key};
304
# return to cooked mode
310
# set things up to log extra info if they want...
311
if ($ENV{LOGPROF_DEBUG}) {
313
open(DEBUG, ">>$ENV{LOGPROF_DEBUG}");
314
my $oldfd = select(DEBUG);
323
$DEBUGGING && debug "Exiting...";
325
# close the debug log if necessary
326
close(DEBUG) if $DEBUGGING;
329
# returns true if the specified program contains references to LD_PRELOAD or
330
# LD_LIBRARY_PATH to give the PX/UX code better suggestions
331
sub check_for_LD_XXX ($) {
334
return undef unless -f $file;
336
# limit our checking to programs/scripts under 10k to speed things up a bit
338
return undef unless ($size && $size < 10000);
341
if (open(F, $file)) {
343
$found = 1 if /LD_(PRELOAD|LIBRARY_PATH)/;
351
sub fatal_error ($) {
354
my $details = "$message\n";
358
# we'll include the stack backtrace if we're debugging...
359
$details = Carp::longmess($message);
361
# write the error to the log
362
print DEBUG $details;
365
# we'll just shoot ourselves in the head if it was one of the yast
366
# interface functions that ran into an error. it gets really ugly if
367
# the yast frontend goes away and we try to notify the user of that
368
# problem by trying to send the yast frontend a pretty dialog box
369
my $caller = (caller(1))[3];
371
exit 1 if defined($caller) && $caller =~ /::(Send|Get)Data(To|From)Yast$/;
373
# tell the user what the hell happened
374
UI_Important($details);
376
# make sure the frontend exits cleanly...
379
# die a horrible flaming death
385
# set up the yast connection if we're running under yast...
386
if ($ENV{YAST_IS_RUNNING}) {
388
# load the yast module if available.
389
eval { require ycp; };
395
# let the frontend know that we're starting
397
type => "initial_handshake",
398
status => "backend_starting"
401
# see if the frontend is just starting up also...
402
my ($ypath, $yarg) = GetDataFromYast();
404
&& (ref($yarg) eq "HASH")
405
&& ($yarg->{type} eq "initial_handshake")
406
&& ($yarg->{status} eq "frontend_starting"))
409
# something's broken, die a horrible, painful death
410
fatal_error "Yast frontend is out of sync from backend agent.";
412
$DEBUGGING && debug "Initial handshake ok";
414
# the yast connection seems to be working okay
424
sub shutdown_yast() {
425
if ($UI_Mode eq "yast") {
426
SendDataToYast({ type => "final_shutdown" });
427
my ($ypath, $yarg) = GetDataFromYast();
431
sub check_for_subdomain () {
433
my ($support_subdomainfs, $support_securityfs);
434
if (open(MOUNTS, "/proc/filesystems")) {
436
$support_subdomainfs = 1 if m/subdomainfs/;
437
$support_securityfs = 1 if m/securityfs/;
442
my $sd_mountpoint = "";
443
if (open(MOUNTS, "/proc/mounts")) {
445
if ($support_subdomainfs) {
446
$sd_mountpoint = $1 if m/^\S+\s+(\S+)\s+subdomainfs\s/;
447
} elsif ($support_securityfs) {
448
if (m/^\S+\s+(\S+)\s+securityfs\s/) {
449
if (-e "$1/apparmor") {
450
$sd_mountpoint = "$1/apparmor";
451
} elsif (-e "$1/subdomain") {
452
$sd_mountpoint = "$1/subdomain";
460
# make sure that subdomain is actually mounted there
461
$sd_mountpoint = undef unless -f "$sd_mountpoint/profiles";
463
return $sd_mountpoint;
466
sub check_for_apparmor () {
467
return check_for_subdomain();
473
foreach my $dir (split(/:/, $ENV{PATH})) {
474
return "$dir/$file" if -x "$dir/$file";
480
# we need to convert subdomain regexps to perl regexps
481
sub convert_regexp ($) {
484
# escape regexp-special characters we don't support
485
$regexp =~ s/(?<!\\)(\.|\+|\$)/\\$1/g;
487
# * and ** globs can't collapse to match an empty string when they're
488
# the only part of the glob at a specific directory level, which
489
# complicates things a little.
491
# ** globs match multiple directory levels
492
$regexp =~ s{(?<!\\)\*\*+}{
493
my ($pre, $post) = ($`, $');
494
if (($pre =~ /\/$/) && (!$post || $post =~ /^\//)) {
495
'SD_INTERNAL_MULTI_REQUIRED';
497
'SD_INTERNAL_MULTI_OPTIONAL';
501
# convert * globs to match anything at the current path level
502
$regexp =~ s{(?<!\\)\*}{
503
my ($pre, $post) = ($`, $');
504
if (($pre =~ /\/$/) && (!$post || $post =~ /^\//)) {
505
'SD_INTERNAL_SINGLE_REQUIRED';
507
'SD_INTERNAL_SINGLE_OPTIONAL';
511
# convert ? globs to match a single character at current path level
512
$regexp =~ s/(?<!\\)\?/[^\/]/g;
514
# convert {foo,baz} to (foo|baz)
515
$regexp =~ y/\{\}\,/\(\)\|/ if $regexp =~ /\{.*\,.*\}/;
517
# convert internal markers to their appropriate regexp equivalents
518
$regexp =~ s/SD_INTERNAL_SINGLE_OPTIONAL/[^\/]*/g;
519
$regexp =~ s/SD_INTERNAL_SINGLE_REQUIRED/[^\/]+/g;
520
$regexp =~ s/SD_INTERNAL_MULTI_OPTIONAL/.*/g;
521
$regexp =~ s/SD_INTERNAL_MULTI_REQUIRED/[^\/].*/g;
526
sub get_full_path ($) {
527
my $originalpath = shift;
529
my $path = $originalpath;
531
# keep track so we can break out of loops
534
# if we don't have any directory foo, look in the current dir
535
$path = cwd() . "/$path" if $path !~ m/\//;
537
# beat symlinks into submission
540
if ($linkcount++ > 64) {
541
fatal_error "Followed too many symlinks resolving $originalpath";
544
# split out the directory/file components
545
if ($path =~ m/^(.*)\/(.+)$/) {
546
my ($dir, $file) = ($1, $2);
548
# figure out where the link is pointing...
549
my $link = readlink($path);
550
if ($link =~ /^\//) {
551
# if it's an absolute link, just replace it
554
# if it's relative, let abs_path handle it
555
$path = $dir . "/$link";
560
return realpath($path);
563
sub findexecutable ($) {
568
$fqdbin = get_full_path($bin);
572
my $which = which($bin);
574
$fqdbin = get_full_path($which);
579
unless ($fqdbin && -e $fqdbin) {
586
sub name_to_prof_filename($) {
590
unless ($bin =~ /^($profiledir)/) {
591
my $fqdbin = findexecutable($bin);
593
$filename = getprofilefilename($fqdbin);
594
return ($filename, $fqdbin) if -f $filename;
598
if ($bin =~ /^$profiledir(.*)/) {
600
return ($bin, $profile);
601
} elsif ($bin =~ /^\//) {
602
$filename = getprofilefilename($bin);
603
return ($filename, $bin);
605
# not an absolute path try it as a profile_
606
$bin = $1 if ($bin !~ /^profile_(.*)/);
607
$filename = getprofilefilename($bin);
608
return ($filename, "profile_${bin}");
618
my ($filename, $name) = name_to_prof_filename($bin)
619
or fatal_error(sprintf(gettext('Can\'t find %s.'), $bin));
621
UI_Info(sprintf(gettext('Setting %s to complain mode.'), $name));
623
setprofileflags($filename, "complain");
631
my ($filename, $name) = name_to_prof_filename($bin)
632
or fatal_error(sprintf(gettext('Can\'t find %s.'), $bin));
634
UI_Info(sprintf(gettext('Setting %s to enforce mode.'), $name));
636
setprofileflags($filename, "");
643
if (open(FILE, $file)) {
651
sub get_output ($@) {
652
my ($program, @args) = @_;
660
$pid = open(KID_TO_READ, "-|");
661
unless (defined $pid) {
662
fatal_error "can't fork: $!";
666
while (<KID_TO_READ>) {
674
open(STDERR, ">&STDOUT")
675
|| fatal_error "can't dup stdout to stderr";
676
exec($program, @args) || fatal_error "can't exec program: $!";
682
return ($ret, @output);
689
my ($ret, @ldd) = get_output($ldd, $file);
692
for my $line (@ldd) {
693
last if $line =~ /not a dynamic executable/;
694
last if $line =~ /cannot read header/;
695
last if $line =~ /statically linked/;
697
# avoid new kernel 2.6 poo
698
next if $line =~ /linux-(gate|vdso(32|64)).so/;
700
if ($line =~ /^\s*\S+ => (\/\S+)/) {
702
} elsif ($line =~ /^\s*(\/\S+)/) {
711
sub handle_binfmt ($$) {
712
my ($profile, $fqdbin) = @_;
715
my @reqs = get_reqs($fqdbin);
717
while (my $library = shift @reqs) {
719
$library = get_full_path($library);
721
push @reqs, get_reqs($library) unless $reqs{$library}++;
723
# does path match anything pulled in by includes in original profile?
724
my $combinedmode = match_prof_incs_to_path($profile, 'allow', $library);
726
# if we found any matching entries, do the modes match?
727
next if $combinedmode;
729
$library = globcommon($library);
731
next unless $library;
733
$profile->{allow}{path}->{$library}{mode} |= str_to_mode("mr");
734
$profile->{allow}{path}->{$library}{audit} |= 0;
738
sub get_inactive_profile($) {
740
if ( $extras{$fqdbin} ) {
741
return {$fqdbin => $extras{$fqdbin}};
747
sub create_new_profile($) {
751
if ($fqdbin =~ /^\// ) {
755
include => { "abstractions/base" => 1 },
756
path => { $fqdbin => { mode => str_to_mode("mr") } },
763
include => { "abstractions/base" => 1 },
768
# if the executable exists on this system, pull in extra dependencies
770
my $hashbang = head($fqdbin);
771
if ($hashbang && $hashbang =~ /^#!\s*(\S+)/) {
772
my $interpreter = get_full_path($1);
773
$profile->{$fqdbin}{allow}{path}->{$fqdbin}{mode} |= str_to_mode("r");
774
$profile->{$fqdbin}{allow}{path}->{$fqdbin}{mode} |= 0;
775
$profile->{$fqdbin}{allow}{path}->{$interpreter}{mode} |= str_to_mode("ix");
776
$profile->{$fqdbin}{allow}{path}->{$interpreter}{audit} |= 0;
777
if ($interpreter =~ /perl/) {
778
$profile->{$fqdbin}{include}->{"abstractions/perl"} = 1;
779
} elsif ($interpreter =~ m/\/bin\/(bash|sh)/) {
780
$profile->{$fqdbin}{include}->{"abstractions/bash"} = 1;
781
} elsif ($interpreter =~ m/python/) {
782
$profile->{$fqdbin}{include}->{"abstractions/python"} = 1;
783
} elsif ($interpreter =~ m/ruby/) {
784
$profile->{$fqdbin}{include}->{"abstractions/ruby"} = 1;
786
handle_binfmt($profile->{$fqdbin}, $interpreter);
788
handle_binfmt($profile->{$fqdbin}, $fqdbin);
792
# create required infrastructure hats if it's a known change_hat app
793
for my $hatglob (keys %{$cfg->{required_hats}}) {
794
if ($fqdbin =~ /$hatglob/) {
795
for my $hat (sort split(/\s+/, $cfg->{required_hats}{$hatglob})) {
796
$profile->{$hat} = { flags => "complain" };
800
push @created, $fqdbin;
801
return { $fqdbin => $profile };
804
sub delete_profile ($) {
806
my $profilefile = getprofilefilename( $profile );
807
if ( -e $profilefile ) {
808
unlink( $profilefile );
810
if ( defined $sd{$profile} ) {
811
delete $sd{$profile};
819
my $distro = $cfg->{repository}{distro};
820
my $repo_url = $cfg->{repository}{url};
824
if (repo_is_enabled()) {
826
UI_BusyStart( gettext("Connecting to repository.....") );
828
my ($status_ok,$ret) =
829
fetch_profiles_by_name($repo_url, $distro, $fqdbin );
832
%profile_hash = %$ret;
835
sprintf(gettext("WARNING: Error fetching profiles from the repository:\n%s\n"),
836
$ret?$ret:gettext("UNKNOWN ERROR"));
837
UI_Important( $errmsg );
841
my $inactive_profile = get_inactive_profile($fqdbin);
842
if ( defined $inactive_profile && $inactive_profile ne "" ) {
843
# set the profile to complain mode
844
my $uname = gettext( "Inactive local profile for ") . $fqdbin;
845
$inactive_profile->{$fqdbin}{$fqdbin}{flags} = "complain";
846
# inactive profiles store where they came from
847
delete $inactive_profile->{$fqdbin}{$fqdbin}{filename};
848
$profile_hash{$uname} =
850
"username" => $uname,
851
"profile_type" => "INACTIVE_LOCAL",
852
"profile" => serialize_profile($inactive_profile->{$fqdbin},
855
"profile_data" => $inactive_profile,
859
return undef if ( keys %profile_hash == 0 ); # No repo profiles, no inactive
863
my $preferred_present = 0;
864
my $preferred_user = $cfg->{repository}{preferred_user} || "NOVELL";
866
foreach my $p ( keys %profile_hash ) {
867
if ( $profile_hash{$p}->{username} eq $preferred_user ) {
868
$preferred_present = 1;
870
push @tmp_list, $profile_hash{$p}->{username};
874
if ( $preferred_present ) {
875
push @options, $preferred_user;
877
push @options, @tmp_list;
881
push @{ $q->{headers} }, gettext("Profile"), $fqdbin;
883
$q->{functions} = [ "CMD_VIEW_PROFILE", "CMD_USE_PROFILE",
884
"CMD_CREATE_PROFILE", "CMD_ABORT", "CMD_FINISHED" ];
886
$q->{default} = "CMD_VIEW_PROFILE";
888
$q->{options} = [@options];
893
($ans, $arg) = UI_PromptUser($q);
894
$p = $profile_hash{$options[$arg]};
895
for (my $i = 0; $i < scalar(@options); $i++) {
896
if ($options[$i] eq $options[$arg]) {
901
if ($ans eq "CMD_VIEW_PROFILE") {
902
if ($UI_Mode eq "yast") {
905
type => "dialog-view-profile",
906
user => $options[$arg],
907
profile => $p->{profile},
908
profile_type => $p->{profile_type}
911
my ($ypath, $yarg) = GetDataFromYast();
913
my $pager = get_pager();
914
open(PAGER, "| $pager");
915
print PAGER gettext("Profile submitted by") .
916
" $options[$arg]:\n\n" . $p->{profile} . "\n\n";
919
} elsif ($ans eq "CMD_USE_PROFILE") {
920
if ( $p->{profile_type} eq "INACTIVE_LOCAL" ) {
921
$profile_data = $p->{profile_data};
922
push @created, $fqdbin; # This really is ugly here
923
# need to find a better place to mark
924
# this as newly created
927
parse_repo_profile($fqdbin, $repo_url, $p);
930
} until ($ans =~ /^CMD_(USE_PROFILE|CREATE_PROFILE)$/);
932
return $profile_data;
935
sub activate_repo_profiles ($$$) {
936
my ($url,$profiles,$complain) = @_;
940
for my $p ( @$profiles ) {
942
my $profile_data = parse_repo_profile( $pname, $url, $p->[1] );
943
attach_profile_data(\%sd, $profile_data);
944
writeprofile($pname);
946
my $filename = getprofilefilename($pname);
947
setprofileflags($filename, "complain");
948
UI_Info(sprintf(gettext('Setting %s to complain mode.'),
953
# if there were errors....
956
print STDERR sprintf(gettext("Error activating profiles: %s\n"), $@);
960
sub autodep_base($$) {
961
my ($bin, $pname) = @_;
964
$bin = $pname if (! $bin) && ($pname =~ /^\//);
966
unless ($repo_cfg || not defined $cfg->{repository}{url}) {
967
$repo_cfg = read_config("repository.conf");
968
if ( (not defined $repo_cfg->{repository}) ||
969
($repo_cfg->{repository}{enabled} eq "later") ) {
970
UI_ask_to_enable_repo();
976
# findexecutable() might fail if we're running on a different system
977
# than the logs were collected on. ugly. we'll just hope for the best.
978
$fqdbin = findexecutable($bin) || $bin;
980
# try to make sure we have a full path in case findexecutable failed
981
return unless $fqdbin =~ /^\//;
984
return if -d $fqdbin;
987
$pname = $fqdbin if $fqdbin;
991
readinactiveprofiles(); # need to read the profiles to see if an
992
# inactive local profile is present
993
$profile_data = eval { get_profile($pname) };
994
# propagate any errors we hit inside the get_profile call
997
unless ($profile_data) {
998
$profile_data = create_new_profile($pname);
1001
my $file = getprofilefilename($pname);
1003
# stick the profile into our data structure.
1004
attach_profile_data(\%sd, $profile_data);
1005
# and store a "clean" version also so we can display the changes we've
1006
# made during this run
1007
attach_profile_data(\%original_sd, $profile_data);
1009
if (-f "$profiledir/tunables/global") {
1010
unless (exists $filelist{$file}) {
1011
$filelist{$file} = { };
1013
$filelist{$file}{include}{'tunables/global'} = 1; # sorry
1016
# write out the profile...
1017
writeprofile_ui_feedback($pname);
1022
return autodep_base($bin, "");
1025
sub getprofilefilename ($) {
1026
my $profile = shift;
1028
my $filename = $profile;
1029
if ($filename =~ /^\//) {
1030
$filename =~ s/^\///; # strip leading /
1032
$filename = "profile_$filename";
1034
$filename =~ s/\//./g; # convert /'s to .'s
1036
return "$profiledir/$filename";
1039
sub setprofileflags ($$) {
1040
my $filename = shift;
1041
my $newflags = shift;
1043
if (open(PROFILE, "$filename")) {
1044
if (open(NEWPROFILE, ">$filename.new")) {
1046
if (m/^(\s*)(("??\/.+?"??)|(profile\s+("??.+?"??)))\s+(flags=\(.+\)\s+)*\{\s*$/) {
1047
my ($space, $binary, $flags) = ($1, $2, $6);
1050
$_ = "$space$binary flags=($newflags) {\n";
1052
$_ = "$space$binary {\n";
1054
} elsif (m/^(\s*\^\S+)\s+(flags=\(.+\)\s+)*\{\s*$/) {
1055
my ($hat, $flags) = ($1, $2);
1058
$_ = "$hat flags=($newflags) {\n";
1066
rename("$filename.new", "$filename");
1072
sub profile_exists($) {
1073
my $program = shift || return 0;
1075
# if it's already in the cache, return true
1076
return 1 if $existing_profiles{$program};
1078
# if the profile exists, mark it in the cache and return true
1079
my $profile = getprofilefilename($program);
1081
$existing_profiles{$program} = 1;
1085
# couldn't find a profile, so we'll return false
1089
sub sync_profiles() {
1091
my ($user, $pass) = get_repo_user_pass();
1092
return unless ( $user && $pass );
1095
my @changed_profiles;
1097
my $serialize_opts = { };
1098
my ($status_ok,$ret) =
1099
fetch_profiles_by_user($cfg->{repository}{url},
1100
$cfg->{repository}{distro},
1103
if ( !$status_ok ) {
1105
sprintf(gettext("WARNING: Error syncronizing profiles with the repository:\n%s\n"),
1106
$ret?$ret:gettext("UNKNOWN ERROR"));
1107
UI_Important($errmsg);
1110
my $users_repo_profiles = $ret;
1111
$serialize_opts->{NO_FLAGS} = 1;
1113
# Find changes made to non-repo profiles
1115
for my $profile (sort keys %sd) {
1116
if (is_repo_profile($sd{$profile}{$profile})) {
1117
push @repo_profiles, $profile;
1119
if ( grep(/^$profile$/, @created) ) {
1120
my $p_local = serialize_profile($sd{$profile},
1123
if ( not defined $users_repo_profiles->{$profile} ) {
1124
push @new_profiles, [ $profile, $p_local, "" ];
1126
my $p_repo = $users_repo_profiles->{$profile}->{profile};
1127
if ( $p_local ne $p_repo ) {
1128
push @changed_profiles, [ $profile, $p_local, $p_repo ];
1135
# Find changes made to local profiles with repo metadata
1137
if (@repo_profiles) {
1138
for my $profile (@repo_profiles) {
1139
my $p_local = serialize_profile($sd{$profile},
1142
if ( not exists $users_repo_profiles->{$profile} ) {
1143
push @new_profiles, [ $profile, $p_local, "" ];
1146
if ( $sd{$profile}{$profile}{repo}{user} eq $user ) {
1147
$p_repo = $users_repo_profiles->{$profile}->{profile};
1149
my ($status_ok,$ret) =
1150
fetch_profile_by_id($cfg->{repository}{url},
1151
$sd{$profile}{$profile}{repo}{id}
1154
$p_repo = $ret->{profile};
1158
gettext("WARNING: Error syncronizing profiles with the repository:\n%s\n"),
1159
$ret?$ret:gettext("UNKNOWN ERROR"));
1160
UI_Important($errmsg);
1164
if ( $p_repo ne $p_local ) {
1165
push @changed_profiles, [ $profile, $p_local, $p_repo ];
1171
if ( @changed_profiles ) {
1172
submit_changed_profiles( \@changed_profiles );
1174
if ( @new_profiles ) {
1175
submit_created_profiles( \@new_profiles );
1180
sub submit_created_profiles($) {
1181
my $new_profiles = shift;
1182
my $url = $cfg->{repository}{url};
1184
if ($UI_Mode eq "yast") {
1185
my $title = gettext("New profiles");
1187
gettext("Please choose the newly created profiles that you would".
1188
" like\nto store in the repository");
1189
yast_select_and_upload_profiles($title,
1194
gettext("Submit newly created profiles to the repository");
1196
gettext("Would you like to upload the newly created profiles?");
1197
console_select_and_upload_profiles($title,
1203
sub submit_changed_profiles($) {
1204
my $changed_profiles = shift;
1205
my $url = $cfg->{repository}{url};
1206
if (@$changed_profiles) {
1207
if ($UI_Mode eq "yast") {
1209
gettext("Select which of the changed profiles you would".
1210
" like to upload\nto the repository");
1211
my $title = gettext("Changed profiles");
1212
yast_select_and_upload_profiles($title,
1217
gettext("Submit changed profiles to the repository");
1219
gettext("The following profiles from the repository were".
1220
" changed.\nWould you like to upload your changes?");
1221
console_select_and_upload_profiles($title,
1228
sub yast_select_and_upload_profiles($$$) {
1230
my ($title, $explanation, $profiles_ref) = @_;
1231
my $url = $cfg->{repository}{url};
1232
my %profile_changes;
1233
my @profiles = @$profiles_ref;
1235
foreach my $prof (@profiles) {
1236
$profile_changes{ $prof->[0] } =
1237
get_profile_diff($prof->[2], $prof->[1]);
1240
my (@selected_profiles, $changelog, $changelogs, $single_changelog);
1243
type => "dialog-select-profiles",
1245
explanation => $explanation,
1246
default_select => "false",
1247
disable_ask_upload => "true",
1248
profiles => \%profile_changes
1251
my ($ypath, $yarg) = GetDataFromYast();
1252
if ($yarg->{STATUS} eq "cancel") {
1255
my $selected_profiles_ref = $yarg->{PROFILES};
1256
@selected_profiles = @$selected_profiles_ref;
1257
$changelogs = $yarg->{CHANGELOG};
1258
if (defined $changelogs->{SINGLE_CHANGELOG}) {
1259
$changelog = $changelogs->{SINGLE_CHANGELOG};
1260
$single_changelog = 1;
1264
for my $profile (@selected_profiles) {
1265
my ($user, $pass) = get_repo_user_pass();
1266
my $profile_string = serialize_profile($sd{$profile}, $profile);
1267
if (!$single_changelog) {
1268
$changelog = $changelogs->{$profile};
1270
my ($status_ok, $ret) = upload_profile( $url,
1273
$cfg->{repository}{distro},
1279
my $newprofile = $ret;
1280
my $newid = $newprofile->{id};
1281
set_repo_info($sd{$profile}{$profile}, $url, $user, $newid);
1282
writeprofile_ui_feedback($profile);
1286
gettext("WARNING: An error occured while uploading the profile %s\n%s\n"),
1287
$profile, $ret?$ret:gettext("UNKNOWN ERROR"));
1288
UI_Important( $errmsg );
1291
UI_Info(gettext("Uploaded changes to repository."));
1293
# Check to see if unselected profiles should be marked as local only
1294
# this is outside of the main repo code as we want users to be able to mark
1295
# profiles as local only even if they aren't able to connect to the repo.
1296
if (defined $yarg->{NEVER_ASK_AGAIN}) {
1297
my @unselected_profiles;
1298
foreach my $prof (@profiles) {
1299
if ( grep(/^$prof->[0]$/, @selected_profiles) == 0 ) {
1300
push @unselected_profiles, $prof->[0];
1303
set_profiles_local_only( @unselected_profiles );
1307
sub console_select_and_upload_profiles($$$) {
1308
my ($title, $explanation, $profiles_ref) = @_;
1309
my $url = $cfg->{repository}{url};
1310
my @profiles = @$profiles_ref;
1312
$q->{title} = $title;
1313
$q->{headers} = [ gettext("Repository"), $url, ];
1315
$q->{explanation} = $explanation;
1317
$q->{functions} = [ "CMD_UPLOAD_CHANGES",
1323
$q->{default} = "CMD_VIEW_CHANGES";
1325
$q->{options} = [ map { $_->[0] } @profiles ];
1330
($ans, $arg) = UI_PromptUser($q);
1332
if ($ans eq "CMD_VIEW_CHANGES") {
1333
display_changes($profiles[$arg]->[2], $profiles[$arg]->[1]);
1335
} until $ans =~ /^CMD_(UPLOAD_CHANGES|ASK_NEVER|ASK_LATER)/;
1337
if ($ans eq "CMD_ASK_NEVER") {
1338
set_profiles_local_only( map { $_->[0] } @profiles );
1339
} elsif ($ans eq "CMD_UPLOAD_CHANGES") {
1340
my $changelog = UI_GetString(gettext("Changelog Entry: "), "");
1341
my ($user, $pass) = get_repo_user_pass();
1342
if ($user && $pass) {
1343
for my $p_data (@profiles) {
1344
my $profile = $p_data->[0];
1345
my $profile_string = $p_data->[1];
1346
my ($status_ok,$ret) =
1347
upload_profile( $url,
1350
$cfg->{repository}{distro},
1356
my $newprofile = $ret;
1357
my $newid = $newprofile->{id};
1358
set_repo_info($sd{$profile}{$profile}, $url, $user, $newid);
1359
writeprofile_ui_feedback($profile);
1361
sprintf(gettext("Uploaded %s to repository."), $profile)
1366
gettext("WARNING: An error occured while uploading the profile %s\n%s\n"),
1367
$profile, $ret?$ret:gettext("UNKNOWN ERROR"));
1368
UI_Important( $errmsg );
1372
UI_Important(gettext("Repository Error\n" .
1373
"Registration or Signin was unsuccessful. User login\n" .
1374
"information is required to upload profiles to the\n" .
1375
"repository. These changes have not been sent.\n"));
1381
# Mark the profiles passed in @profiles as local only
1382
# and don't prompt to upload changes to the repository
1384
sub set_profiles_local_only(@) {
1386
for my $profile (@profiles) {
1387
$sd{$profile}{$profile}{repo}{neversubmit} = 1;
1388
writeprofile_ui_feedback($profile);
1392
##########################################################################
1393
# Here are the console/yast interface functions
1398
$DEBUGGING && debug "UI_Info: $UI_Mode: $text";
1400
if ($UI_Mode eq "text") {
1403
ycp::y2milestone($text);
1407
sub UI_Important ($) {
1410
$DEBUGGING && debug "UI_Important: $UI_Mode: $text";
1412
if ($UI_Mode eq "text") {
1415
SendDataToYast({ type => "dialog-error", message => $text });
1416
my ($path, $yarg) = GetDataFromYast();
1422
my $default = shift;
1424
$DEBUGGING && debug "UI_YesNo: $UI_Mode: $text $default";
1427
if ($UI_Mode eq "text") {
1429
my $yes = gettext("(Y)es");
1430
my $no = gettext("(N)o");
1432
# figure out our localized hotkeys
1433
my $usrmsg = "PromptUser: " . gettext("Invalid hotkey for");
1434
$yes =~ /\((\S)\)/ or fatal_error "$usrmsg '$yes'";
1435
my $yeskey = lc($1);
1436
$no =~ /\((\S)\)/ or fatal_error "$usrmsg '$no'";
1440
if ($default eq "y") {
1441
print "\n[$yes] / $no\n";
1443
print "\n$yes / [$no]\n";
1445
$ans = getkey() || (($default eq "y") ? $yeskey : $nokey);
1447
# convert back from a localized answer to english y or n
1448
$ans = (lc($ans) eq $yeskey) ? "y" : "n";
1451
SendDataToYast({ type => "dialog-yesno", question => $text });
1452
my ($ypath, $yarg) = GetDataFromYast();
1453
$ans = $yarg->{answer} || $default;
1460
sub UI_YesNoCancel ($$) {
1462
my $default = shift;
1464
$DEBUGGING && debug "UI_YesNoCancel: $UI_Mode: $text $default";
1467
if ($UI_Mode eq "text") {
1469
my $yes = gettext("(Y)es");
1470
my $no = gettext("(N)o");
1471
my $cancel = gettext("(C)ancel");
1473
# figure out our localized hotkeys
1474
my $usrmsg = "PromptUser: " . gettext("Invalid hotkey for");
1475
$yes =~ /\((\S)\)/ or fatal_error "$usrmsg '$yes'";
1476
my $yeskey = lc($1);
1477
$no =~ /\((\S)\)/ or fatal_error "$usrmsg '$no'";
1479
$cancel =~ /\((\S)\)/ or fatal_error "$usrmsg '$cancel'";
1480
my $cancelkey = lc($1);
1482
$ans = "XXXINVALIDXXX";
1483
while ($ans !~ /^(y|n|c)$/) {
1485
if ($default eq "y") {
1486
print "\n[$yes] / $no / $cancel\n";
1487
} elsif ($default eq "n") {
1488
print "\n$yes / [$no] / $cancel\n";
1490
print "\n$yes / $no / [$cancel]\n";
1496
# convert back from a localized answer to english y or n
1498
if ($ans eq $yeskey) {
1500
} elsif ($ans eq $nokey) {
1502
} elsif ($ans eq $cancelkey) {
1511
SendDataToYast({ type => "dialog-yesnocancel", question => $text });
1512
my ($ypath, $yarg) = GetDataFromYast();
1513
$ans = $yarg->{answer} || $default;
1520
sub UI_GetString ($$) {
1522
my $default = shift;
1524
$DEBUGGING && debug "UI_GetString: $UI_Mode: $text $default";
1527
if ($UI_Mode eq "text") {
1530
$string = $term->readline($text, $default);
1541
type => "dialog-getstring",
1545
my ($ypath, $yarg) = GetDataFromYast();
1546
$string = $yarg->{string};
1552
sub UI_GetFile ($) {
1555
$DEBUGGING && debug "UI_GetFile: $UI_Mode";
1558
if ($UI_Mode eq "text") {
1561
print "$f->{description}\n";
1562
$filename = <STDIN>;
1567
$f->{type} = "dialog-getfile";
1570
my ($ypath, $yarg) = GetDataFromYast();
1571
if ($yarg->{answer} eq "okay") {
1572
$filename = $yarg->{filename};
1579
sub UI_BusyStart ($) {
1580
my $message = shift;
1581
$DEBUGGING && debug "UI_BusyStart: $UI_Mode";
1583
if ($UI_Mode eq "text") {
1584
UI_Info( $message );
1587
type => "dialog-busy-start",
1588
message => $message,
1590
my ($ypath, $yarg) = GetDataFromYast();
1595
$DEBUGGING && debug "UI_BusyStop: $UI_Mode";
1597
if ($UI_Mode ne "text") {
1598
SendDataToYast({ type => "dialog-busy-stop" });
1599
my ($ypath, $yarg) = GetDataFromYast();
1605
CMD_ALLOW => "(A)llow",
1606
CMD_OTHER => "(M)ore",
1607
CMD_AUDIT_NEW => "Audi(t)",
1608
CMD_AUDIT_OFF => "Audi(t) off",
1609
CMD_AUDIT_FULL => "Audit (A)ll",
1610
CMD_OTHER => "(O)pts",
1611
CMD_USER_ON => "(O)wner permissions on",
1612
CMD_USER_OFF => "(O)wner permissions off",
1613
CMD_DENY => "(D)eny",
1614
CMD_ABORT => "Abo(r)t",
1615
CMD_FINISHED => "(F)inish",
1616
CMD_ix => "(I)nherit",
1617
CMD_px => "(P)rofile",
1618
CMD_px_safe => "(P)rofile Clean Exec",
1619
CMD_cx => "(C)hild",
1620
CMD_cx_safe => "(C)hild Clean Exec",
1622
CMD_nx_safe => "(N)amed Clean Exec",
1623
CMD_ux => "(U)nconfined",
1624
CMD_ux_safe => "(U)nconfined Clean Exec",
1625
CMD_pix => "(P)rofile ix",
1626
CMD_pix_safe => "(P)rofile ix Clean Exec",
1627
CMD_cix => "(C)hild ix",
1628
CMD_cix_safe => "(C)hild ix Cx Clean Exec",
1629
CMD_nix => "(N)ame ix",
1630
CMD_nix_safe => "(N)ame ix",
1631
CMD_EXEC_IX_ON => "(X)ix",
1632
CMD_EXEC_IX_OFF => "(X)ix",
1633
CMD_SAVE => "(S)ave Changes",
1634
CMD_CONTINUE => "(C)ontinue Profiling",
1636
CMD_GLOB => "(G)lob",
1637
CMD_GLOBEXT => "Glob w/(E)xt",
1638
CMD_ADDHAT => "(A)dd Requested Hat",
1639
CMD_USEDEFAULT => "(U)se Default Hat",
1640
CMD_SCAN => "(S)can system log for AppArmor events",
1641
CMD_HELP => "(H)elp",
1642
CMD_VIEW_PROFILE => "(V)iew Profile",
1643
CMD_USE_PROFILE => "(U)se Profile",
1644
CMD_CREATE_PROFILE => "(C)reate New Profile",
1645
CMD_UPDATE_PROFILE => "(U)pdate Profile",
1646
CMD_IGNORE_UPDATE => "(I)gnore Update",
1647
CMD_SAVE_CHANGES => "(S)ave Changes",
1648
CMD_UPLOAD_CHANGES => "(U)pload Changes",
1649
CMD_VIEW_CHANGES => "(V)iew Changes",
1650
CMD_VIEW => "(V)iew",
1651
CMD_ENABLE_REPO => "(E)nable Repository",
1652
CMD_DISABLE_REPO => "(D)isable Repository",
1653
CMD_ASK_NEVER => "(N)ever Ask Again",
1654
CMD_ASK_LATER => "Ask Me (L)ater",
1657
CMD_ALL_NET => "Allow All (N)etwork",
1658
CMD_NET_FAMILY => "Allow Network Fa(m)ily",
1659
CMD_OVERWRITE => "(O)verwrite Profile",
1660
CMD_KEEP => "(K)eep Profile",
1661
CMD_CONTINUE => "(C)ontinue",
1664
sub UI_PromptUser ($) {
1668
if ($UI_Mode eq "text") {
1670
($cmd, $arg) = Text_PromptUser($q);
1674
$q->{type} = "wizard";
1677
my ($ypath, $yarg) = GetDataFromYast();
1679
$cmd = $yarg->{selection} || "CMD_ABORT";
1680
$arg = $yarg->{selected};
1683
if ($cmd eq "CMD_ABORT") {
1684
confirm_and_abort();
1685
$cmd = "XXXINVALIDXXX";
1686
} elsif ($cmd eq "CMD_FINISHED") {
1687
confirm_and_finish();
1688
$cmd = "XXXINVALIDXXX";
1692
return ($cmd, $arg);
1699
sub UI_ShortMessage($$) {
1700
my ($headline, $message) = @_;
1704
type => "short-dialog-message",
1705
headline => $headline,
1709
my ($ypath, $yarg) = GetDataFromYast();
1712
sub UI_LongMessage($$) {
1713
my ($headline, $message) = @_;
1715
$headline = "MISSING" if not defined $headline;
1716
$message = "MISSING" if not defined $message;
1720
type => "long-dialog-message",
1721
headline => $headline,
1725
my ($ypath, $yarg) = GetDataFromYast();
1728
##########################################################################
1729
# here are the interface functions to send data back and forth between
1730
# the yast frontend and the perl backend
1732
# this is super ugly, but waits for the next ycp Read command and sends data
1733
# back to the ycp front end.
1735
sub SendDataToYast($) {
1738
$DEBUGGING && debug "SendDataToYast: Waiting for YCP command";
1741
$DEBUGGING && debug "SendDataToYast: YCP: $_";
1742
my ($ycommand, $ypath, $yargument) = ycp::ParseCommand($_);
1744
if ($ycommand && $ycommand eq "Read") {
1747
my $debugmsg = Data::Dumper->Dump([$data], [qw(*data)]);
1748
debug "SendDataToYast: Sending--\n$debugmsg";
1756
$DEBUGGING && debug "SendDataToYast: Expected 'Read' but got-- $_";
1761
# if we ever break out here, something's horribly wrong.
1762
fatal_error "SendDataToYast: didn't receive YCP command before connection died";
1765
# this is super ugly, but waits for the next ycp Write command and grabs
1766
# whatever the ycp front end gives us
1768
sub GetDataFromYast() {
1770
$DEBUGGING && debug "GetDataFromYast: Waiting for YCP command";
1773
$DEBUGGING && debug "GetDataFromYast: YCP: $_";
1774
my ($ycmd, $ypath, $yarg) = ycp::ParseCommand($_);
1777
my $debugmsg = Data::Dumper->Dump([$yarg], [qw(*data)]);
1778
debug "GetDataFromYast: Received--\n$debugmsg";
1781
if ($ycmd && $ycmd eq "Write") {
1783
ycp::Return("true");
1784
return ($ypath, $yarg);
1787
$DEBUGGING && debug "GetDataFromYast: Expected 'Write' but got-- $_";
1791
# if we ever break out here, something's horribly wrong.
1792
fatal_error "GetDataFromYast: didn't receive YCP command before connection died";
1795
sub confirm_and_abort() {
1796
my $ans = UI_YesNo(gettext("Are you sure you want to abandon this set of profile changes and exit?"), "n");
1798
UI_Info(gettext("Abandoning all changes."));
1800
foreach my $prof (@created) {
1801
delete_profile($prof);
1807
sub confirm_and_finish() {
1811
sub build_x_functions($$$) {
1812
my ($default, $options, $exec_toggle) = @_;
1815
push @list, "CMD_ix" if $options =~ /i/;
1816
push @list, "CMD_pix" if $options =~ /p/ and $options =~ /i/;
1817
push @list, "CMD_cix" if $options =~ /c/ and $options =~ /i/;
1818
push @list, "CMD_nix" if $options =~ /n/ and $options =~ /i/;
1819
push @list, "CMD_ux" if $options =~ /u/;
1821
push @list, "CMD_ix" if $options =~ /i/;
1822
push @list, "CMD_px" if $options =~ /p/;
1823
push @list, "CMD_cx" if $options =~ /c/;
1824
push @list, "CMD_nx" if $options =~ /n/;
1825
push @list, "CMD_ux" if $options =~ /u/;
1828
push @list, "CMD_EXEC_IX_OFF" if $options =~/p|c|n/;
1830
push @list, "CMD_EXEC_IX_ON" if $options =~/p|c|n/;
1832
push @list, "CMD_DENY", "CMD_ABORT", "CMD_FINISHED";
1836
##########################################################################
1837
# this is the hideously ugly function that descends down the flow/event
1838
# trees that we've generated by parsing the logfile
1840
sub handlechildren($$$);
1842
sub handlechildren($$$) {
1843
my $profile = shift;
1847
my @entries = @$root;
1848
for my $entry (@entries) {
1849
fatal_error "$entry is not a ref" if not ref($entry);
1851
if (ref($entry->[0])) {
1852
handlechildren($profile, $hat, $entry);
1855
my @entry = @$entry;
1856
my $type = shift @entry;
1858
if ($type eq "fork") {
1859
my ($pid, $p, $h) = @entry;
1861
if ( ($p !~ /null(-complain)*-profile/)
1862
&& ($h !~ /null(-complain)*-profile/))
1869
$profilechanges{$pid} = $profile . "//" . $hat;
1871
$profilechanges{$pid} = $profile;
1873
} elsif ($type eq "unknown_hat") {
1874
my ($pid, $p, $h, $sdmode, $uhat) = @entry;
1876
if ($p !~ /null(-complain)*-profile/) {
1880
if ($sd{$profile}{$uhat}) {
1885
my $new_p = update_repo_profile($sd{$profile}{$profile});
1887
UI_SelectUpdatedRepoProfile($profile, $new_p) and
1888
$sd{$profile}{$uhat} ) {
1893
# figure out what our default hat for this application is.
1895
for my $hatglob (keys %{$cfg->{defaulthat}}) {
1896
$defaulthat = $cfg->{defaulthat}{$hatglob}
1897
if $profile =~ /$hatglob/;
1899
# keep track of previous answers for this run...
1900
my $context = $profile;
1901
$context .= " -> ^$uhat";
1902
my $ans = $transitions{$context} || "XXXINVALIDXXX";
1904
while ($ans !~ /^CMD_(ADDHAT|USEDEFAULT|DENY)$/) {
1907
push @{ $q->{headers} }, gettext("Profile"), $profile;
1909
push @{ $q->{headers} }, gettext("Default Hat"), $defaulthat;
1911
push @{ $q->{headers} }, gettext("Requested Hat"), $uhat;
1913
$q->{functions} = [];
1914
push @{ $q->{functions} }, "CMD_ADDHAT";
1915
push @{ $q->{functions} }, "CMD_USEDEFAULT" if $defaulthat;
1916
push @{$q->{functions}}, "CMD_DENY", "CMD_ABORT",
1919
$q->{default} = ($sdmode eq "PERMITTING") ? "CMD_ADDHAT" : "CMD_DENY";
1923
$ans = UI_PromptUser($q);
1926
$transitions{$context} = $ans;
1928
if ($ans eq "CMD_ADDHAT") {
1930
$sd{$profile}{$hat}{flags} = $sd{$profile}{$profile}{flags};
1931
} elsif ($ans eq "CMD_USEDEFAULT") {
1933
} elsif ($ans eq "CMD_DENY") {
1937
} elsif ($type eq "capability") {
1938
my ($pid, $p, $h, $prog, $sdmode, $capability) = @entry;
1940
if ( ($p !~ /null(-complain)*-profile/)
1941
&& ($h !~ /null(-complain)*-profile/))
1947
# print "$pid $profile $hat $prog $sdmode capability $capability\n";
1949
next unless $profile && $hat;
1951
$prelog{$sdmode}{$profile}{$hat}{capability}{$capability} = 1;
1952
} elsif (($type eq "path") || ($type eq "exec")) {
1953
my ($pid, $p, $h, $prog, $sdmode, $mode, $detail, $to_name) = @entry;
1955
$mode = 0 unless ($mode);
1957
if ( ($p !~ /null(-complain)*-profile/)
1958
&& ($h !~ /null(-complain)*-profile/))
1964
next unless $profile && $hat && $detail;
1965
my $domainchange = ($type eq "exec") ? "change" : "nochange";
1967
# escape special characters that show up in literal paths
1968
$detail =~ s/(\[|\]|\+|\*|\{|\})/\\$1/g;
1970
# we need to give the Execute dialog if they're requesting x
1971
# access for something that's not a directory - we'll force
1972
# a "ix" Path dialog for directories
1974
my $exec_target = $detail;
1976
if ($mode & str_to_mode("x")) {
1977
if (-d $exec_target) {
1978
$mode &= (~$ALL_AA_EXEC_TYPE);
1979
$mode |= str_to_mode("ix");
1985
if ($mode & $AA_MAY_LINK) {
1986
if ($detail =~ m/^from (.+) to (.+)$/) {
1987
my ($path, $target) = ($1, $2);
1989
my $frommode = str_to_mode("lr");
1990
if (defined $prelog{$sdmode}{$profile}{$hat}{path}{$path}) {
1991
$frommode |= $prelog{$sdmode}{$profile}{$hat}{path}{$path};
1993
$prelog{$sdmode}{$profile}{$hat}{path}{$path} = $frommode;
1995
my $tomode = str_to_mode("lr");
1996
if (defined $prelog{$sdmode}{$profile}{$hat}{path}{$target}) {
1997
$tomode |= $prelog{$sdmode}{$profile}{$hat}{path}{$target};
1999
$prelog{$sdmode}{$profile}{$hat}{path}{$target} = $tomode;
2001
# print "$pid $profile $hat $prog $sdmode $path:$frommode -> $target:$tomode\n";
2008
if (defined $prelog{$sdmode}{$profile}{$hat}{path}{$path}) {
2009
$mode |= $prelog{$sdmode}{$profile}{$hat}{path}{$path};
2011
$prelog{$sdmode}{$profile}{$hat}{path}{$path} = $mode;
2013
# print "$pid $profile $hat $prog $sdmode $mode $path\n";
2017
next if ( profile_known_exec($sd{$profile}{$hat},
2018
"exec", $exec_target ) );
2020
my $p = update_repo_profile($sd{$profile}{$profile});
2023
next if ( $to_name and
2024
UI_SelectUpdatedRepoProfile($profile, $p) and
2025
profile_known_exec($sd{$profile}{$hat},
2026
"exec", $to_name ) );
2028
next if ( UI_SelectUpdatedRepoProfile($profile, $p) and
2029
profile_known_exec($sd{$profile}{$hat},
2030
"exec", $exec_target ) );
2033
my $context = $profile;
2034
$context .= "^$hat" if $profile ne $hat;
2035
$context .= " -> $exec_target";
2036
my $ans = $transitions{$context} || "";
2038
my ($combinedmode, $combinedaudit, $cm, $am, @m);
2042
# does path match any regexps in original profile?
2043
($cm, $am, @m) = rematchfrag($sd{$profile}{$hat}, 'allow', $exec_target);
2044
$combinedmode |= $cm if $cm;
2045
$combinedaudit |= $am if $am;
2047
# find the named transition if is present
2048
if ($combinedmode & str_to_mode("x")) {
2050
foreach my $entry (@m) {
2051
if ($sd{$profile}{$hat}{allow}{path}{$entry}{to}) {
2052
$nt_name = $sd{$profile}{$hat}{allow}{path}{$entry}{to};
2056
if ($to_name and $nt_name and ($to_name ne $nt_name)) {
2057
#fatal_error "transition name from "
2058
} elsif ($nt_name) {
2059
$to_name = $nt_name;
2063
# does path match anything pulled in by includes in
2065
($cm, $am, @m) = match_prof_incs_to_path($sd{$profile}{$hat}, 'allow', $exec_target);
2066
$combinedmode |= $cm if $cm;
2067
$combinedaudit |= $am if $am;
2068
if ($combinedmode & str_to_mode("x")) {
2070
foreach my $entry (@m) {
2071
if ($sd{$profile}{$hat}{allow}{path}{$entry}{to}) {
2072
$nt_name = $sd{$profile}{$hat}{allow}{path}{$entry}{to};
2076
if ($to_name and $nt_name and ($to_name ne $nt_name)) {
2077
#fatal_error "transition name from "
2078
} elsif ($nt_name) {
2079
$to_name = $nt_name;
2084
#nx does not exist in profiles. It does in log
2085
#files however. The log parsing routines will convert
2086
#it to its profile form.
2087
#nx is internally represented by cx/px/cix/pix + to_name
2089
if (contains($combinedmode, "pix")) {
2095
$exec_mode = str_to_mode("pixr");
2096
} elsif (contains($combinedmode, "cix")) {
2102
$exec_mode = str_to_mode("cixr");
2103
} elsif (contains($combinedmode, "Pix")) {
2105
$ans = "CMD_nix_safe";
2107
$ans = "CMD_pix_safe";
2109
$exec_mode = str_to_mode("Pixr");
2110
} elsif (contains($combinedmode, "Cix")) {
2112
$ans = "CMD_nix_safe";
2114
$ans = "CMD_cix_safe";
2116
$exec_mode = str_to_mode("Cixr");
2117
} elsif (contains($combinedmode, "ix")) {
2119
$exec_mode = str_to_mode("ixr");
2120
} elsif (contains($combinedmode, "px")) {
2126
$exec_mode = str_to_mode("px");
2127
} elsif (contains($combinedmode, "cx")) {
2133
$exec_mode = str_to_mode("cx");
2134
} elsif (contains($combinedmode, "ux")) {
2136
$exec_mode = str_to_mode("ux");
2137
} elsif (contains($combinedmode, "Px")) {
2139
$ans = "CMD_nx_safe";
2141
$ans = "CMD_px_safe";
2143
$exec_mode = str_to_mode("Px");
2144
} elsif (contains($combinedmode, "Cx")) {
2146
$ans = "CMD_nx_safe";
2148
$ans = "CMD_cx_safe";
2150
$exec_mode = str_to_mode("Cx");
2151
} elsif (contains($combinedmode, "Ux")) {
2152
$ans = "CMD_ux_safe";
2153
$exec_mode = str_to_mode("Ux");
2155
my $options = $cfg->{qualifiers}{$exec_target} || "ipcnu";
2156
fatal_error "$entry has transition name but not transition mode" if $to_name;
2158
# force "ix" as the only option when the profiled
2159
# program executes itself
2160
$options = "i" if $exec_target eq $profile;
2162
# for now don't allow hats to cx
2163
$options =~ s/c// if $hat and $hat ne $profile;
2165
# we always need deny...
2168
# figure out what our default option should be...
2171
&& -e getprofilefilename($exec_target))
2173
$default = "CMD_px";
2174
} elsif ($options =~ /i/) {
2175
$default = "CMD_ix";
2176
} elsif ($options =~ /c/) {
2177
$default = "CMD_cx";
2178
} elsif ($options =~ /n/) {
2179
$default = "CMD_nx";
2181
$default = "CMD_DENY";
2184
# ugh, this doesn't work if someone does an ix before
2185
# calling this particular child process. at least
2186
# it's only a hint instead of mandatory to get this
2188
my $parent_uses_ld_xxx = check_for_LD_XXX($profile);
2190
my $severity = $sevdb->rank($exec_target, "x");
2192
# build up the prompt...
2195
push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
2196
if ($prog && $prog ne "HINT") {
2197
push @{ $q->{headers} }, gettext("Program"), $prog;
2199
# $to_name should NOT exist here other wise we know what
2200
# mode we are supposed to be transitioning to
2201
# which is handled above.
2202
push @{ $q->{headers} }, gettext("Execute"), $exec_target;
2203
push @{ $q->{headers} }, gettext("Severity"), $severity;
2205
$q->{functions} = [];
2207
my $prompt = "\n$context\n";
2208
my $exec_toggle = 0;
2210
push @{ $q->{functions} }, build_x_functions($default, $options, $exec_toggle);
2212
$options = join("|", split(//, $options));
2216
while ($ans !~ m/^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)$/) {
2217
$ans = UI_PromptUser($q);
2219
if ($ans =~ /CMD_EXEC_IX_/) {
2220
$exec_toggle = !$exec_toggle;
2222
$q->{functions} = [ ];
2223
push @{ $q->{functions} }, build_x_functions($default, $options, $exec_toggle);
2227
if ($ans =~ /CMD_(nx|nix)/) {
2228
my $arg = $exec_target;
2231
if ($profile eq $hat) {
2232
$ynans = UI_YesNo("Are you specifying a transition to a local profile?", "n");
2235
if ($ynans eq "y") {
2236
if ($ans eq "CMD_nx") {
2242
if ($ans eq "CMD_nx") {
2248
$to_name = UI_GetString(gettext("Enter profile name to transition to: "), $arg);
2250
if ($ans =~ /CMD_ix/) {
2251
$exec_mode = str_to_mode("ix");
2252
} elsif ($ans =~ /CMD_(px|cx|nx|pix|cix|nix)/) {
2254
$exec_mode = str_to_mode($match);
2255
my $px_default = "n";
2256
my $px_mesg = gettext("Should AppArmor sanitize the environment when\nswitching profiles?\n\nSanitizing the environment is more secure,\nbut some applications depend on the presence\nof LD_PRELOAD or LD_LIBRARY_PATH.");
2257
if ($parent_uses_ld_xxx) {
2258
$px_mesg = gettext("Should AppArmor sanitize the environment when\nswitching profiles?\n\nSanitizing the environment is more secure,\nbut this application appears to use LD_PRELOAD\nor LD_LIBRARY_PATH and clearing these could\ncause functionality problems.");
2260
my $ynans = UI_YesNo($px_mesg, $px_default);
2261
$ans = "CMD_$match";
2262
if ($ynans eq "y") {
2263
$exec_mode &= ~($AA_EXEC_UNSAFE | ($AA_EXEC_UNSAFE << $AA_OTHER_SHIFT));
2265
} elsif ($ans eq "CMD_ux") {
2266
$exec_mode = str_to_mode("ux");
2267
my $ynans = UI_YesNo(sprintf(gettext("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");
2268
if ($ynans eq "y") {
2269
my $ynans = UI_YesNo(gettext("Should AppArmor sanitize the environment when\nrunning this program unconfined?\n\nNot sanitizing the environment when unconfining\na program opens up significant security holes\nand should be avoided if at all possible."), "y");
2270
if ($ynans eq "y") {
2271
$exec_mode &= ~($AA_EXEC_UNSAFE | ($AA_EXEC_UNSAFE << $AA_OTHER_SHIFT));
2278
$transitions{$context} = $ans;
2280
if ($ans =~ /CMD_(ix|px|cx|nx|pix|cix|nix)/) {
2281
# if we're inheriting, things'll bitch unless we have r
2282
if ($exec_mode & str_to_mode("i")) {
2283
$exec_mode |= str_to_mode("r");
2287
if ($ans eq "CMD_DENY") {
2288
$sd{$profile}{$hat}{deny}{path}{$exec_target}{mode} |= str_to_mode("x");
2290
$sd{$profile}{$hat}{deny}{path}{$exec_target}{audit} |= 0;
2291
$changed{$profile} = 1;
2292
# skip all remaining events if they say to deny
2294
return if $domainchange eq "change";
2299
unless ($ans eq "CMD_DENY") {
2300
# ???? if its defined in the prelog we shouldn't have asked
2301
if (defined $prelog{PERMITTING}{$profile}{$hat}{path}{$exec_target}) {
2302
# $exec_mode = $prelog{PERMITTING}{$profile}{$hat}{path}{$exec_target};
2305
$prelog{PERMITTING}{$profile}{$hat}{path}{$exec_target} |= $exec_mode;
2306
$log{PERMITTING}{$profile} = {};
2307
$sd{$profile}{$hat}{allow}{path}{$exec_target}{mode} |= $exec_mode;
2308
$sd{$profile}{$hat}{allow}{path}{$exec_target}{audit} |= 0;
2309
$sd{$profile}{$hat}{allow}{path}{$exec_target}{to} = $to_name if ($to_name);
2311
# mark this profile as changed
2312
$changed{$profile} = 1;
2314
if ($exec_mode & str_to_mode("i")) {
2315
if ($exec_target =~ /perl/) {
2316
$sd{$profile}{$hat}{include}{"abstractions/perl"} = 1;
2317
} elsif ($detail =~ m/\/bin\/(bash|sh)/) {
2318
$sd{$profile}{$hat}{include}{"abstractions/bash"} = 1;
2320
my $hashbang = head($exec_target);
2321
if ($hashbang =~ /^#!\s*(\S+)/) {
2322
my $interpreter = get_full_path($1);
2323
$sd{$profile}{$hat}{path}->{$interpreter}{mode} |= str_to_mode("ix");
2324
$sd{$profile}{$hat}{path}->{$interpreter}{audit} |= 0;
2325
if ($interpreter =~ /perl/) {
2326
$sd{$profile}{$hat}{include}{"abstractions/perl"} = 1;
2327
} elsif ($interpreter =~ m/\/bin\/(bash|sh)/) {
2328
$sd{$profile}{$hat}{include}{"abstractions/bash"} = 1;
2335
# print "$pid $profile $hat EXEC $exec_target $ans $exec_mode\n";
2337
# update our tracking info based on what kind of change
2339
if ($ans eq "CMD_ix") {
2341
$profilechanges{$pid} = $profile . "//" . $hat;
2343
$profilechanges{$pid} = $profile;
2345
} elsif ($ans =~ /^CMD_(px|nx|pix|nix)/) {
2346
$exec_target = $to_name if ($to_name);
2347
if ($sdmode eq "PERMITTING") {
2348
if ($domainchange eq "change") {
2349
$profile = $exec_target;
2350
$hat = $exec_target;
2351
$profilechanges{$pid} = $profile;
2354
# if they want to use px, make sure a profile
2355
# exists for the target.
2356
unless (-e getprofilefilename($exec_target)) {
2358
if ($exec_mode & str_to_mode("i")) {
2359
$ynans = UI_YesNo(sprintf(gettext("A profile for %s does not exist. Create one?"), $exec_target), "n");
2361
if ($ynans eq "y") {
2362
$helpers{$exec_target} = "enforce";
2364
autodep_base("", $exec_target);
2366
autodep_base($exec_target, "");
2368
reload_base($exec_target);
2371
} elsif ($ans =~ /^CMD_(cx|cix)/) {
2372
$exec_target = $to_name if ($to_name);
2373
if ($sdmode eq "PERMITTING") {
2374
if ($domainchange eq "change") {
2375
$profilechanges{$pid} = "${profile}//${exec_target}";
2376
# $profile = $exec_target;
2377
# $hat = $exec_target;
2381
# if they want to use cx, make sure a profile
2382
# exists for the target.
2383
unless ($sd{$profile}{$exec_target}) {
2385
if ($exec_mode & str_to_mode("i")) {
2386
$ynans = UI_YesNo(sprintf(gettext("A local profile for %s does not exist. Create one?"), $exec_target), "n");
2388
if ($ynans eq "y") {
2389
$hat = $exec_target;
2390
# keep track of profile flags
2391
#$profile_data->{$profile}{$hat}{flags} = ;
2393
# we have seen more than a declaration so clear it
2394
$sd{$profile}{$hat}{'declared'} = 0;
2395
$sd{$profile}{$hat}{profile} = 1;
2397
# Otherwise sub-profiles end up getting
2398
# put in enforce mode with genprof
2399
$sd{$profile}{$hat}{flags} = $sd{$profile}{$profile}{flags} if $profile ne $hat;
2401
$sd{$profile}{$hat}{flags} = 'complain';
2402
$sd{$profile}{$hat}{allow}{path} = { };
2403
$sd{$profile}{$hat}{allow}{netdomain} = { };
2404
my $file = $sd{$profile}{$profile}{filename};
2405
$filelist{$file}{profiles}{$profile}{$hat} = 1;
2409
} elsif ($ans =~ /^CMD_ux/) {
2410
$profilechanges{$pid} = "unconfined";
2411
return if $domainchange eq "change";
2414
} elsif ( $type eq "netdomain" ) {
2415
my ($pid, $p, $h, $prog, $sdmode, $family, $sock_type, $protocol) =
2418
if ( ($p !~ /null(-complain)*-profile/)
2419
&& ($h !~ /null(-complain)*-profile/))
2425
next unless $profile && $hat;
2431
{$sock_type} = 1 unless ( !$family || !$sock_type );
2438
sub add_to_tree ($$$@) {
2439
my ($pid, $parent, $type, @event) = @_;
2441
my $eventmsg = Data::Dumper->Dump([@event], [qw(*event)]);
2442
$eventmsg =~ s/\n/ /g;
2443
debug " add_to_tree: pid [$pid] type [$type] event [ $eventmsg ]";
2446
unless (exists $pid{$pid}) {
2447
my $profile = $event[0];
2448
my $hat = $event[1];
2449
if ($parent && exists $pid{$parent}) {
2450
# fork entry is missing fake one so that fork tracking will work
2451
$hat ||= "null-complain-profile";
2453
push @{ $pid{$parent} }, $arrayref;
2454
$pid{$pid} = $arrayref;
2455
push @{$arrayref}, [ "fork", $pid, $profile, $hat ];
2458
push @log, $arrayref;
2459
$pid{$pid} = $arrayref;
2463
push @{ $pid{$pid} }, [ $type, $pid, @event ];
2467
# variables used in the logparsing routines
2470
our $next_log_entry;
2473
my $RE_LOG_v2_0_syslog = qr/SubDomain/;
2474
my $RE_LOG_v2_1_syslog = qr/kernel:\s+(\[[\d\.\s]+\]\s+)?(audit\([\d\.\:]+\):\s+)?type=150[1-6]/;
2475
my $RE_LOG_v2_6_syslog = qr/kernel:\s+(\[[\d\.\s]+\]\s+)?type=\d+\s+audit\([\d\.\:]+\):\s+apparmor=/;
2476
my $RE_LOG_v2_0_audit =
2477
qr/type=(APPARMOR|UNKNOWN\[1500\]) msg=audit\([\d\.\:]+\):/;
2478
my $RE_LOG_v2_1_audit =
2479
qr/type=(UNKNOWN\[150[1-6]\]|APPARMOR_(AUDIT|ALLOWED|DENIED|HINT|STATUS|ERROR))/;
2480
my $RE_LOG_v2_6_audit =
2481
qr/type=AVC\s+(msg=)?audit\([\d\.\:]+\):\s+apparmor=/;
2483
sub prefetch_next_log_entry() {
2484
# if we already have an existing cache entry, something's broken
2485
if ($next_log_entry) {
2486
print STDERR "Already had next log entry: $next_log_entry";
2489
# read log entries until we either hit the end or run into an
2490
# AA event message format we recognize
2492
$next_log_entry = <$LOG>;
2493
$DEBUGGING && debug "prefetch_next_log_entry: next_log_entry = " . ($next_log_entry ? $next_log_entry : "empty");
2494
} until (!$next_log_entry || $next_log_entry =~ m{
2495
$RE_LOG_v2_0_syslog |
2496
$RE_LOG_v2_0_audit |
2497
$RE_LOG_v2_1_audit |
2498
$RE_LOG_v2_1_syslog |
2499
$RE_LOG_v2_6_syslog |
2500
$RE_LOG_v2_6_audit |
2505
sub get_next_log_entry() {
2506
# make sure we've got a next log entry if there is one
2507
prefetch_next_log_entry() unless $next_log_entry;
2509
# save a copy of the next log entry...
2510
my $log_entry = $next_log_entry;
2512
# zero out our cache of the next log entry
2513
$next_log_entry = undef;
2518
sub peek_at_next_log_entry() {
2519
# make sure we've got a next log entry if there is one
2520
prefetch_next_log_entry() unless $next_log_entry;
2522
# return a copy of the next log entry without pulling it out of the cache
2523
return $next_log_entry;
2526
sub throw_away_next_log_entry() {
2527
$next_log_entry = undef;
2530
sub parse_log_record_v_2_0 ($$) {
2531
my ($record, $last) = @_;
2532
$DEBUGGING && debug "parse_log_record_v_2_0: $record";
2534
# What's this early out for? As far as I can tell, parse_log_record_v_2_0
2535
# won't ever be called without something in $record
2536
return $last if ( ! $record );
2540
if (s/(PERMITTING|REJECTING)-SYSLOGFIX/$1/) {
2544
if (m/LOGPROF-HINT unknown_hat (\S+) pid=(\d+) profile=(.+) active=(.+)/) {
2545
my ($uhat, $pid, $profile, $hat) = ($1, $2, $3, $4);
2549
# we want to ignore entries for profiles that don't exist
2550
# they're most likely broken entries or old entries for
2553
if ( ($profile ne 'null-complain-profile')
2554
&& (!profile_exists($profile)));
2556
add_to_tree($pid, 0, "unknown_hat", $profile, $hat,
2557
"PERMITTING", $uhat);
2558
} elsif (m/LOGPROF-HINT (unknown_profile|missing_mandatory_profile) image=(.+) pid=(\d+) profile=(.+) active=(.+)/) {
2559
my ($image, $pid, $profile, $hat) = ($2, $3, $4, $5);
2561
return $& if $last =~ /PERMITTING x access to $image/;
2564
# we want to ignore entries for profiles that don't exist
2565
# they're most likely broken entries or old entries for
2568
if ( ($profile ne 'null-complain-profile')
2569
&& (!profile_exists($profile)));
2571
add_to_tree($pid, 0, "exec", $profile, $hat, "HINT", "PERMITTING", "x", $image);
2573
} elsif (m/(PERMITTING|REJECTING) (\S+) access (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
2574
my ($sdmode, $mode, $detail, $prog, $pid, $profile, $hat) =
2575
($1, $2, $3, $4, $5, $6, $7);
2577
if ($mode eq "link") {
2580
if (!validate_log_mode($mode)) {
2581
fatal_error(sprintf(gettext('Log contains unknown mode %s.'), $mode));
2584
my $domainchange = "nochange";
2587
# we need to try to check if we're doing a domain transition
2588
if ($sdmode eq "PERMITTING") {
2589
my $following = peek_at_next_log_entry();
2591
if ($following && ($following =~ m/changing_profile/)) {
2592
$domainchange = "change";
2593
throw_away_next_log_entry();
2598
# we want to ignore duplicates for things other than executes...
2599
return $& if $seen{$&};
2605
# we want to ignore entries for profiles that don't exist
2606
# they're most likely broken entries or old entries for
2608
if (($profile ne 'null-complain-profile')
2609
&& (!profile_exists($profile)))
2614
# currently no way to stick pipe mediation in a profile, ignore
2615
# any messages like this
2616
return $& if $detail =~ /to pipe:/;
2618
# strip out extra extended attribute info since we don't
2619
# currently have a way to specify it in the profile and
2620
# instead just need to provide the access to the base filename
2621
$detail =~ s/\s+extended attribute \S+//;
2623
# kerberos code checks to see if the krb5.conf file is world
2624
# writable in a stupid way so we'll ignore any w accesses to
2626
return $& if (($detail eq "to /etc/krb5.conf") && contains($mode, "w"));
2628
# strip off the (deleted) tag that gets added if it's a
2630
$detail =~ s/\s+\(deleted\)$//;
2632
# next if (($detail =~ /to \/lib\/ld-/) && ($mode =~ /x/));
2634
$detail =~ s/^to\s+//;
2636
if ($domainchange eq "change") {
2637
add_to_tree($pid, 0, "exec", $profile, $hat, $prog,
2638
$sdmode, str_to_mode($mode), $detail);
2640
add_to_tree($pid, 0, "path", $profile, $hat, $prog,
2641
$sdmode, str_to_mode($mode), $detail);
2644
} elsif (m/(PERMITTING|REJECTING) (?:mk|rm)dir on (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
2645
my ($sdmode, $path, $prog, $pid, $profile, $hat) =
2646
($1, $2, $3, $4, $5, $6);
2648
# we want to ignore duplicates for things other than executes...
2649
return $& if $seen{$&}++;
2653
# we want to ignore entries for profiles that don't exist
2654
# they're most likely broken entries or old entries for
2657
if ( ($profile ne 'null-complain-profile')
2658
&& (!profile_exists($profile)));
2660
add_to_tree($pid, 0, "path", $profile, $hat, $prog, $sdmode,
2663
} elsif (m/(PERMITTING|REJECTING) xattr (\S+) on (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
2664
my ($sdmode, $xattr_op, $path, $prog, $pid, $profile, $hat) =
2665
($1, $2, $3, $4, $5, $6, $7);
2667
# we want to ignore duplicates for things other than executes...
2668
return $& if $seen{$&}++;
2672
# we want to ignore entries for profiles that don't exist
2673
# they're most likely broken entries or old entries for
2676
if ( ($profile ne 'null-complain-profile')
2677
&& (!profile_exists($profile)));
2680
if ($xattr_op eq "get" || $xattr_op eq "list") {
2682
} elsif ($xattr_op eq "set" || $xattr_op eq "remove") {
2687
add_to_tree($pid, 0, "path", $profile, $hat, $prog, $sdmode,
2688
str_to_mode($xattrmode), $path);
2691
} elsif (m/(PERMITTING|REJECTING) attribute \((.*?)\) change to (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
2692
my ($sdmode, $change, $path, $prog, $pid, $profile, $hat) =
2693
($1, $2, $3, $4, $5, $6, $7);
2695
# we want to ignore duplicates for things other than executes...
2696
return $& if $seen{$&};
2701
# we want to ignore entries for profiles that don't exist
2702
# they're most likely broken entries or old entries for
2705
if ( ($profile ne 'null-complain-profile')
2706
&& (!profile_exists($profile)));
2708
# kerberos code checks to see if the krb5.conf file is world
2709
# writable in a stupid way so we'll ignore any w accesses to
2711
return $& if $path eq "/etc/krb5.conf";
2713
add_to_tree($pid, 0, "path", $profile, $hat, $prog, $sdmode,
2714
str_to_mode("w"), $path);
2716
} elsif (m/(PERMITTING|REJECTING) access to capability '(\S+)' \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
2717
my ($sdmode, $capability, $prog, $pid, $profile, $hat) =
2718
($1, $2, $3, $4, $5, $6);
2720
return $& if $seen{$&};
2725
# we want to ignore entries for profiles that don't exist - they're
2726
# most likely broken entries or old entries for deleted profiles
2728
if ( ($profile ne 'null-complain-profile')
2729
&& (!profile_exists($profile)));
2731
add_to_tree($pid, 0, "capability", $profile, $hat, $prog,
2732
$sdmode, $capability);
2734
} elsif (m/Fork parent (\d+) child (\d+) profile (.+) active (.+)/
2735
|| m/LOGPROF-HINT fork pid=(\d+) child=(\d+) profile=(.+) active=(.+)/
2736
|| m/LOGPROF-HINT fork pid=(\d+) child=(\d+)/)
2738
my ($parent, $child, $profile, $hat) = ($1, $2, $3, $4);
2740
$profile ||= "null-complain-profile";
2741
$hat ||= "null-complain-profile";
2745
# we want to ignore entries for profiles that don't exist
2746
# they're most likely broken entries or old entries for
2749
if ( ($profile ne 'null-complain-profile')
2750
&& (!profile_exists($profile)));
2753
if (exists $pid{$parent}) {
2754
push @{ $pid{$parent} }, $arrayref;
2756
push @log, $arrayref;
2758
$pid{$child} = $arrayref;
2759
push @{$arrayref}, [ "fork", $child, $profile, $hat ];
2761
$DEBUGGING && debug "UNHANDLED: $_";
2766
sub parse_log_record ($) {
2768
$DEBUGGING && debug "parse_log_record: $record";
2769
my $e = parse_event($record);
2775
sub add_event_to_tree ($) {
2778
my $sdmode = $e->{sdmode}?$e->{sdmode}:"UNKNOWN";
2780
if ( $e->{type} =~ /(UNKNOWN\[1501\]|APPARMOR_AUDIT|1501)/ ) {
2782
} elsif ( $e->{type} =~ /(UNKNOWN\[1502\]|APPARMOR_ALLOWED|1502)/ ) {
2783
$sdmode = "PERMITTING";
2784
} elsif ( $e->{type} =~ /(UNKNOWN\[1503\]|APPARMOR_DENIED|1503)/ ) {
2785
$sdmode = "REJECTING";
2786
} elsif ( $e->{type} =~ /(UNKNOWN\[1504\]|APPARMOR_HINT|1504)/ ) {
2788
} elsif ( $e->{type} =~ /(UNKNOWN\[1505\]|APPARMOR_STATUS|1505)/ ) {
2790
} elsif ( $e->{type} =~ /(UNKNOWN\[1506\]|APPARMOR_ERROR|1506)/ ) {
2793
$sdmode = "UNKNOWN";
2796
return if ( $sdmode =~ /UNKNOWN|AUDIT|STATUS|ERROR/ );
2797
return if ($e->{operation} =~ /profile_set/);
2799
my ($profile, $hat);
2801
# The version of AppArmor that was accepted into the mainline kernel
2802
# issues audit events for things like change_hat while unconfined.
2803
# Previous versions just returned -EPERM without the audit so the
2804
# events wouldn't have been picked up here.
2805
return if (!$e->{profile});
2807
# just convert new null profile style names to old before we begin processing
2808
# profile and name can contain multiple layers of null- but all we care about
2809
# currently is single level.
2810
if ($e->{profile} =~ m/\/\/null-/) {
2811
$e->{profile} = "null-complain-profile";
2813
($profile, $hat) = split /\/\//, $e->{profile};
2814
if ( $e->{operation} eq "change_hat" ) {
2815
#screen out change_hat events that aren't part of learning, as before
2816
#AppArmor 2.4 these events only happend as hints during learning
2817
return if ($sdmode ne "HINT" && $sdmode ne "PERMITTING");
2818
($profile, $hat) = split /\/\//, $e->{name};
2820
$hat = $profile if ( !$hat );
2821
# TODO - refactor add_to_tree as prog is no longer supplied
2822
# HINT is from previous format where prog was not
2823
# consistently passed
2826
return if ($profile ne 'null-complain-profile' && !profile_exists($profile));
2828
if ($e->{operation} eq "exec") {
2829
if ( defined $e->{info} && $e->{info} eq "mandatory profile missing" ) {
2830
add_to_tree( $e->{pid},
2841
} elsif ( defined $e->{name2} && $e->{name2} =~ m/\/\/null-/) {
2842
add_to_tree( $e->{pid},
2854
} elsif ($e->{operation} =~ m/file_/) {
2855
add_to_tree( $e->{pid},
2866
} elsif ($e->{operation} eq "open" ||
2867
$e->{operation} eq "truncate" ||
2868
$e->{operation} eq "mkdir" ||
2869
$e->{operation} eq "mknod" ||
2870
$e->{operation} eq "rename_src" ||
2871
$e->{operation} eq "rename_dest" ||
2872
$e->{operation} =~ m/^(unlink|rmdir|symlink_create|link)$/) {
2873
add_to_tree( $e->{pid},
2884
} elsif ($e->{operation} eq "capable") {
2885
add_to_tree( $e->{pid},
2894
} elsif ($e->{operation} =~ m/xattr/ ||
2895
$e->{operation} eq "setattr") {
2896
add_to_tree( $e->{pid},
2907
} elsif ($e->{operation} =~ m/inode_/) {
2908
my $is_domain_change = 0;
2910
if ($e->{operation} eq "inode_permission" &&
2911
$e->{denied_mask} & $AA_MAY_EXEC &&
2912
$sdmode eq "PERMITTING") {
2914
my $following = peek_at_next_log_entry();
2916
my $entry = parse_log_record($following);
2919
$entry->{info} eq "set profile" ) {
2921
$is_domain_change = 1;
2922
throw_away_next_log_entry();
2927
if ($is_domain_change) {
2928
add_to_tree( $e->{pid},
2940
add_to_tree( $e->{pid},
2952
} elsif ($e->{operation} eq "sysctl") {
2953
add_to_tree( $e->{pid},
2964
} elsif ($e->{operation} eq "clone") {
2965
my ($parent, $child) = ($e->{pid}, $e->{task});
2966
$profile ||= "null-complain-profile";
2967
$hat ||= "null-complain-profile";
2969
if (exists $pid{$parent}) {
2970
push @{ $pid{$parent} }, $arrayref;
2972
push @log, $arrayref;
2974
$pid{$child} = $arrayref;
2975
push @{$arrayref}, [ "fork", $child, $profile, $hat ];
2976
} elsif (optype($e->{operation}) eq "net") {
2977
add_to_tree( $e->{pid},
2988
} elsif ($e->{operation} eq "change_hat") {
2989
add_to_tree($e->{pid}, $e->{parent}, "unknown_hat", $profile, $hat, $sdmode, $hat);
2992
my $msg = Data::Dumper->Dump([$e], [qw(*event)]);
2993
debug "UNHANDLED: $msg";
3000
$seenmark = $logmark ? 0 : 1;
3004
# okay, done loading the previous profiles, get on to the good stuff...
3005
open($LOG, $filename)
3006
or fatal_error "Can't read AppArmor logfile $filename: $!";
3007
while ($_ = get_next_log_entry()) {
3010
$DEBUGGING && debug "read_log: $_";
3012
$seenmark = 1 if /$logmark/;
3014
$DEBUGGING && debug "read_log: seenmark = $seenmark";
3015
next unless $seenmark;
3017
my $last_match = ""; # v_2_0 syslog record parsing requires
3018
# the previous aa record in the mandatory profile
3020
# all we care about is apparmor messages
3021
if (/$RE_LOG_v2_0_syslog/ || /$RE_LOG_v2_0_audit/) {
3022
$last_match = parse_log_record_v_2_0( $_, $last_match );
3024
my $event = parse_log_record($_);
3025
add_event_to_tree($event) if ( $event );
3033
sub UI_SelectUpdatedRepoProfile ($$) {
3035
my ($profile, $p) = @_;
3036
my $distro = $cfg->{repository}{distro};
3037
my $url = $sd{$profile}{$profile}{repo}{url};
3038
my $user = $sd{$profile}{$profile}{repo}{user};
3039
my $id = $sd{$profile}{$profile}{repo}{id};
3045
"Profile", $profile,
3047
"Old Revision", $id,
3048
"New Revision", $p->{id},
3051
gettext( "An updated version of this profile has been found in the profile repository. Would you like to use it?");
3053
"CMD_VIEW_CHANGES", "CMD_UPDATE_PROFILE", "CMD_IGNORE_UPDATE",
3054
"CMD_ABORT", "CMD_FINISHED"
3059
$ans = UI_PromptUser($q);
3061
if ($ans eq "CMD_VIEW_CHANGES") {
3062
my $oldprofile = serialize_profile($sd{$profile}, $profile);
3063
my $newprofile = $p->{profile};
3064
display_changes($oldprofile, $newprofile);
3066
} until $ans =~ /^CMD_(UPDATE_PROFILE|IGNORE_UPDATE)/;
3068
if ($ans eq "CMD_UPDATE_PROFILE") {
3071
parse_profile_data($p->{profile}, getprofilefilename($profile), 0);
3072
if ($profile_data) {
3073
attach_profile_data(\%sd, $profile_data);
3074
$changed{$profile} = 1;
3077
set_repo_info($sd{$profile}{$profile}, $url, $user, $p->{id});
3081
gettext("Updated profile %s to revision %s."),
3088
UI_Info(gettext("Error parsing repository profile."));
3097
sub UI_repo_signup() {
3099
my ($url, $res, $save_config, $newuser, $user, $pass, $email, $signup_okay);
3100
$url = $cfg->{repository}{url};
3102
if ($UI_Mode eq "yast") {
3105
type => "dialog-repo-sign-in",
3109
my ($ypath, $yarg) = GetDataFromYast();
3110
$email = $yarg->{email};
3111
$user = $yarg->{user};
3112
$pass = $yarg->{pass};
3113
$newuser = $yarg->{newuser};
3114
$save_config = $yarg->{save_config};
3115
if ($yarg->{cancelled} && $yarg->{cancelled} eq "y") {
3118
$DEBUGGING && debug("AppArmor Repository: \n\t " .
3120
"New User\n\temail: [" . $email . "]" :
3121
"Signin" . "\n\t user[" . $user . "]" .
3122
"password [" . $pass . "]\n");
3124
$newuser = UI_YesNo(gettext("Create New User?"), "n");
3125
$user = UI_GetString(gettext("Username: "), $user);
3126
$pass = UI_GetString(gettext("Password: "), $pass);
3127
$email = UI_GetString(gettext("Email Addr: "), $email)
3128
if ($newuser eq "y");
3129
$save_config = UI_YesNo(gettext("Save Configuration? "), "y");
3132
if ($newuser eq "y") {
3133
my ($status_ok,$res) = user_register($url, $user, $pass, $email);
3138
gettext("The Profile Repository server returned the following error:") .
3139
"\n" . $res?$res:gettext("UNKOWN ERROR") . "\n" .
3140
gettext("Please re-enter registration information or contact the administrator.");
3141
UI_Important(gettext("Login Error\n") . $errmsg);
3144
my ($status_ok,$res) = user_login($url, $user, $pass);
3148
my $errmsg = gettext("Login failure\n Please check username and password and try again.") . "\n" . $res;
3149
UI_Important($errmsg);
3152
} until $signup_okay;
3154
$repo_cfg->{repository}{user} = $user;
3155
$repo_cfg->{repository}{pass} = $pass;
3156
$repo_cfg->{repository}{email} = $email;
3158
write_config("repository.conf", $repo_cfg) if ( $save_config eq "y" );
3160
return ($user, $pass);
3163
sub UI_ask_to_enable_repo() {
3166
return if ( not defined $cfg->{repository}{url} );
3168
gettext("Repository"), $cfg->{repository}{url},
3170
$q->{explanation} = gettext( "Would you like to enable access to the
3171
profile repository?" ); $q->{functions} = [ "CMD_ENABLE_REPO",
3172
"CMD_DISABLE_REPO", "CMD_ASK_LATER", ];
3176
$cmd = UI_PromptUser($q);
3177
} until $cmd =~ /^CMD_(ENABLE_REPO|DISABLE_REPO|ASK_LATER)/;
3179
if ($cmd eq "CMD_ENABLE_REPO") {
3180
$repo_cfg->{repository}{enabled} = "yes";
3181
} elsif ($cmd eq "CMD_DISABLE_REPO") {
3182
$repo_cfg->{repository}{enabled} = "no";
3183
} elsif ($cmd eq "CMD_ASK_LATER") {
3184
$repo_cfg->{repository}{enabled} = "later";
3187
eval { write_config("repository.conf", $repo_cfg) };
3194
sub UI_ask_to_upload_profiles() {
3198
gettext("Repository"), $cfg->{repository}{url},
3201
gettext( "Would you like to upload newly created and changed profiles to
3202
the profile repository?" );
3204
"CMD_YES", "CMD_NO", "CMD_ASK_LATER",
3209
$cmd = UI_PromptUser($q);
3210
} until $cmd =~ /^CMD_(YES|NO|ASK_LATER)/;
3212
if ($cmd eq "CMD_NO") {
3213
$repo_cfg->{repository}{upload} = "no";
3214
} elsif ($cmd eq "CMD_YES") {
3215
$repo_cfg->{repository}{upload} = "yes";
3216
} elsif ($cmd eq "CMD_ASK_LATER") {
3217
$repo_cfg->{repository}{upload} = "later";
3220
eval { write_config("repository.conf", $repo_cfg) };
3227
sub parse_repo_profile($$$) {
3228
my ($fqdbin, $repo_url, $profile) = @_;
3230
my $profile_data = eval {
3231
parse_profile_data($profile->{profile}, getprofilefilename($fqdbin), 0);
3234
print STDERR "PARSING ERROR: $@\n";
3235
$profile_data = undef;
3238
if ($profile_data) {
3239
set_repo_info($profile_data->{$fqdbin}{$fqdbin}, $repo_url,
3240
$profile->{username}, $profile->{id});
3243
return $profile_data;
3247
sub set_repo_info($$$$) {
3248
my ($profile_data, $repo_url, $username, $id) = @_;
3250
# save repository metadata
3251
$profile_data->{repo}{url} = $repo_url;
3252
$profile_data->{repo}{user} = $username;
3253
$profile_data->{repo}{id} = $id;
3257
sub is_repo_profile($) {
3258
my $profile_data = shift;
3260
return $profile_data->{repo}{url} &&
3261
$profile_data->{repo}{user} &&
3262
$profile_data->{repo}{id};
3266
sub get_repo_user_pass() {
3270
$user = $repo_cfg->{repository}{user};
3271
$pass = $repo_cfg->{repository}{pass};
3274
unless ($user && $pass) {
3275
($user, $pass) = UI_repo_signup();
3278
return ($user, $pass);
3282
sub get_preferred_user ($) {
3283
my $repo_url = shift;
3284
return $cfg->{repository}{preferred_user} || "NOVELL";
3288
sub repo_is_enabled () {
3290
if ($cfg->{repository}{url} &&
3292
$repo_cfg->{repository}{enabled} &&
3293
$repo_cfg->{repository}{enabled} eq "yes") {
3300
sub update_repo_profile($) {
3301
my $profile = shift;
3303
return undef if ( not is_repo_profile($profile) );
3304
my $distro = $cfg->{repository}{distro};
3305
my $url = $profile->{repo}{url};
3306
my $user = $profile->{repo}{user};
3307
my $id = $profile->{repo}{id};
3309
UI_BusyStart( gettext("Connecting to repository.....") );
3310
my ($status_ok,$res) = fetch_newer_profile( $url,
3317
if ( ! $status_ok ) {
3320
gettext("WARNING: Profile update check failed\nError Detail:\n%s"),
3321
defined $res?$res:gettext("UNKNOWN ERROR"));
3322
UI_Important($errmsg);
3328
sub UI_ask_mode_toggles ($$$) {
3329
my ($audit_toggle, $owner_toggle, $oldmode) = @_;
3331
$q->{headers} = [ ];
3332
# "Repository", $cfg->{repository}{url},
3334
$q->{explanation} = gettext( "Change mode modifiers");
3336
if ($audit_toggle) {
3337
$q->{functions} = [ "CMD_AUDIT_OFF" ];
3339
$q->{functions} = [ "CMD_AUDIT_NEW" ];
3340
push @{$q->{functions}}, "CMD_AUDIT_FULL" if ($oldmode);
3343
if ($owner_toggle) {
3344
push @{$q->{functions}}, "CMD_USER_OFF";
3346
push @{$q->{functions}}, "CMD_USER_ON";
3348
push @{$q->{functions}}, "CMD_CONTINUE";
3352
$cmd = UI_PromptUser($q);
3353
} until $cmd =~ /^CMD_(AUDIT_OFF|AUDIT_NEW|AUDIT_FULL|USER_ON|USER_OFF|CONTINUE)/;
3355
if ($cmd eq "CMD_AUDIT_OFF") {
3357
} elsif ($cmd eq "CMD_AUDIT_NEW") {
3359
} elsif ($cmd eq "CMD_AUDIT_FULL") {
3361
} elsif ($cmd eq "CMD_USER_ON") {
3363
} elsif ($cmd eq "CMD_USER_OFF") {
3366
# $owner_toggle++ if (!$oldmode && $owner_toggle == 2);
3367
# $owner_toggle = 0 if ($owner_toggle > 3);
3369
return ($audit_toggle, $owner_toggle);
3372
sub ask_the_questions() {
3373
my $found; # do the magic foo-foo
3374
for my $sdmode (sort keys %log) {
3376
# let them know what sort of changes we're about to list...
3377
if ($sdmode eq "PERMITTING") {
3378
UI_Info(gettext("Complain-mode changes:"));
3379
} elsif ($sdmode eq "REJECTING") {
3380
UI_Info(gettext("Enforce-mode changes:"));
3383
# if we're not permitting and not rejecting, something's broken.
3384
# most likely the code we're using to build the hash tree of log
3385
# entries - this should never ever happen
3386
fatal_error(sprintf(gettext('Invalid mode found: %s'), $sdmode));
3389
for my $profile (sort keys %{ $log{$sdmode} }) {
3390
my $p = update_repo_profile($sd{$profile}{$profile});
3391
UI_SelectUpdatedRepoProfile($profile, $p) if ( $p );
3395
# this sorts the list of hats, but makes sure that the containing
3396
# profile shows up in the list first to keep the question order
3399
grep { $_ ne $profile } keys %{ $log{$sdmode}{$profile} };
3400
unshift @hats, $profile
3401
if defined $log{$sdmode}{$profile}{$profile};
3403
for my $hat (@hats) {
3405
# step through all the capabilities first...
3406
for my $capability (sort keys %{ $log{$sdmode}{$profile}{$hat}{capability} }) {
3408
# we don't care about it if we've already added it to the
3410
next if profile_known_capability($sd{$profile}{$hat},
3413
my $severity = $sevdb->rank(uc("cap_$capability"));
3415
my $defaultoption = 1;
3418
@newincludes = matchcapincludes($sd{$profile}{$hat},
3426
map { "#include <$_>" } sort(uniq(@newincludes));
3430
push @options, "capability $capability";
3431
$q->{options} = [@options];
3432
$q->{selected} = $defaultoption - 1;
3436
push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
3437
push @{ $q->{headers} }, gettext("Capability"), $capability;
3438
push @{ $q->{headers} }, gettext("Severity"), $severity;
3440
my $audit_toggle = 0;
3442
"CMD_ALLOW", "CMD_DENY", "CMD_AUDIT_NEW", "CMD_ABORT", "CMD_FINISHED"
3445
# complain-mode events default to allow - enforce defaults
3447
$q->{default} = ($sdmode eq "PERMITTING") ? "CMD_ALLOW" : "CMD_DENY";
3451
while ( not $done ) {
3452
# what did the grand exalted master tell us to do?
3453
my ($ans, $selected) = UI_PromptUser($q);
3455
if ($ans =~ /^CMD_AUDIT/) {
3456
$audit_toggle = !$audit_toggle;
3458
if ($audit_toggle) {
3460
"CMD_ALLOW", "CMD_DENY", "CMD_AUDIT_OFF", "CMD_ABORT", "CMD_FINISHED"
3465
"CMD_ALLOW", "CMD_DENY", "CMD_AUDIT_NEW", "CMD_ABORT", "CMD_FINISHED"
3469
push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
3470
push @{ $q->{headers} }, gettext("Capability"), $audit . $capability;
3471
push @{ $q->{headers} }, gettext("Severity"), $severity;
3473
} if ($ans eq "CMD_ALLOW") {
3475
# they picked (a)llow, so...
3477
my $selection = $options[$selected];
3480
$selection =~ m/^#include <(.+)>$/) {
3483
$deleted = delete_duplicates($sd{$profile}{$hat},
3486
$sd{$profile}{$hat}{include}{$inc} = 1;
3488
$changed{$profile} = 1;
3490
gettext('Adding #include <%s> to profile.'),
3493
gettext('Deleted %s previous matching profile entries.'),
3494
$deleted)) if $deleted;
3496
# stick the capability into the profile
3497
$sd{$profile}{$hat}{allow}{capability}{$capability}{set} = 1;
3498
$sd{$profile}{$hat}{allow}{capability}{$capability}{audit} = $audit_toggle;
3500
# mark this profile as changed
3501
$changed{$profile} = 1;
3503
# give a little feedback to the user
3504
UI_Info(sprintf(gettext('Adding capability %s to profile.'), $capability));
3505
} elsif ($ans eq "CMD_DENY") {
3506
$sd{$profile}{$hat}{deny}{capability}{$capability}{set} = 1;
3507
# mark this profile as changed
3508
$changed{$profile} = 1;
3509
UI_Info(sprintf(gettext('Denying capability %s to profile.'), $capability));
3517
# and then step through all of the path entries...
3518
for my $path (sort keys %{ $log{$sdmode}{$profile}{$hat}{path} }) {
3520
my $mode = $log{$sdmode}{$profile}{$hat}{path}{$path};
3522
# do original profile lookup once.
3525
my $allow_audit = 0;
3529
my ($fmode, $famode, $imode, $iamode, @fm, @im, $cm, $am, $cam, @m);
3530
($fmode, $famode, @fm) = rematchfrag($sd{$profile}{$hat}, 'allow', $path);
3531
$allow_mode |= $fmode if $fmode;
3532
$allow_audit |= $famode if $famode;
3533
($imode, $iamode, @im) = match_prof_incs_to_path($sd{$profile}{$hat}, 'allow', $path);
3534
$allow_mode |= $imode if $imode;
3535
$allow_audit |= $iamode if $iamode;
3537
($cm, $cam, @m) = rematchfrag($sd{$profile}{$hat}, 'deny', $path);
3538
$deny_mode |= $cm if $cm;
3539
$deny_audit |= $cam if $cam;
3540
($cm, $cam, @m) = match_prof_incs_to_path($sd{$profile}{$hat}, 'deny', $path);
3541
$deny_mode |= $cm if $cm;
3542
$deny_audit |= $cam if $cam;
3544
if ($deny_mode & $AA_MAY_EXEC) {
3545
$deny_mode |= $ALL_AA_EXEC_TYPE;
3548
# mask off the modes that have been denied
3549
$mode &= ~$deny_mode;
3550
$allow_mode &= ~$deny_mode;
3552
# if we had an access(X_OK) request or some other kind of
3553
# event that generates a "PERMITTING x" syslog entry,
3554
# first check if it was already dealt with by a i/p/x
3555
# question due to a exec(). if not, ask about adding ix
3557
if ($mode & $AA_MAY_EXEC) {
3559
# get rid of the access() markers.
3560
$mode &= (~$ALL_AA_EXEC_TYPE);
3562
unless ($allow_mode & $allow_mode & $AA_MAY_EXEC) {
3563
$mode |= str_to_mode("ix");
3567
# if we had an mmap(PROT_EXEC) request, first check if we
3568
# already have added an ix rule to the profile
3569
if ($mode & $AA_EXEC_MMAP) {
3570
# ix implies m. don't ask if they want to add an "m"
3571
# rule when we already have a matching ix rule.
3572
if ($allow_mode && contains($allow_mode, "ix")) {
3573
$mode &= (~$AA_EXEC_MMAP);
3589
unless ($allow_mode && mode_contains($allow_mode, $mode)) {
3591
my $defaultoption = 1;
3594
# check the path against the available set of include
3598
for my $incname (keys %include) {
3601
# don't suggest it if we're already including it,
3603
next if $sd{$profile}{$hat}{$incname};
3605
# only match includes that can be suggested to
3607
if ($cfg->{settings}{custom_includes}) {
3608
for my $incm (split(/\s+/,
3609
$cfg->{settings}{custom_includes})
3611
$includevalid = 1 if $incname =~ /$incm/;
3614
$includevalid = 1 if $incname =~ /abstractions/;
3615
next if ($includevalid == 0);
3617
($cm, $am, @m) = match_include_to_path($incname, 'allow', $path);
3618
if ($cm && mode_contains($cm, $mode)) {
3619
#make sure it doesn't deny $mode
3620
my $dm = match_include_to_path($incname, 'deny', $path);
3621
unless (($mode & $dm) || (grep { $_ eq "/**" } @m)) {
3622
push @newincludes, $incname;
3628
# did any match? add them to the option list...
3631
map { "#include <$_>" }
3632
sort(uniq(@newincludes));
3635
# include the literal path in the option list...
3636
push @options, $path;
3638
# match the current path against the globbing list in
3640
my @globs = globcommon($path);
3642
push @matches, @globs;
3645
# suggest any matching globs the user manually entered
3646
for my $userglob (@userglobs) {
3647
push @matches, $userglob
3648
if matchliteral($userglob, $path);
3651
# we'll take the cheesy way and order the suggested
3652
# globbing list by length, which is usually right,
3653
# but not always always
3655
sort { length($b) <=> length($a) }
3656
grep { $_ ne $path }
3658
$defaultoption = $#options + 1;
3660
my $severity = $sevdb->rank($path, mode_to_str($mode));
3662
my $audit_toggle = 0;
3663
my $owner_toggle = $cfg->{settings}{default_owner_prompt};
3669
push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
3670
push @{ $q->{headers} }, gettext("Path"), $path;
3672
# merge in any previous modes from this run
3675
#print "mode: " . print_mode($mode) . " allow: " . print_mode($allow_mode) . "\n";
3676
$mode |= $allow_mode;
3679
if ($owner_toggle == 0) {
3680
$prompt_mode = flatten_mode($mode);
3681
$tail = " " . gettext("(owner permissions off)");
3682
} elsif ($owner_toggle == 1) {
3683
$prompt_mode = $mode;
3685
} elsif ($owner_toggle == 2) {
3686
$prompt_mode = $allow_mode | owner_flatten_mode($mode & ~$allow_mode);
3687
$tail = " " . gettext("(force new perms to owner)");
3689
$prompt_mode = owner_flatten_mode($mode);
3690
$tail = " " . gettext("(force all rule perms to owner)");
3693
if ($audit_toggle == 1) {
3694
$str = mode_to_str_user($allow_mode);
3695
$str .= ", " if ($allow_mode);
3696
$str .= "audit " . mode_to_str_user($prompt_mode & ~$allow_mode) . $tail;
3697
} elsif ($audit_toggle == 2) {
3698
$str = "audit " . mode_to_str_user($prompt_mode) . $tail;
3700
$str = mode_to_str_user($prompt_mode) . $tail;
3702
push @{ $q->{headers} }, gettext("Old Mode"), mode_to_str_user($allow_mode);
3703
push @{ $q->{headers} }, gettext("New Mode"), $str;
3706
if ($audit_toggle) {
3711
if ($owner_toggle == 0) {
3712
$prompt_mode = flatten_mode($mode);
3713
$tail = " " . gettext("(owner permissions off)");
3714
} elsif ($owner_toggle == 1) {
3715
$prompt_mode = $mode;
3718
$prompt_mode = owner_flatten_mode($mode);
3719
$tail = " " . gettext("(force perms to owner)");
3721
$str .= mode_to_str_user($prompt_mode) . $tail;
3722
push @{ $q->{headers} }, gettext("Mode"), $str;
3724
push @{ $q->{headers} }, gettext("Severity"), $severity;
3726
$q->{options} = [@options];
3727
$q->{selected} = $defaultoption - 1;
3730
"CMD_ALLOW", "CMD_DENY", "CMD_GLOB", "CMD_GLOBEXT", "CMD_NEW",
3731
"CMD_ABORT", "CMD_FINISHED", "CMD_OTHER"
3735
($sdmode eq "PERMITTING")
3740
# if they just hit return, use the default answer
3741
my ($ans, $selected) = UI_PromptUser($q);
3743
if ($ans eq "CMD_OTHER") {
3745
($audit_toggle, $owner_toggle) = UI_ask_mode_toggles($audit_toggle, $owner_toggle, $allow_mode);
3746
} elsif ($ans eq "CMD_USER_TOGGLE") {
3748
$owner_toggle++ if (!$allow_mode && $owner_toggle == 2);
3749
$owner_toggle = 0 if ($owner_toggle > 3);
3750
} elsif ($ans eq "CMD_ALLOW") {
3751
$path = $options[$selected];
3753
if ($path =~ m/^#include <(.+)>$/) {
3757
$deleted = delete_duplicates($sd{$profile}{$hat},
3760
# record the new entry
3761
$sd{$profile}{$hat}{include}{$inc} = 1;
3763
$changed{$profile} = 1;
3764
UI_Info(sprintf(gettext('Adding #include <%s> to profile.'), $inc));
3765
UI_Info(sprintf(gettext('Deleted %s previous matching profile entries.'), $deleted)) if $deleted;
3767
if ($sd{$profile}{$hat}{allow}{path}{$path}{mode}) {
3768
$mode |= $sd{$profile}{$hat}{allow}{path}{$path}{mode};
3772
for my $entry (keys %{ $sd{$profile}{$hat}{allow}{path} }) {
3774
next if $path eq $entry;
3776
if (matchregexp($path, $entry)) {
3778
# regexp matches, add it's mode to
3779
# the list to check against
3780
if (mode_contains($mode,
3781
$sd{$profile}{$hat}{allow}{path}{$entry}{mode})) {
3782
delete $sd{$profile}{$hat}{allow}{path}{$entry};
3788
# record the new entry
3789
if ($owner_toggle == 0) {
3790
$mode = flatten_mode($mode);
3791
} elsif ($owner_toggle == 1) {
3793
} elsif ($owner_toggle == 2) {
3794
$mode = $allow_mode | owner_flatten_mode($mode & ~$allow_mode);
3795
} elsif ($owner_toggle == 3) {
3796
$mode = owner_flatten_mode($mode);
3798
$sd{$profile}{$hat}{allow}{path}{$path}{mode} |= $mode;
3799
my $tmpmode = ($audit_toggle == 1) ? $mode & ~$allow_mode : 0;
3800
$tmpmode = ($audit_toggle == 2) ? $mode : $tmpmode;
3801
$sd{$profile}{$hat}{allow}{path}{$path}{audit} |= $tmpmode;
3803
$changed{$profile} = 1;
3804
UI_Info(sprintf(gettext('Adding %s %s to profile.'), $path, mode_to_str_user($mode)));
3805
UI_Info(sprintf(gettext('Deleted %s previous matching profile entries.'), $deleted)) if $deleted;
3807
} elsif ($ans eq "CMD_DENY") {
3808
# record the new entry
3809
$sd{$profile}{$hat}{deny}{path}{$path}{mode} |= $mode & ~$allow_mode;
3810
$sd{$profile}{$hat}{deny}{path}{$path}{audit} |= 0;
3812
$changed{$profile} = 1;
3814
# go on to the next entry without saving this
3817
} elsif ($ans eq "CMD_NEW") {
3818
my $arg = $options[$selected];
3819
if ($arg !~ /^#include/) {
3820
$ans = UI_GetString(gettext("Enter new path: "), $arg);
3822
unless (matchliteral($ans, $path)) {
3823
my $ynprompt = gettext("The specified path does not match this log entry:") . "\n\n";
3824
$ynprompt .= " " . gettext("Log Entry") . ": $path\n";
3825
$ynprompt .= " " . gettext("Entered Path") . ": $ans\n\n";
3826
$ynprompt .= gettext("Do you really want to use this path?") . "\n";
3828
# we default to no if they just hit return...
3829
my $key = UI_YesNo($ynprompt, "n");
3831
next if $key eq "n";
3834
# save this one for later
3835
push @userglobs, $ans;
3837
push @options, $ans;
3838
$defaultoption = $#options + 1;
3841
} elsif ($ans eq "CMD_GLOB") {
3843
# do globbing if they don't have an include
3845
my $newpath = $options[$selected];
3847
unless ($newpath =~ /^#include/) {
3848
# is this entry directory specific
3849
if ( $newpath =~ m/\/$/ ) {
3850
# do we collapse to /* or /**?
3851
if ($newpath =~ m/\/\*{1,2}\/$/) {
3853
s/\/[^\/]+\/\*{1,2}\/$/\/\*\*\//;
3855
$newpath =~ s/\/[^\/]+\/$/\/\*\//;
3858
# do we collapse to /* or /**?
3859
if ($newpath =~ m/\/\*{1,2}$/) {
3860
$newpath =~ s/\/[^\/]+\/\*{1,2}$/\/\*\*/;
3862
$newpath =~ s/\/[^\/]+$/\/\*/;
3865
if ($newpath ne $selected) {
3866
push @options, $newpath;
3867
$defaultoption = $#options + 1;
3870
} elsif ($ans eq "CMD_GLOBEXT") {
3872
# do globbing if they don't have an include
3874
my $newpath = $options[$selected];
3875
unless ($newpath =~ /^#include/) {
3876
# do we collapse to /*.ext or /**.ext?
3877
if ($newpath =~ m/\/\*{1,2}\.[^\/]+$/) {
3878
$newpath =~ s/\/[^\/]+\/\*{1,2}(\.[^\/]+)$/\/\*\*$1/;
3880
$newpath =~ s/\/[^\/]+(\.[^\/]+)$/\/\*$1/;
3882
if ($newpath ne $selected) {
3883
push @options, $newpath;
3884
$defaultoption = $#options + 1;
3887
} elsif ($ans =~ /\d/) {
3888
$defaultoption = $ans;
3894
# and then step through all of the netdomain entries...
3895
for my $family (sort keys %{$log{$sdmode}
3900
# TODO - severity handling for net toggles
3901
#my $severity = $sevdb->rank();
3902
for my $sock_type (sort keys %{$log{$sdmode}
3908
# we don't care about it if we've already added it to the
3910
next if ( profile_known_network($sd{$profile}{$hat},
3913
my $defaultoption = 1;
3916
@newincludes = matchnetincludes($sd{$profile}{$hat},
3924
map { "#include <$_>" } sort(uniq(@newincludes));
3928
push @options, "network $family $sock_type";
3929
$q->{options} = [@options];
3930
$q->{selected} = $defaultoption - 1;
3934
push @{ $q->{headers} },
3936
combine_name($profile, $hat);
3937
push @{ $q->{headers} },
3938
gettext("Network Family"),
3940
push @{ $q->{headers} },
3941
gettext("Socket Type"),
3944
my $audit_toggle = 0;
3954
# complain-mode events default to allow - enforce defaults
3956
$q->{default} = ($sdmode eq "PERMITTING") ? "CMD_ALLOW" :
3961
# what did the grand exalted master tell us to do?
3963
while ( not $done ) {
3964
my ($ans, $selected) = UI_PromptUser($q);
3965
if ($ans =~ /^CMD_AUDIT/) {
3966
$audit_toggle = !$audit_toggle;
3967
my $audit = $audit_toggle ? "audit " : "";
3968
if ($audit_toggle) {
3986
push @{ $q->{headers} },
3988
combine_name($profile, $hat);
3989
push @{ $q->{headers} },
3990
gettext("Network Family"),
3992
push @{ $q->{headers} },
3993
gettext("Socket Type"),
3995
} elsif ($ans eq "CMD_ALLOW") {
3996
my $selection = $options[$selected];
3999
$selection =~ m/^#include <(.+)>$/) {
4002
$deleted = delete_duplicates($sd{$profile}{$hat},
4005
# record the new entry
4006
$sd{$profile}{$hat}{include}{$inc} = 1;
4008
$changed{$profile} = 1;
4011
gettext('Adding #include <%s> to profile.'),
4015
gettext('Deleted %s previous matching profile entries.'),
4016
$deleted)) if $deleted;
4019
# stick the whole rule into the profile
4026
{$sock_type} = $audit_toggle;
4036
# mark this profile as changed
4037
$changed{$profile} = 1;
4039
# give a little feedback to the user
4041
gettext('Adding network access %s %s to profile.'),
4047
} elsif ($ans eq "CMD_DENY") {
4049
# record the new entry
4058
$changed{$profile} = 1;
4060
gettext('Denying network access %s %s to profile.'),
4076
sub delete_net_duplicates($$) {
4077
my ($netrules, $incnetrules) = @_;
4079
if ( $incnetrules && $netrules ) {
4080
my $incnetglob = defined $incnetrules->{all};
4082
# See which if any profile rules are matched by the include and can be
4084
for my $fam ( keys %$netrules ) {
4085
if ( $incnetglob || (ref($incnetrules->{rule}{$fam}) ne "HASH" &&
4086
$incnetrules->{rule}{$fam} == 1)) { # include allows
4089
if ( ref($netrules->{rule}{$fam}) eq "HASH" ) {
4090
$deleted += ( keys %{$netrules->{rule}{$fam}} );
4094
delete $netrules->{rule}{$fam};
4095
} elsif ( ref($netrules->{rule}{$fam}) ne "HASH" &&
4096
$netrules->{rule}{$fam} == 1 ){
4097
next; # profile has all family
4099
for my $socket_type ( keys %{$netrules->{rule}{$fam}} ) {
4100
if ( defined $incnetrules->{$fam}{$socket_type} ) {
4101
delete $netrules->{$fam}{$socket_type};
4111
sub delete_cap_duplicates ($$) {
4112
my ($profilecaps, $inccaps) = @_;
4114
if ( $profilecaps && $inccaps ) {
4115
for my $capname ( keys %$profilecaps ) {
4116
if ( defined $inccaps->{$capname}{set} && $inccaps->{$capname}{set} == 1 ) {
4117
delete $profilecaps->{$capname};
4125
sub delete_path_duplicates ($$$) {
4126
my ($profile, $incname, $allow) = @_;
4129
for my $entry (keys %{ $profile->{$allow}{path} }) {
4130
next if $entry eq "#include <$incname>";
4131
my ($cm, $am, @m) = match_include_to_path($incname, $allow, $entry);
4133
&& mode_contains($cm, $profile->{$allow}{path}{$entry}{mode})
4134
&& mode_contains($am, $profile->{$allow}{path}{$entry}{audit}))
4136
delete $profile->{$allow}{path}{$entry};
4143
sub delete_duplicates (\%$) {
4144
my ( $profile, $incname ) = @_;
4147
# don't cross delete allow rules covered by denied rules as the coverage
4148
# may not be complete. ie. want to deny a subset of allow, allow a subset
4149
# of deny with different perms.
4152
$deleted += delete_net_duplicates($profile->{allow}{netdomain}, $include{$incname}{$incname}{allow}{netdomain});
4153
$deleted += delete_net_duplicates($profile->{deny}{netdomain}, $include{$incname}{$incname}{deny}{netdomain});
4156
$deleted += delete_cap_duplicates($profile->{allow}{capability},
4157
$include{$incname}{$incname}{allow}{capability});
4158
$deleted += delete_cap_duplicates($profile->{deny}{capability},
4159
$include{$incname}{$incname}{deny}{capability});
4162
$deleted += delete_path_duplicates($profile, $incname, 'allow');
4163
$deleted += delete_path_duplicates($profile, $incname, 'deny');
4168
sub matchnetinclude ($$$) {
4169
my ($incname, $family, $type) = @_;
4173
# scan the include fragments for this profile looking for matches
4174
my @includelist = ($incname);
4176
while (my $name = shift @includelist) {
4177
push @checked, $name;
4179
if netrules_access_check($include{$name}{$name}{allow}{netdomain}, $family, $type);
4180
# if this fragment includes others, check them too
4181
if (keys %{ $include{$name}{$name}{include} } &&
4182
(grep($name, @checked) == 0) ) {
4183
push @includelist, keys %{ $include{$name}{$name}{include} };
4189
sub matchcapincludes (\%$) {
4190
my ($profile, $cap) = @_;
4192
# check the path against the available set of include
4196
for my $incname (keys %include) {
4199
# don't suggest it if we're already including it,
4201
next if $profile->{include}{$incname};
4203
# only match includes that can be suggested to
4205
if ($cfg->{settings}{custom_includes}) {
4206
for my $incm (split(/\s+/,
4207
$cfg->{settings}{custom_includes})) {
4208
$includevalid = 1 if $incname =~ /$incm/;
4211
$includevalid = 1 if $incname =~ /abstractions/;
4212
next if ($includevalid == 0);
4214
push @newincludes, $incname
4215
if ( defined $include{$incname}{$incname}{allow}{capability}{$cap}{set} &&
4216
$include{$incname}{$incname}{allow}{capability}{$cap}{set} == 1 );
4218
return @newincludes;
4221
sub matchnetincludes (\%$$) {
4222
my ($profile, $family, $type) = @_;
4224
# check the path against the available set of include
4228
for my $incname (keys %include) {
4231
# don't suggest it if we're already including it,
4233
next if $profile->{include}{$incname};
4235
# only match includes that can be suggested to
4237
if ($cfg->{settings}{custom_includes}) {
4238
for my $incm (split(/\s+/, $cfg->{settings}{custom_includes})) {
4239
$includevalid = 1 if $incname =~ /$incm/;
4242
$includevalid = 1 if $incname =~ /abstractions/;
4243
next if ($includevalid == 0);
4245
push @newincludes, $incname
4246
if matchnetinclude($incname, $family, $type);
4248
return @newincludes;
4252
sub do_logprof_pass($) {
4253
my $logmark = shift || "";
4255
# zero out the state variables for this pass...
4260
%profilechanges = ( );
4268
UI_Info(sprintf(gettext('Reading log entries from %s.'), $filename));
4269
UI_Info(sprintf(gettext('Updating AppArmor profiles in %s.'), $profiledir));
4273
$sevdb = new Immunix::Severity("$confdir/severity.db", gettext("unknown
4277
# we need to be able to break all the way out of deep into subroutine calls
4278
# if they select "Finish" so we can take them back out to the genprof prompt
4280
unless ($repo_cfg || not defined $cfg->{repository}{url}) {
4281
$repo_cfg = read_config("repository.conf");
4282
unless ($repo_cfg->{repository}{enabled} &&
4283
($repo_cfg->{repository}{enabled} eq "yes" ||
4284
$repo_cfg->{repository}{enabled} eq "no")) {
4285
UI_ask_to_enable_repo();
4291
for my $root (@log) {
4292
handlechildren(undef, undef, $root);
4295
for my $pid (sort { $a <=> $b } keys %profilechanges) {
4296
setprocess($pid, $profilechanges{$pid});
4301
ask_the_questions();
4303
if ($UI_Mode eq "yast") {
4304
if (not $running_under_genprof) {
4306
my $w = { type => "wizard" };
4307
$w->{explanation} = gettext("The profile analyzer has completed processing the log files.\n\nAll updated profiles will be reloaded");
4308
$w->{functions} = [ "CMD_ABORT", "CMD_FINISHED" ];
4310
my $foo = GetDataFromYast();
4312
my $w = { type => "wizard" };
4313
$w->{explanation} = gettext("No unhandled AppArmor events were found in the system log.");
4314
$w->{functions} = [ "CMD_ABORT", "CMD_FINISHED" ];
4316
my $foo = GetDataFromYast();
4324
if ($@ =~ /FINISHING/) {
4333
if (repo_is_enabled()) {
4334
if ( (not defined $repo_cfg->{repository}{upload}) ||
4335
($repo_cfg->{repository}{upload} eq "later") ) {
4336
UI_ask_to_upload_profiles();
4338
if ($repo_cfg->{repository}{upload} eq "yes") {
4344
# if they hit "Finish" we need to tell the caller that so we can exit
4345
# all the way instead of just going back to the genprof prompt
4346
return $finishing ? "FINISHED" : "NORMAL";
4349
sub save_profiles() {
4350
# make sure the profile changes we've made are saved to disk...
4351
my @changed = sort keys %changed;
4353
# first make sure that profiles in %changed are active (or actual profiles
4354
# in %sd) - this is to handle the sloppiness of setting profiles as changed
4355
# when they are parsed in the case of legacy hat code that we want to write
4356
# out in an updated format
4357
foreach my $profile_name ( keys %changed ) {
4358
if ( ! is_active_profile( $profile_name ) ) {
4359
delete $changed{ $profile_name };
4362
@changed = sort keys %changed;
4365
if ($UI_Mode eq "yast") {
4366
my (@selected_profiles, $title, $explanation, %profile_changes);
4367
foreach my $prof (@changed) {
4368
my $oldprofile = serialize_profile($original_sd{$prof}, $prof);
4369
my $newprofile = serialize_profile($sd{$prof}, $prof);
4371
$profile_changes{$prof} = get_profile_diff($oldprofile,
4374
$explanation = gettext("Select which profile changes you would like to save to the\nlocal profile set");
4375
$title = gettext("Local profile changes");
4378
type => "dialog-select-profiles",
4380
explanation => $explanation,
4381
default_select => "true",
4382
get_changelog => "false",
4383
profiles => \%profile_changes
4386
my ($ypath, $yarg) = GetDataFromYast();
4387
if ($yarg->{STATUS} eq "cancel") {
4390
my $selected_profiles_ref = $yarg->{PROFILES};
4391
for my $profile (@$selected_profiles_ref) {
4392
writeprofile_ui_feedback($profile);
4393
reload_base($profile);
4398
$q->{title} = "Changed Local Profiles";
4402
gettext( "The following local profiles were changed. Would you like to save them?");
4404
$q->{functions} = [ "CMD_SAVE_CHANGES",
4408
$q->{default} = "CMD_VIEW_CHANGES";
4410
$q->{options} = [@changed];
4413
my ($p, $ans, $arg);
4415
($ans, $arg) = UI_PromptUser($q);
4417
if ($ans eq "CMD_VIEW_CHANGES") {
4418
my $which = $changed[$arg];
4420
serialize_profile($original_sd{$which}, $which);
4421
my $newprofile = serialize_profile($sd{$which}, $which);
4422
display_changes($oldprofile, $newprofile);
4425
} until $ans =~ /^CMD_SAVE_CHANGES/;
4427
for my $profile (sort keys %changed) {
4428
writeprofile_ui_feedback($profile);
4429
reload_base($profile);
4438
if ( $ENV{PAGER} and (-x "/usr/bin/$ENV{PAGER}" ||
4439
-x "/usr/sbin/$ENV{PAGER}" )
4448
sub display_text($$) {
4449
my ($header, $body) = @_;
4450
my $pager = get_pager();
4451
if (open(PAGER, "| $pager")) {
4452
print PAGER "$header\n\n$body";
4457
sub get_profile_diff($$) {
4458
my ($oldprofile, $newprofile) = @_;
4459
my $oldtmp = new File::Temp(UNLINK => 0);
4460
print $oldtmp $oldprofile;
4463
my $newtmp = new File::Temp(UNLINK => 0);
4464
print $newtmp $newprofile;
4467
my $difftmp = new File::Temp(UNLINK => 0);
4469
system("diff -u $oldtmp $newtmp > $difftmp");
4470
while (<$difftmp>) {
4471
push(@diff, $_) unless (($_ =~ /^(---|\+\+\+)/) ||
4472
($_ =~ /^\@\@.*\@\@$/));
4477
return join("", @diff);
4480
sub display_changes($$) {
4481
my ($oldprofile, $newprofile) = @_;
4483
my $oldtmp = new File::Temp( UNLINK => 0 );
4484
print $oldtmp $oldprofile;
4487
my $newtmp = new File::Temp( UNLINK => 0 );
4488
print $newtmp $newprofile;
4491
my $difftmp = new File::Temp(UNLINK => 0);
4493
system("diff -u $oldtmp $newtmp > $difftmp");
4494
if ($UI_Mode eq "yast") {
4495
while (<$difftmp>) {
4496
push(@diff, $_) unless (($_ =~ /^(---|\+\+\+)/) ||
4497
($_ =~ /^\@\@.*\@\@$/));
4499
UI_LongMessage(gettext("Profile Changes"), join("", @diff));
4501
system("less $difftmp");
4509
sub setprocess ($$) {
4510
my ($pid, $profile) = @_;
4512
# don't do anything if the process exited already...
4513
return unless -e "/proc/$pid/attr/current";
4515
return unless open(CURR, "/proc/$pid/attr/current");
4516
my $current = <CURR>;
4517
return unless $current;
4521
# only change null profiles
4522
return unless $current =~ /null(-complain)*-profile/;
4524
return unless open(STAT, "/proc/$pid/stat");
4529
return unless $stat =~ /^\d+ \((\S+)\) /;
4532
open(CURR, ">/proc/$pid/attr/current") or return;
4533
print CURR "setprofile $profile";
4537
sub collapselog () {
4538
for my $sdmode (keys %prelog) {
4539
for my $profile (keys %{ $prelog{$sdmode} }) {
4540
for my $hat (keys %{ $prelog{$sdmode}{$profile} }) {
4541
for my $path (keys %{ $prelog{$sdmode}{$profile}{$hat}{path} }) {
4543
my $mode = $prelog{$sdmode}{$profile}{$hat}{path}{$path};
4545
# we want to ignore anything from the log that's already
4547
my $combinedmode = 0;
4549
# is it in the original profile?
4550
if ($sd{$profile}{$hat}{allow}{path}{$path}) {
4551
$combinedmode |= $sd{$profile}{$hat}{allow}{path}{$path}{mode};
4554
# does path match any regexps in original profile?
4555
$combinedmode |= rematchfrag($sd{$profile}{$hat}, 'allow', $path);
4557
# does path match anything pulled in by includes in
4559
$combinedmode |= match_prof_incs_to_path($sd{$profile}{$hat}, 'allow', $path);
4561
# if we found any matching entries, do the modes match?
4562
unless ($combinedmode && mode_contains($combinedmode, $mode)) {
4564
# merge in any previous modes from this run
4565
if ($log{$sdmode}{$profile}{$hat}{$path}) {
4566
$mode |= $log{$sdmode}{$profile}{$hat}{path}{$path};
4569
# record the new entry
4570
$log{$sdmode}{$profile}{$hat}{path}{$path} = $mode;
4574
for my $capability (keys %{ $prelog{$sdmode}{$profile}{$hat}{capability} }) {
4576
# if we don't already have this capability in the profile,
4578
unless ($sd{$profile}{$hat}{allow}{capability}{$capability}{set}) {
4579
$log{$sdmode}{$profile}{$hat}{capability}{$capability} = 1;
4583
# Network toggle handling
4584
my $ndref = $prelog{$sdmode}{$profile}{$hat}{netdomain};
4585
for my $family ( keys %{$ndref} ) {
4586
for my $sock_type ( keys %{$ndref->{$family}} ) {
4587
unless ( profile_known_network($sd{$profile}{$hat},
4588
$family, $sock_type)) {
4603
sub profilemode ($) {
4606
my $modifier = ($mode =~ m/[iupUP]/)[0];
4608
$mode =~ s/[iupUPx]//g;
4609
$mode .= $modifier . "x";
4616
sub commonprefix (@) { (join("\0", @_) =~ m/^([^\0]*)[^\0]*(\0\1[^\0]*)*$/)[0] }
4617
sub commonsuffix (@) { reverse(((reverse join("\0", @_)) =~ m/^([^\0]*)[^\0]*(\0\1[^\0]*)*$/)[0]); }
4621
my @result = sort grep { !$seen{$_}++ } @_;
4625
our $MODE_MAP_RE = "r|w|l|m|k|a|x|i|u|p|c|n|I|U|P|C|N";
4626
our $LOG_MODE_RE = "r|w|l|m|k|a|x|ix|ux|px|cx|nx|pix|cix|Ix|Ux|Px|PUx|Cx|Nx|Pix|Cix";
4627
our $PROFILE_MODE_RE = "r|w|l|m|k|a|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix";
4628
our $PROFILE_MODE_NT_RE = "r|w|l|m|k|a|x|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix";
4629
our $PROFILE_MODE_DENY_RE = "r|w|l|m|k|a|x";
4631
sub split_log_mode($) {
4636
if ($mode =~ /(.*?)::(.*)/) {
4638
$other = $2 if ($2);
4643
return ($user, $other);
4646
sub map_log_mode ($) {
4649
# $mode =~ s/(.*l.*)::.*/$1/ge;
4650
# $mode =~ s/.*::(.*l.*)/$1/ge;
4656
sub hide_log_mode($) {
4663
sub validate_log_mode ($) {
4666
return ($mode =~ /^($LOG_MODE_RE)+$/) ? 1 : 0;
4669
sub validate_profile_mode ($$$) {
4670
my ($mode, $allow, $nt_name) = @_;
4672
if ($allow eq 'deny') {
4673
return ($mode =~ /^($PROFILE_MODE_DENY_RE)+$/) ? 1 : 0;
4674
} elsif ($nt_name) {
4675
return ($mode =~ /^($PROFILE_MODE_NT_RE)+$/) ? 1 : 0;
4678
return ($mode =~ /^($PROFILE_MODE_RE)+$/) ? 1 : 0;
4681
# modes internally are stored as a bit Mask
4682
sub sub_str_to_mode($) {
4686
return 0 if (not $str);
4688
while ($str =~ s/(${MODE_MAP_RE})//) {
4690
#print "found mode $1\n";
4692
if ($tmp && $MODE_HASH{$tmp}) {
4693
$mode |= $MODE_HASH{$tmp};
4695
#print "found mode $tmp\n";
4699
#my $tmp = mode_to_str($mode);
4700
#print "parsed_mode $mode\n";
4704
sub print_mode ($) {
4707
my ($user, $other) = split_mode($mode);
4708
my $str = sub_mode_to_str($user) . "::" . sub_mode_to_str($other);
4713
sub str_to_mode ($) {
4716
return 0 if (not $str);
4718
my ($user, $other) = split_log_mode($str);
4720
#print "str: $str user: $user, other $other\n";
4721
# we only allow user or all
4722
$user = $other if (!$user);
4724
my $mode = sub_str_to_mode($user);
4725
$mode |= (sub_str_to_mode($other) << $AA_OTHER_SHIFT);
4727
#print "user: $user " .sub_str_to_mode($user) . " other: $other " . (sub_str_to_mode($other) << $AA_OTHER_SHIFT) . " mode = $mode\n";
4732
sub log_str_to_mode($$$) {
4733
my ($profile, $str, $nt_name) = @_;
4735
my $mode = str_to_mode($str);
4737
# this will cover both nx and nix
4738
if (contains($mode, "Nx")) {
4739
# need to transform to px, cx
4741
if ($nt_name =~ /(.+?)\/\/(.+?)/) {
4742
my ($lprofile, $lhat) = @_;
4744
if ($profile eq $profile) {
4745
if ($mode & ($AA_MAY_EXEC)) {
4746
$tmode = str_to_mode("Cx::");
4748
if ($mode & ($AA_MAY_EXEC << $AA_OTHER_SHIFT)) {
4749
$tmode |= str_to_mode("Cx");
4753
if ($mode & ($AA_MAY_EXEC)) {
4754
$tmode = str_to_mode("Px::");
4756
if ($mode & ($AA_MAY_EXEC << $AA_OTHER_SHIFT)) {
4757
$tmode |= str_to_mode("Px");
4761
$mode = ($mode & ~(str_to_mode("Nx")));
4765
return ($mode, $nt_name);
4768
sub split_mode ($) {
4771
my $user = $mode & $AA_USER_MASK;
4772
my $other = ($mode >> $AA_OTHER_SHIFT) & $AA_USER_MASK;
4774
return ($user, $other);
4777
sub is_user_mode ($) {
4780
my ($user, $other) = split_mode($mode);
4782
if ($user && !$other) {
4788
sub sub_mode_to_str($) {
4793
$mode &= (~$AA_MAY_APPEND) if ($mode & $AA_MAY_WRITE);
4794
$str .= "m" if ($mode & $AA_EXEC_MMAP);
4795
$str .= "r" if ($mode & $AA_MAY_READ);
4796
$str .= "w" if ($mode & $AA_MAY_WRITE);
4797
$str .= "a" if ($mode & $AA_MAY_APPEND);
4798
$str .= "l" if ($mode & $AA_MAY_LINK);
4799
$str .= "k" if ($mode & $AA_MAY_LOCK);
4800
if ($mode & $AA_EXEC_UNCONFINED) {
4801
if ($mode & $AA_EXEC_UNSAFE) {
4807
if ($mode & ($AA_EXEC_PROFILE | $AA_EXEC_NT)) {
4808
if ($mode & $AA_EXEC_UNSAFE) {
4814
if ($mode & $AA_EXEC_CHILD) {
4815
if ($mode & $AA_EXEC_UNSAFE) {
4821
$str .= "i" if ($mode & $AA_EXEC_INHERIT);
4822
$str .= "x" if ($mode & $AA_MAY_EXEC);
4827
sub flatten_mode ($) {
4830
return 0 if (!$mode);
4832
$mode = ($mode & $AA_USER_MASK) | (($mode >> $AA_OTHER_SHIFT) & $AA_USER_MASK);
4833
$mode |= ($mode << $AA_OTHER_SHIFT);
4836
sub mode_to_str ($) {
4838
$mode = flatten_mode($mode);
4839
return sub_mode_to_str($mode);
4842
sub owner_flatten_mode($) {
4844
$mode = flatten_mode($mode) & $AA_USER_MASK;
4848
sub mode_to_str_user ($) {
4851
my ($user, $other) = split_mode($mode);
4854
$user = 0 if (!$user);
4855
$other = 0 if (!$other);
4857
if ($user & ~$other) {
4858
# more user perms than other
4859
$str = sub_mode_to_str($other). " + " if ($other);
4860
$str .= "owner " . sub_mode_to_str($user & ~$other);
4861
} elsif (is_user_mode($mode)) {
4862
$str = "owner " . sub_mode_to_str($user);
4864
$str = sub_mode_to_str(flatten_mode($mode));
4869
sub mode_contains ($$) {
4870
my ($mode, $subset) = @_;
4873
if ($mode & $AA_MAY_WRITE) {
4874
$mode |= $AA_MAY_APPEND;
4876
if ($mode & ($AA_MAY_WRITE << $AA_OTHER_SHIFT)) {
4877
$mode |= ($AA_MAY_APPEND << $AA_OTHER_SHIFT);
4881
if ($mode & $AA_EXEC_INHERIT) {
4882
$mode |= $AA_EXEC_MMAP;
4884
if ($mode & ($AA_EXEC_INHERIT << $AA_OTHER_SHIFT)) {
4885
$mode |= ($AA_EXEC_MMAP << $AA_OTHER_SHIFT);
4888
return (($mode & $subset) == $subset);
4892
my ($mode, $str) = @_;
4894
return mode_contains($mode, str_to_mode($str));
4897
# isSkippableFile - return true if filename matches something that
4898
# should be skipped (rpm backup files, dotfiles, emacs backup files
4899
# Annoyingly, this needs to be kept in sync with the skipped files
4900
# in the apparmor initscript.
4901
sub isSkippableFile($) {
4904
return ($path =~ /(^|\/)\.[^\/]*$/
4905
|| $path =~ /\.rpm(save|new)$/
4906
|| $path =~ /\.dpkg-(old|new)$/
4907
|| $path =~ /\.swp$/
4911
# isSkippableDir - return true if directory matches something that
4912
# should be skipped (cache directory, symlink directories, etc.)
4913
sub isSkippableDir($) {
4916
return ($path eq "disable"
4918
|| $path eq "force-complain");
4921
sub checkIncludeSyntax($) {
4924
if (opendir(SDDIR, $profiledir)) {
4925
my @incdirs = grep { (!/^\./) && (-d "$profiledir/$_") } readdir(SDDIR);
4927
while (my $id = shift @incdirs) {
4928
next if isSkippableDir($id);
4929
if (opendir(SDDIR, "$profiledir/$id")) {
4930
for my $path (grep { !/^\./ } readdir(SDDIR)) {
4932
next if isSkippableFile($path);
4933
if (-f "$profiledir/$id/$path") {
4934
my $file = "$id/$path";
4935
$file =~ s/$profiledir\///;
4936
eval { loadinclude($file); };
4937
if ( defined $@ && $@ ne "" ) {
4940
} elsif (-d "$id/$path") {
4941
push @incdirs, "$id/$path";
4951
sub checkProfileSyntax ($) {
4954
# Check the syntax of profiles
4956
opendir(SDDIR, $profiledir)
4957
or fatal_error "Can't read AppArmor profiles in $profiledir.";
4958
for my $file (grep { -f "$profiledir/$_" } readdir(SDDIR)) {
4959
next if isSkippableFile($file);
4960
my $err = readprofile("$profiledir/$file", \&printMessageErrorHandler, 1);
4961
if (defined $err and $err ne "") {
4962
push @$errors, $err;
4969
sub printMessageErrorHandler ($) {
4970
my $message = shift;
4974
sub readprofiles () {
4975
opendir(SDDIR, $profiledir)
4976
or fatal_error "Can't read AppArmor profiles in $profiledir.";
4977
for my $file (grep { -f "$profiledir/$_" } readdir(SDDIR)) {
4978
next if isSkippableFile($file);
4979
readprofile("$profiledir/$file", \&fatal_error, 1);
4984
sub readinactiveprofiles () {
4985
return if ( ! -e $extraprofiledir );
4986
opendir(ESDDIR, $extraprofiledir) or
4987
fatal_error "Can't read AppArmor profiles in $extraprofiledir.";
4988
for my $file (grep { -f "$extraprofiledir/$_" } readdir(ESDDIR)) {
4989
next if $file =~ /\.rpm(save|new)|README$/;
4990
readprofile("$extraprofiledir/$file", \&fatal_error, 0);
4995
sub readprofile ($$$) {
4997
my $error_handler = shift;
4998
my $active_profile = shift;
4999
if (open(SDPROF, "$file")) {
5001
my $data = <SDPROF>;
5005
my $profile_data = parse_profile_data($data, $file, 0);
5006
if ($profile_data && $active_profile) {
5007
attach_profile_data(\%sd, $profile_data);
5008
attach_profile_data(\%original_sd, $profile_data);
5009
} elsif ( $profile_data ) {
5010
attach_profile_data(\%extras, $profile_data);
5014
# if there were errors loading the profile, call the error handler
5017
return &$error_handler($@);
5020
$DEBUGGING && debug "readprofile: can't read $file - skipping";
5024
sub attach_profile_data($$) {
5025
my ($profiles, $profile_data) = @_;
5027
# make deep copies of the profile data so that if we change one set of
5028
# profile data, we're not changing others because of sharing references
5029
for my $p ( keys %$profile_data) {
5030
$profiles->{$p} = dclone($profile_data->{$p});
5034
sub parse_profile_data($$$) {
5035
my ($data, $file, $do_include) = @_;
5038
my ($profile_data, $profile, $hat, $in_contained_hat, $repo_data,
5040
my $initial_comment = "";
5047
for (split(/\n/, $data)) {
5050
# we don't care about blank lines
5053
# start of a profile...
5054
if (m/^\s*(("??\/.+?"??)|(profile\s+("??.+?"??)))\s+((flags=)?\((.+)\)\s+)*\{\s*(#.*)?$/) {
5055
# if we run into the start of a profile while we're already in a
5056
# profile, something's wrong...
5058
unless (($profile eq $hat) and $4) {
5059
die "$profile profile in $file contains syntax errors.\n";
5063
# we hit the start of a profile, keep track of it...
5064
if ($profile && ($profile eq $hat) && $4) {
5067
$in_contained_hat = 1;
5068
$profile_data->{$profile}{$hat}{profile} = 1;
5070
$profile = $2 || $4;
5071
# hat is same as profile name if we're not in a hat
5072
($profile, $hat) = split /\/\//, $profile;
5073
$in_contained_hat = 0;
5075
$profile_data->{$profile}{$hat}{external} = 1;
5083
# deal with whitespace in profile and hat names.
5084
$profile = strip_quotes($profile);
5085
$hat = strip_quotes($hat) if $hat;
5087
# save off the name and filename
5088
$profile_data->{$profile}{$hat}{name} = $profile;
5089
$profile_data->{$profile}{$hat}{filename} = $file;
5090
$filelist{$file}{profiles}{$profile}{$hat} = 1;
5092
# keep track of profile flags
5093
$profile_data->{$profile}{$hat}{flags} = $flags;
5095
$profile_data->{$profile}{$hat}{allow}{netdomain} = { };
5096
$profile_data->{$profile}{$hat}{allow}{path} = { };
5098
# store off initial comment if they have one
5099
$profile_data->{$profile}{$hat}{initial_comment} = $initial_comment
5100
if $initial_comment;
5101
$initial_comment = "";
5104
$profile_data->{$profile}{$profile}{repo}{url} = $repo_data->{url};
5105
$profile_data->{$profile}{$profile}{repo}{user} = $repo_data->{user};
5106
$profile_data->{$profile}{$profile}{repo}{id} = $repo_data->{id};
5110
} elsif (m/^\s*\}\s*(#.*)?$/) { # end of a profile...
5112
# if we hit the end of a profile when we're not in one, something's
5115
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5118
if ($in_contained_hat) {
5120
$in_contained_hat = 0;
5122
push @parsed_profiles, $profile;
5123
# mark that we're outside of a profile now...
5127
$initial_comment = "";
5129
} elsif (m/^\s*(audit\s+)?(deny\s+)?capability\s+(\S+)\s*,\s*(#.*)?$/) { # capability entry
5131
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5134
my $audit = $1 ? 1 : 0;
5135
my $allow = $2 ? 'deny' : 'allow';
5136
$allow = 'deny' if ($2);
5137
my $capability = $3;
5138
$profile_data->{$profile}{$hat}{$allow}{capability}{$capability}{set} = 1;
5139
$profile_data->{$profile}{$hat}{$allow}{capability}{$capability}{audit} = $audit;
5140
} elsif (m/^\s*set capability\s+(\S+)\s*,\s*(#.*)?$/) { # capability entry
5142
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5145
my $capability = $1;
5146
$profile_data->{$profile}{$hat}{set_capability}{$capability} = 1;
5148
} elsif (m/^\s*(audit\s+)?(deny\s+)?link\s+(((subset)|(<=))\s+)?([\"\@\/].*?"??)\s+->\s*([\"\@\/].*?"??)\s*,\s*(#.*)?$/) { # for now just keep link
5150
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5152
my $audit = $1 ? 1 : 0;
5153
my $allow = $2 ? 'deny' : 'allow';
5156
my $link = strip_quotes($7);
5157
my $value = strip_quotes($8);
5158
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{to} = $value;
5159
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{mode} |= $AA_MAY_LINK;
5161
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{mode} |= $AA_LINK_SUBSET;
5164
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{audit} |= $AA_LINK_SUBSET;
5166
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{audit} |= 0;
5169
} elsif (m/^\s*change_profile\s+->\s*("??.+?"??),(#.*)?$/) { # for now just keep change_profile
5171
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5173
my $cp = strip_quotes($1);
5175
$profile_data->{$profile}{$hat}{change_profile}{$cp} = 1;
5176
} elsif (m/^\s*alias\s+("??.+?"??)\s+->\s*("??.+?"??)\s*,(#.*)?$/) { # never do anything with aliases just keep them
5177
my $from = strip_quotes($1);
5178
my $to = strip_quotes($2);
5181
$profile_data->{$profile}{$hat}{alias}{$from} = $to;
5183
unless (exists $filelist{$file}) {
5184
$filelist{$file} = { };
5186
$filelist{$file}{alias}{$from} = $to;
5189
} elsif (m/^\s*set\s+rlimit\s+(.+)\s+<=\s*(.+)\s*,(#.*)?$/) { # never do anything with rlimits just keep them
5191
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5196
$profile_data->{$profile}{$hat}{rlimit}{$from} = $to;
5198
} elsif (/^\s*(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*=\s*(true|false)\s*,?\s*(#.*)?$/i) { # boolean definition
5200
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5205
$profile_data->{$profile}{$hat}{lvar}{$bool_var} = $value;
5206
} elsif (/^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\+?=\s*(.+?)\s*,?\s*(#.*)?$/) { # variable additions both += and = doesn't mater
5207
my $list_var = strip_quotes($1);
5208
my $value = strip_quotes($2);
5211
unless (exists $profile_data->{$profile}{$hat}{lvar}) {
5212
# create lval hash by sticking an empty list into list_var
5214
$profile_data->{$profile}{$hat}{lvar}{$list_var} = \@empty;
5217
store_list_var($profile_data->{$profile}{$hat}{lvar}, $list_var, $value);
5219
unless (exists $filelist{$file}{lvar}) {
5220
# create lval hash by sticking an empty list into list_var
5222
$filelist{$file}{lvar}{$list_var} = \@empty;
5225
store_list_var($filelist{$file}{lvar}, $list_var, $value);
5227
} elsif (m/^\s*if\s+(not\s+)?(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*\{\s*(#.*)?$/) { # conditional -- boolean
5228
} elsif (m/^\s*if\s+(not\s+)?defined\s+(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\{\s*(#.*)?$/) { # conditional -- variable defined
5229
} elsif (m/^\s*if\s+(not\s+)?defined\s+(\$\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\{\s*(#.*)?$/) { # conditional -- boolean defined
5230
} elsif (m/^\s*(audit\s+)?(deny\s+)?(owner\s+)?([\"\@\/].*?)\s+(\S+)(\s+->\s*(.*?))?\s*,\s*(#.*)?$/) { # path entry
5232
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5235
my $audit = $1 ? 1 : 0;
5236
my $allow = $2 ? 'deny' : 'allow';
5237
my $user = $3 ? 1 : 0;
5238
my ($path, $mode, $nt_name) = ($4, $5, $7);
5240
# strip off any trailing spaces.
5242
$nt_name =~ s/\s+$// if $nt_name;
5244
$path = strip_quotes($path);
5245
$nt_name = strip_quotes($nt_name) if $nt_name;
5247
# make sure they don't have broken regexps in the profile
5248
my $p_re = convert_regexp($path);
5249
eval { "foo" =~ m/^$p_re$/; };
5251
die sprintf(gettext('Profile %s contains invalid regexp %s.'),
5252
$file, $path) . "\n";
5255
if (!validate_profile_mode($mode, $allow, $nt_name)) {
5256
fatal_error(sprintf(gettext('Profile %s contains invalid mode %s.'), $file, $mode));
5261
$tmpmode = str_to_mode("${mode}::");
5263
$tmpmode = str_to_mode($mode);
5266
$profile_data->{$profile}{$hat}{$allow}{path}{$path}{mode} |= $tmpmode;
5267
$profile_data->{$profile}{$hat}{$allow}{path}{$path}{to} = $nt_name if $nt_name;
5269
$profile_data->{$profile}{$hat}{$allow}{path}{$path}{audit} |= $tmpmode;
5271
$profile_data->{$profile}{$hat}{$allow}{path}{$path}{audit} |= 0;
5273
} elsif (m/^\s*#include <(.+)>\s*$/) { # include stuff
5277
$profile_data->{$profile}{$hat}{include}{$include} = 1;
5279
unless (exists $filelist{$file}) {
5280
$filelist{$file} = { };
5282
$filelist{$file}{include}{$include} = 1;
5286
if (-d "$profiledir/$include") {
5287
if (opendir(SDINCDIR, "$profiledir/$include")) {
5288
for my $path (readdir(SDINCDIR)) {
5290
next if isSkippableFile($path);
5291
if (-f "$profiledir/$include/$path") {
5292
my $file = "$include/$path";
5293
$file =~ s/$profiledir\///;
5294
my $ret = eval { loadinclude($file); };
5296
return $ret if ( $ret != 0 );
5302
# try to load the include...
5303
my $ret = eval { loadinclude($include); };
5304
# propagate errors up the chain
5306
return $ret if ( $ret != 0 );
5308
} elsif (/^\s*(audit\s+)?(deny\s+)?network(.*)/) {
5310
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5312
my $audit = $1 ? 1 : 0;
5313
my $allow = $2 ? 'deny' : 'allow';
5316
unless ($profile_data->{$profile}{$hat}{$allow}{netdomain}{rule}) {
5317
$profile_data->{$profile}{$hat}{$allow}{netdomain}{rule} = { };
5320
if ($network =~ /\s+(\S+)\s+(\S+)\s*,\s*(#.*)?$/ ) {
5323
$profile_data->{$profile}{$hat}{$allow}{netdomain}{rule}{$fam}{$type} = 1;
5324
$profile_data->{$profile}{$hat}{$allow}{netdomain}{audit}{$fam}{$type} = $audit;
5325
} elsif ( $network =~ /\s+(\S+)\s*,\s*(#.*)?$/ ) {
5327
$profile_data->{$profile}{$hat}{$allow}{netdomain}{rule}{$fam} = 1;
5328
$profile_data->{$profile}{$hat}{$allow}{netdomain}{audit}{$fam} = $audit;
5330
$profile_data->{$profile}{$hat}{$allow}{netdomain}{rule}{all} = 1;
5331
$profile_data->{$profile}{$hat}{$allow}{netdomain}{audit}{all} = 1;
5333
} elsif (/^\s*(tcp_connect|tcp_accept|udp_send|udp_receive)/) {
5334
# just ignore and drop old style network
5335
# die sprintf(gettext('%s contains old style network rules.'), $file) . "\n";
5337
} elsif (m/^\s*\^(\"??.+?\"??)\s*,\s*(#.*)?$/) {
5339
die "$file contains syntax errors.";
5341
# change_hat declaration - needed to change_hat to an external
5344
$hat = $1 if $hat =~ /^"(.+)"$/;
5346
#store we have a declaration if the hat hasn't been seen
5347
$profile_data->{$profile}{$hat}{'declared'} = 1
5348
unless exists($profile_data->{$profile}{$hat}{declared});
5350
} elsif (m/^\s*\^(\"??.+?\"??)\s+((flags=)?\((.+)\)\s+)*\{\s*(#.*)?$/) {
5351
# start of embedded hat syntax hat definition
5352
# read in and mark as changed so that will be written out in the new
5355
# if we hit the start of a contained hat when we're not in a profile
5356
# something is wrong...
5358
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
5361
$in_contained_hat = 1;
5363
# we hit the start of a hat inside the current profile
5368
$hat = $1 if $hat =~ /^"(.+)"$/;
5370
# keep track of profile flags
5371
$profile_data->{$profile}{$hat}{flags} = $flags;
5372
# we have seen more than a declaration so clear it
5373
$profile_data->{$profile}{$hat}{'declared'} = 0;
5374
$profile_data->{$profile}{$hat}{allow}{path} = { };
5375
$profile_data->{$profile}{$hat}{allow}{netdomain} = { };
5377
# store off initial comment if they have one
5378
$profile_data->{$profile}{$hat}{initial_comment} = $initial_comment
5379
if $initial_comment;
5380
$initial_comment = "";
5381
#don't mark profile as changed just because it has an embedded
5383
#$changed{$profile} = 1;
5385
$filelist{$file}{profiles}{$profile}{$hat} = 1;
5387
} elsif (/^\s*\#/) {
5388
# we only currently handle initial comments
5390
# ignore vim syntax highlighting lines
5391
next if /^\s*\# vim:syntax/;
5392
# ignore Last Modified: lines
5393
next if /^\s*\# Last Modified:/;
5394
if (/^\s*\# REPOSITORY: (\S+) (\S+) (\S+)$/) {
5395
$repo_data = { url => $1, user => $2, id => $3 };
5396
} elsif (/^\s*\# REPOSITORY: NEVERSUBMIT$/) {
5397
$repo_data = { neversubmit => 1 };
5399
$initial_comment .= "$_\n";
5403
# we hit something we don't understand in a profile...
5404
die sprintf(gettext('%s contains syntax errors. Line [%s]'), $file, $_) . "\n";
5409
# Cleanup : add required hats if not present in the
5412
if (not $do_include) {
5413
for my $hatglob (keys %{$cfg->{required_hats}}) {
5414
for my $parsed_profile ( sort @parsed_profiles ) {
5415
if ($parsed_profile =~ /$hatglob/) {
5416
for my $hat (split(/\s+/, $cfg->{required_hats}{$hatglob})) {
5417
unless ($profile_data->{$parsed_profile}{$hat}) {
5418
$profile_data->{$parsed_profile}{$hat} = { };
5425
} # if we're still in a profile when we hit the end of the file, it's bad
5426
if ($profile and not $do_include) {
5427
die "Reached the end of $file while we were still inside the $profile profile.\n";
5430
return $profile_data;
5433
sub eliminate_duplicates(@) {
5436
my %set = map { $_ => 1 } @_;
5442
sub separate_vars($) {
5446
# while ($vs =~ /\s*(((\"([^\"]|\\\"))+?\")|\S*)\s*(.*)$/) {
5447
while ($vs =~ /\s*((\".+?\")|([^\"]\S+))\s*(.*)$/) {
5449
push @data, strip_quotes($tmp);
5456
sub is_active_profile ($) {
5458
if ( $sd{$pname} ) {
5465
sub store_list_var (\%$$) {
5466
my ($vars, $list_var, $value) = @_;
5468
my @vlist = (separate_vars($value));
5470
# if (exists $profile_data->{$profile}{$hat}{lvar}{$list_var}) {
5471
# @vlist = (@vlist, @{$profile_data->{$profile}{$hat}{lvar}{$list_var}});
5474
# @vlist = eliminate_duplicates(@vlist);
5475
# $profile_data->{$profile}{$hat}{lvar}{$list_var} = \@vlist;
5477
if (exists $vars->{$list_var}) {
5478
@vlist = (@vlist, @{$vars->{$list_var}});
5481
@vlist = eliminate_duplicates(@vlist);
5482
$vars->{$list_var} = \@vlist;
5487
sub strip_quotes ($) {
5489
$data = $1 if $data =~ /^\"(.*)\"$/;
5493
sub quote_if_needed ($) {
5495
$data = "\"$data\"" if $data =~ /\s/;
5501
my $dangerous = shift;
5503
$dangerous = strip_quotes($dangerous);
5505
$dangerous =~ s/((?<!\\))"/$1\\"/g;
5506
if ($dangerous =~ m/(\s|^$|")/) {
5507
$dangerous = "\"$dangerous\"";
5513
sub writeheader ($$$$$) {
5514
my ($profile_data, $depth, $name, $embedded_hat, $write_flags) = @_;
5516
my $pre = ' ' x $depth;
5518
# deal with whitespace in profile names...
5519
$name = quote_if_needed($name);
5521
$name = "profile $name" if ((!$embedded_hat && $name =~ /^[^\/]|^"[^\/]/)
5522
|| ($embedded_hat && $name =~/^[^^]/));
5524
#push @data, "#include <tunables/global>" unless ( $is_hat );
5525
if ($write_flags and $profile_data->{flags}) {
5526
push @data, "${pre}$name flags=($profile_data->{flags}) {";
5528
push @data, "${pre}$name {";
5536
return quote_if_needed($value);
5539
sub write_single ($$$$$$) {
5540
my ($profile_data, $depth, $allow, $name, $prefix, $tail) = @_;
5545
$ref = $profile_data->{$allow};
5546
if ($allow eq 'deny') {
5552
$ref = $profile_data;
5556
my $pre = " " x $depth;
5560
if (exists $ref->{$name}) {
5561
for my $key (sort keys %{$ref->{$name}}) {
5562
my $qkey = quote_if_needed($key);
5563
push @data, "${pre}${allow}${prefix}${qkey}${tail}";
5565
push @data, "" if keys %{$ref->{$name}};
5571
sub write_pair ($$$$$$$$) {
5572
my ($profile_data, $depth, $allow, $name, $prefix, $sep, $tail, $fn) = @_;
5577
$ref = $profile_data->{$allow};
5578
if ($allow eq 'deny') {
5584
$ref = $profile_data;
5588
my $pre = " " x $depth;
5591
if (exists $ref->{$name}) {
5592
for my $key (sort keys %{$ref->{$name}}) {
5593
my $value = &{$fn}($ref->{$name}{$key});
5594
push @data, "${pre}${allow}${prefix}${key}${sep}${value}${tail}";
5596
push @data, "" if keys %{$ref->{$name}};
5602
sub writeincludes ($$) {
5603
my ($prof_data, $depth) = @_;
5605
return write_single($prof_data, $depth,'', 'include', "#include <", ">");
5608
sub writechange_profile ($$) {
5609
my ($prof_data, $depth) = @_;
5611
return write_single($prof_data, $depth, '', 'change_profile', "change_profile -> ", ",");
5614
sub writealiases ($$) {
5615
my ($prof_data, $depth) = @_;
5617
return write_pair($prof_data, $depth, '', 'alias', "alias ", " -> ", ",", \&qin_trans);
5620
sub writerlimits ($$) {
5621
my ($prof_data, $depth) = @_;
5623
return write_pair($prof_data, $depth, '', 'rlimit', "set rlimit ", " <= ", ",", \&qin_trans);
5626
# take a list references and process it
5627
sub var_transform($) {
5632
foreach my $value (@in) {
5633
push @data, quote_if_needed($value);
5636
return join " ", @data;
5639
sub writelistvars ($$) {
5640
my ($prof_data, $depth) = @_;
5642
return write_pair($prof_data, $depth, '', 'lvar', "", " = ", "", \&var_transform);
5645
sub writecap_rules ($$$) {
5646
my ($profile_data, $depth, $allow) = @_;
5648
my $allowstr = $allow eq 'deny' ? 'deny ' : '';
5649
my $pre = " " x $depth;
5652
if (exists $profile_data->{$allow}{capability}) {
5653
for my $cap (sort keys %{$profile_data->{$allow}{capability}}) {
5654
my $audit = ($profile_data->{$allow}{capability}{$cap}{audit}) ? 'audit ' : '';
5655
if ($profile_data->{$allow}{capability}{$cap}{set}) {
5656
push @data, "${pre}${audit}${allowstr}capability ${cap},";
5665
sub writecapabilities ($$) {
5666
my ($prof_data, $depth) = @_;
5668
push @data, write_single($prof_data, $depth, '', 'set_capability', "set capability ", ",");
5669
push @data, writecap_rules($prof_data, $depth, 'deny');
5670
push @data, writecap_rules($prof_data, $depth, 'allow');
5674
sub writenet_rules ($$$) {
5675
my ($profile_data, $depth, $allow) = @_;
5677
my $allowstr = $allow eq 'deny' ? 'deny ' : '';
5679
my $pre = " " x $depth;
5683
# dump out the netdomain entries...
5684
if (exists $profile_data->{$allow}{netdomain}) {
5685
if ( $profile_data->{$allow}{netdomain}{rule} &&
5686
$profile_data->{$allow}{netdomain}{rule} eq 'all') {
5687
$audit = "audit " if $profile_data->{$allow}{netdomain}{audit}{all};
5688
push @data, "${pre}${audit}network,";
5690
for my $fam (sort keys %{$profile_data->{$allow}{netdomain}{rule}}) {
5691
if ( $profile_data->{$allow}{netdomain}{rule}{$fam} == 1 ) {
5692
$audit = "audit " if $profile_data->{$allow}{netdomain}{audit}{$fam};
5693
push @data, "${pre}${audit}${allowstr}network $fam,";
5696
(sort keys %{$profile_data->{$allow}{netdomain}{rule}{$fam}}) {
5697
$audit = "audit " if $profile_data->{$allow}{netdomain}{audit}{$fam}{$type};
5698
push @data, "${pre}${audit}${allowstr}network $fam $type,";
5703
push @data, "" if %{$profile_data->{$allow}{netdomain}};
5709
sub writenetdomain ($$) {
5710
my ($prof_data, $depth) = @_;
5713
push @data, writenet_rules($prof_data, $depth, 'deny');
5714
push @data, writenet_rules($prof_data, $depth, 'allow');
5719
sub writelink_rules ($$$) {
5720
my ($profile_data, $depth, $allow) = @_;
5722
my $allowstr = $allow eq 'deny' ? 'deny ' : '';
5723
my $pre = " " x $depth;
5726
if (exists $profile_data->{$allow}{link}) {
5727
for my $path (sort keys %{$profile_data->{$allow}{link}}) {
5728
my $to = $profile_data->{$allow}{link}{$path}{to};
5729
my $subset = ($profile_data->{$allow}{link}{$path}{mode} & $AA_LINK_SUBSET) ? 'subset ' : '';
5730
my $audit = ($profile_data->{$allow}{link}{$path}{audit}) ? 'audit ' : '';
5731
# deal with whitespace in path names
5732
$path = quote_if_needed($path);
5733
$to = quote_if_needed($to);
5734
push @data, "${pre}${audit}${allowstr}link ${subset}${path} -> ${to},";
5742
sub writelinks ($$) {
5743
my ($profile_data, $depth) = @_;
5746
push @data, writelink_rules($profile_data, $depth, 'deny');
5747
push @data, writelink_rules($profile_data, $depth, 'allow');
5752
sub writepath_rules ($$$) {
5753
my ($profile_data, $depth, $allow) = @_;
5755
my $allowstr = $allow eq 'deny' ? 'deny ' : '';
5756
my $pre = " " x $depth;
5759
if (exists $profile_data->{$allow}{path}) {
5760
for my $path (sort keys %{$profile_data->{$allow}{path}}) {
5761
my $mode = $profile_data->{$allow}{path}{$path}{mode};
5762
my $audit = $profile_data->{$allow}{path}{$path}{audit};
5764
$tail = " -> " . $profile_data->{$allow}{path}{$path}{to} if ($profile_data->{$allow}{path}{$path}{to});
5765
my ($user, $other) = split_mode($mode);
5766
my ($user_audit, $other_audit) = split_mode($audit);
5767
# determine whether the rule contains any owner only components
5769
while ($user || $other) {
5771
my ($tmpmode, $tmpaudit) = 0;
5772
if ($user & ~$other) {
5773
# user contains bits not set in other
5774
$ownerstr = "owner ";
5775
$tmpmode = $user & ~$other;
5776
$tmpaudit = $user_audit;
5778
# } elsif ($other & ~$user) {
5779
# $ownerstr = "other ";
5780
# $tmpmode = $other & ~$user;
5781
# $tmpaudit = $other_audit;
5782
# $other &= ~$tmpmode;
5784
if ($user_audit & ~$other_audit & $user) {
5785
$ownerstr = "owner ";
5786
$tmpaudit = $user_audit & ~$other_audit & $user;
5787
$tmpmode = $user & $tmpaudit;
5789
# } elsif ($other_audit & ~$user_audit & $other) {
5790
# $ownerstr = "other ";
5791
# $tmpaudit = $other_audit & ~$user_audit & $other;
5792
# $tmpmode = $other & $tmpaudit;
5793
# $other &= ~$tmpmode;
5795
# user == other && user_audit == other_audit
5797
#include exclusive other for now
5799
# $tmpaudit = $user_audit;
5800
$tmpmode = $user | $other;
5801
$tmpaudit = $user_audit | $other_audit;
5803
$other &= ~$tmpmode;
5807
if ($tmpmode & $tmpaudit) {
5808
my $modestr = mode_to_str($tmpmode & $tmpaudit);
5809
if ($path =~ /\s/) {
5810
push @data, "${pre}audit ${allowstr}${ownerstr}\"$path\" ${modestr}${tail},";
5812
push @data, "${pre}audit ${allowstr}${ownerstr}$path ${modestr}${tail},";
5814
$tmpmode &= ~$tmpaudit;
5817
my $modestr = mode_to_str($tmpmode);
5818
if ($path =~ /\s/) {
5819
push @data, "${pre}${allowstr}${ownerstr}\"$path\" ${modestr}${tail},";
5821
push @data, "${pre}${allowstr}${ownerstr}$path ${modestr}${tail},";
5833
sub writepaths ($$) {
5834
my ($prof_data, $depth) = @_;
5837
push @data, writepath_rules($prof_data, $depth, 'deny');
5838
push @data, writepath_rules($prof_data, $depth, 'allow');
5843
sub write_rules ($$) {
5844
my ($prof_data, $depth) = @_;
5847
push @data, writealiases($prof_data, $depth);
5848
push @data, writelistvars($prof_data, $depth);
5849
push @data, writeincludes($prof_data, $depth);
5850
push @data, writerlimits($prof_data, $depth);
5851
push @data, writecapabilities($prof_data, $depth);
5852
push @data, writenetdomain($prof_data, $depth);
5853
push @data, writelinks($prof_data, $depth);
5854
push @data, writepaths($prof_data, $depth);
5855
push @data, writechange_profile($prof_data, $depth);
5860
sub writepiece ($$$$$);
5861
sub writepiece ($$$$$) {
5862
my ($profile_data, $depth, $name, $nhat, $write_flags) = @_;
5864
my $pre = ' ' x $depth;
5868
if ($name eq $nhat) {
5871
$wname = "$name//$nhat";
5875
push @data, writeheader($profile_data->{$name}, $depth, $wname, 0, $write_flags);
5876
push @data, write_rules($profile_data->{$name}, $depth + 1);
5878
my $pre2 = ' ' x ($depth + 1);
5879
# write external hat declarations
5880
for my $hat (grep { $_ ne $name } sort keys %{$profile_data}) {
5881
if ($profile_data->{$hat}{declared}) {
5882
push @data, "${pre2}^$hat,";
5887
# write embedded hats
5888
for my $hat (grep { $_ ne $name } sort keys %{$profile_data}) {
5889
if ((not $profile_data->{$hat}{external}) and
5890
(not $profile_data->{$hat}{declared})) {
5892
if ($profile_data->{$hat}{profile}) {
5893
push @data, map { "$_" } writeheader($profile_data->{$hat},
5897
push @data, map { "$_" } writeheader($profile_data->{$hat},
5898
$depth + 1, "^$hat",
5901
push @data, map { "$_" } write_rules($profile_data->{$hat},
5903
push @data, "${pre2}}";
5906
push @data, "${pre}}";
5908
#write external hats
5909
for my $hat (grep { $_ ne $name } sort keys %{$profile_data}) {
5910
if (($name eq $nhat) and $profile_data->{$hat}{external}) {
5912
push @data, map { " $_" } writepiece($profile_data, $depth - 1,
5913
$name, $hat, $write_flags);
5921
sub serialize_profile($$$) {
5922
my ($profile_data, $name, $options) = @_;
5925
my $include_metadata = 0; # By default don't write out metadata
5926
my $include_flags = 1;
5927
if ( $options and ref($options) eq "HASH" ) {
5928
$include_metadata = 1 if ( defined $options->{METADATA} );
5929
$include_flags = 0 if ( defined $options->{NO_FLAGS} );
5932
if ($include_metadata) {
5933
# keep track of when the file was last updated
5934
$string .= "# Last Modified: " . localtime(time) . "\n";
5936
# print out repository metadata
5937
if ($profile_data->{$name}{repo} &&
5938
$profile_data->{$name}{repo}{url} &&
5939
$profile_data->{$name}{repo}{user} &&
5940
$profile_data->{$name}{repo}{id}) {
5941
my $repo = $profile_data->{$name}{repo};
5942
$string .= "# REPOSITORY: $repo->{url} $repo->{user} $repo->{id}\n";
5943
} elsif ($profile_data->{$name}{repo}{neversubmit}) {
5944
$string .= "# REPOSITORY: NEVERSUBMIT\n";
5948
# print out initial comment
5949
if ($profile_data->{$name}{initial_comment}) {
5950
my $comment = $profile_data->{$name}{initial_comment};
5951
$comment =~ s/\\n/\n/g;
5952
$string .= "$comment\n";
5955
#bleah this is stupid the data structure needs to be reworked
5956
my $filename = getprofilefilename($name);
5958
if ($filelist{$filename}) {
5959
push @data, writealiases($filelist{$filename}, 0);
5960
push @data, writelistvars($filelist{$filename}, 0);
5961
push @data, writeincludes($filelist{$filename}, 0);
5967
# # dump variables defined in this file
5968
# if ($variables{$filename}) {
5969
# for my $var (sort keys %{$variables{$filename}}) {
5970
# if ($var =~ m/^@/) {
5971
# my @values = sort @{$variables{$filename}{$var}};
5972
# @values = map { escape($_) } @values;
5973
# my $values = join (" ", @values);
5974
# print SDPROF "$var = ";
5975
# print SDPROF $values;
5976
# } elsif ($var =~ m/^\$/) {
5977
# print SDPROF "$var = ";
5978
# print SDPROF ${$variables{$filename}{$var}};
5979
# } elsif ($var =~ m/^\#/) {
5982
# print SDPROF "#include <$inc>";
5984
# print SDPROF "\n";
5988
push @data, writepiece($profile_data, 0, $name, $name, $include_flags);
5989
$string .= join("\n", @data);
5994
sub writeprofile_ui_feedback ($) {
5995
my $profile = shift;
5996
UI_Info(sprintf(gettext('Writing updated profile for %s.'), $profile));
5997
writeprofile($profile);
6000
sub writeprofile ($) {
6001
my ($profile) = shift;
6003
my $filename = $sd{$profile}{$profile}{filename} || getprofilefilename($profile);
6005
open(SDPROF, ">$filename") or
6006
fatal_error "Can't write new AppArmor profile $filename: $!";
6007
my $serialize_opts = { };
6008
$serialize_opts->{METADATA} = 1;
6010
#make sure to write out all the profiles in the file
6011
my $profile_string = serialize_profile($sd{$profile}, $profile, $serialize_opts);
6012
print SDPROF $profile_string;
6015
# mark the profile as up-to-date
6016
delete $changed{$profile};
6017
$original_sd{$profile} = dclone($sd{$profile});
6020
sub getprofileflags($) {
6021
my $filename = shift;
6023
my $flags = "enforce";
6025
if (open(PROFILE, "$filename")) {
6027
if (m/^\s*\/\S+\s+flags=\((.+)\)\s+{\s*$/) {
6040
sub matchliteral($$) {
6041
my ($sd_regexp, $literal) = @_;
6043
my $p_regexp = convert_regexp($sd_regexp);
6045
# check the log entry against our converted regexp...
6046
my $matches = eval { $literal =~ /^$p_regexp$/; };
6048
# doesn't match if we've got a broken regexp
6054
# test if profile has exec rule for $exec_target
6055
sub profile_known_exec (\%$$) {
6056
my ($profile, $type, $exec_target) = @_;
6057
if ( $type eq "exec" ) {
6061
($cm, $am, @m) = rematchfrag($profile, 'deny', $exec_target);
6062
if ($cm & $AA_MAY_EXEC) {
6065
($cm, $am, @m) = match_prof_incs_to_path($profile, 'deny', $exec_target);
6066
if ($cm & $AA_MAY_EXEC) {
6070
# now test the generally longer allow lists
6071
($cm, $am, @m) = rematchfrag($profile, 'allow', $exec_target);
6072
if ($cm & $AA_MAY_EXEC) {
6076
($cm, $am, @m) = match_prof_incs_to_path($profile, 'allow', $exec_target);
6077
if ($cm & $AA_MAY_EXEC) {
6084
sub profile_known_capability (\%$) {
6085
my ($profile, $capname) = @_;
6087
return -1 if $profile->{deny}{capability}{$capname}{set};
6088
return 1 if $profile->{allow}{capability}{$capname}{set};
6089
for my $incname ( keys %{$profile->{include}} ) {
6090
return -1 if $include{$incname}{$incname}{deny}{capability}{$capname}{set};
6091
return 1 if $include{$incname}{$incname}{allow}{capability}{$capname}{set};
6096
sub profile_known_network (\%$$) {
6097
my ($profile, $family, $sock_type) = @_;
6099
return -1 if netrules_access_check( $profile->{deny}{netdomain},
6100
$family, $sock_type);
6101
return 1 if netrules_access_check( $profile->{allow}{netdomain},
6102
$family, $sock_type);
6104
for my $incname ( keys %{$profile->{include}} ) {
6105
return -1 if netrules_access_check($include{$incname}{$incname}{deny}{netdomain},
6106
$family, $sock_type);
6107
return 1 if netrules_access_check($include{$incname}{$incname}{allow}{netdomain},
6108
$family, $sock_type);
6114
sub netrules_access_check ($$$) {
6115
my ($netrules, $family, $sock_type) = @_;
6116
return 0 if ( not defined $netrules );
6117
my %netrules = %$netrules;
6118
my $all_net = defined $netrules{rule}{all};
6119
my $all_net_family = defined $netrules{rule}{$family} && $netrules{rule}{$family} == 1;
6120
my $net_family_sock = defined $netrules{rule}{$family} &&
6121
ref($netrules{rule}{$family}) eq "HASH" &&
6122
defined $netrules{rule}{$family}{$sock_type};
6124
if ( $all_net || $all_net_family || $net_family_sock ) {
6131
sub reload_base($) {
6134
# don't try to reload profile if AppArmor is not running
6135
return unless check_for_subdomain();
6137
my $filename = getprofilefilename($bin);
6139
system("/bin/cat '$filename' | $parser -I$profiledir -r >/dev/null 2>&1");
6145
# don't reload the profile if the corresponding executable doesn't exist
6146
my $fqdbin = findexecutable($bin) or return;
6148
return reload_base($fqdbin);
6151
sub read_include_from_file($) {
6155
if (open(INCLUDE, "$profiledir/$which")) {
6164
sub get_include_data($) {
6167
my $data = read_include_from_file($which);
6169
fatal_error "Can't find include file $which: $!";
6174
sub loadinclude($) {
6177
# don't bother loading it again if we already have
6178
return 0 if $include{$which}{$which};
6180
my @loadincludes = ($which);
6181
while (my $incfile = shift @loadincludes) {
6183
my $data = get_include_data($incfile);
6184
my $incdata = parse_profile_data($data, $incfile, 1);
6186
attach_profile_data(\%include, $incdata);
6192
sub rematchfrag ($$$) {
6193
my ($frag, $allow, $path) = @_;
6195
my $combinedmode = 0;
6196
my $combinedaudit = 0;
6199
for my $entry (keys %{ $frag->{$allow}{path} }) {
6201
my $regexp = convert_regexp($entry);
6203
# check the log entry against our converted regexp...
6204
if ($path =~ /^$regexp$/) {
6206
# regexp matches, add it's mode to the list to check against
6207
$combinedmode |= $frag->{$allow}{path}{$entry}{mode};
6208
$combinedaudit |= $frag->{$allow}{path}{$entry}{audit};
6209
push @matches, $entry;
6213
return wantarray ? ($combinedmode, $combinedaudit, @matches) : $combinedmode;
6216
sub match_include_to_path ($$$) {
6217
my ($incname, $allow, $path) = @_;
6219
my $combinedmode = 0;
6220
my $combinedaudit = 0;
6223
my @includelist = ( $incname );
6224
while (my $incfile = shift @includelist) {
6225
my $ret = eval { loadinclude($incfile); };
6226
if ($@) { fatal_error $@; }
6227
my ($cm, $am, @m) = rematchfrag($include{$incfile}{$incfile}, $allow, $path);
6229
$combinedmode |= $cm;
6230
$combinedaudit |= $am;
6234
# check if a literal version is in the current include fragment
6235
if ($include{$incfile}{$incfile}{$allow}{path}{$path}) {
6236
$combinedmode |= $include{$incfile}{$incfile}{$allow}{path}{$path}{mode};
6237
$combinedaudit |= $include{$incfile}{$incfile}{$allow}{path}{$path}{audit};
6240
# if this fragment includes others, check them too
6241
if (keys %{ $include{$incfile}{$incfile}{include} }) {
6242
push @includelist, keys %{ $include{$incfile}{$incfile}{include} };
6246
return wantarray ? ($combinedmode, $combinedaudit, @matches) : $combinedmode;
6249
sub match_prof_incs_to_path ($$$) {
6250
my ($frag, $allow, $path) = @_;
6252
my $combinedmode = 0;
6253
my $combinedaudit = 0;
6256
# scan the include fragments for this profile looking for matches
6257
my @includelist = keys %{ $frag->{include} };
6258
while (my $include = shift @includelist) {
6259
my ($cm, $am, @m) = match_include_to_path($include, $allow, $path);
6261
$combinedmode |= $cm;
6262
$combinedaudit |= $am;
6267
return wantarray ? ($combinedmode, $combinedaudit, @matches) : $combinedmode;
6270
#find includes that match the path to suggest
6271
sub suggest_incs_for_path($$$) {
6272
my ($incname, $path, $allow) = @_;
6275
my $combinedmode = 0;
6276
my $combinedaudit = 0;
6279
# scan the include fragments looking for matches
6280
my @includelist = ($incname);
6281
while (my $include = shift @includelist) {
6282
my ($cm, $am, @m) = rematchfrag($include{$include}{$include}, 'allow', $path);
6284
$combinedmode |= $cm;
6285
$combinedaudit |= $am;
6289
# check if a literal version is in the current include fragment
6290
if ($include{$include}{$include}{allow}{path}{$path}) {
6291
$combinedmode |= $include{$include}{$include}{allow}{path}{$path}{mode};
6292
$combinedaudit |= $include{$include}{$include}{allow}{path}{$path}{audit};
6295
# if this fragment includes others, check them too
6296
if (keys %{ $include{$include}{$include}{include} }) {
6297
push @includelist, keys %{ $include{$include}{$include}{include} };
6301
if ($combinedmode) {
6302
return wantarray ? ($combinedmode, $combinedaudit, @matches) : $combinedmode;
6308
sub check_qualifiers($) {
6309
my $program = shift;
6311
if ($cfg->{qualifiers}{$program}) {
6312
unless($cfg->{qualifiers}{$program} =~ /p/) {
6313
fatal_error(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."), $program));
6318
sub loadincludes() {
6319
if (opendir(SDDIR, $profiledir)) {
6320
my @incdirs = grep { (!/^\./) && (-d "$profiledir/$_") } readdir(SDDIR);
6323
while (my $id = shift @incdirs) {
6324
next if isSkippableDir($id);
6325
if (opendir(SDDIR, "$profiledir/$id")) {
6326
for my $path (readdir(SDDIR)) {
6328
next if isSkippableFile($path);
6329
if (-f "$profiledir/$id/$path") {
6330
my $file = "$id/$path";
6331
$file =~ s/$profiledir\///;
6332
my $ret = eval { loadinclude($file); };
6333
if ($@) { fatal_error $@; }
6334
} elsif (-d "$id/$path") {
6335
push @incdirs, "$id/$path";
6344
sub globcommon ($) {
6349
# glob library versions in both foo-5.6.so and baz.so.9.2 form
6350
if ($path =~ m/[\d\.]+\.so$/ || $path =~ m/\.so\.[\d\.]+$/) {
6351
my $libpath = $path;
6352
$libpath =~ s/[\d\.]+\.so$/*.so/;
6353
$libpath =~ s/\.so\.[\d\.]+$/.so.*/;
6354
push @globs, $libpath if $libpath ne $path;
6357
for my $glob (keys %{$cfg->{globs}}) {
6358
if ($path =~ /$glob/) {
6359
my $globbedpath = $path;
6360
$globbedpath =~ s/$glob/$cfg->{globs}{$glob}/g;
6361
push @globs, $globbedpath if $globbedpath ne $path;
6366
return sort { length($b) <=> length($a) } uniq(@globs);
6368
my @list = sort { length($b) <=> length($a) } uniq(@globs);
6369
return $list[$#list];
6373
# this is an ugly, nasty function that attempts to see if one regexp
6374
# is a subset of another regexp
6375
sub matchregexp ($$) {
6376
my ($new, $old) = @_;
6378
# bail out if old pattern has {foo,bar,baz} stuff in it
6379
return undef if $old =~ /\{.*(\,.*)*\}/;
6381
# are there any regexps at all in the old pattern?
6382
if ($old =~ /\[.+\]/ or $old =~ /\*/ or $old =~ /\?/) {
6384
# convert {foo,baz} to (foo|baz)
6385
$new =~ y/\{\}\,/\(\)\|/ if $new =~ /\{.*\,.*\}/;
6387
# \001 == SD_GLOB_RECURSIVE
6388
# \002 == SD_GLOB_SIBLING
6390
$new =~ s/\*\*/\001/g;
6391
$new =~ s/\*/\002/g;
6393
$old =~ s/\*\*/\001/g;
6394
$old =~ s/\*/\002/g;
6396
# strip common prefix
6397
my $prefix = commonprefix($new, $old);
6400
# make sure we don't accidentally gobble up a trailing * or **
6401
$prefix =~ s/(\001|\002)$//;
6402
$new =~ s/^$prefix//;
6403
$old =~ s/^$prefix//;
6406
# strip common suffix
6407
my $suffix = commonsuffix($new, $old);
6410
# make sure we don't accidentally gobble up a leading * or **
6411
$suffix =~ s/^(\001|\002)//;
6412
$new =~ s/$suffix$//;
6413
$old =~ s/$suffix$//;
6416
# if we boiled the differences down to a ** in the new entry, it matches
6417
# whatever's in the old entry
6418
return 1 if $new eq "\001";
6420
# if we've paired things down to a * in new, old matches if there are no
6421
# slashes left in the path
6422
return 1 if ($new eq "\002" && $old =~ /^[^\/]+$/);
6424
# we'll bail out if we have more globs in the old version
6425
return undef if $old =~ /\001|\002/;
6427
# see if we can match * globs in new against literal elements in old
6428
$new =~ s/\002/[^\/]*/g;
6430
return 1 if $old =~ /^$new$/;
6434
my $new_regexp = convert_regexp($new);
6436
# check the log entry against our converted regexp...
6437
return 1 if $old =~ /^$new_regexp$/;
6444
sub combine_name($$) { return ($_[0] eq $_[1]) ? $_[0] : "$_[0]^$_[1]"; }
6445
sub split_name ($) { my ($p, $h) = split(/\^/, $_[0]); $h ||= $p; ($p, $h); }
6447
##########################
6449
# prompt_user($headers, $functions, $default, $options, $selected);
6452
# a required arrayref made up of "key, value" pairs in the order you'd
6453
# like them displayed to user
6456
# a required arrayref of the different options to display at the bottom
6457
# of the prompt like "(A)llow", "(D)eny", and "Ba(c)on". the character
6458
# contained by ( and ) will be used as the key to select the specified
6462
# a required character which is the default "key" to enter when they
6466
# an optional arrayref of the choices like the glob suggestions to be
6467
# presented to the user
6470
# specifies which option is currently selected
6472
# when prompt_user() is called without an $options list, it returns a
6473
# single value which is the key for the specified "function".
6475
# when prompt_user() is called with an $options list, it returns an array
6476
# of two elements, the key for the specified function as well as which
6477
# option was currently selected
6478
#######################################################################
6480
sub Text_PromptUser ($) {
6481
my $question = shift;
6483
my $title = $question->{title};
6484
my $explanation = $question->{explanation};
6486
my @headers = (@{ $question->{headers} });
6487
my @functions = (@{ $question->{functions} });
6489
my $default = $question->{default};
6490
my $options = $question->{options};
6491
my $selected = $question->{selected} || 0;
6493
my $helptext = $question->{helptext};
6495
push @functions, "CMD_HELP" if $helptext;
6499
for my $cmd (@functions) {
6501
# make sure we know about this particular command
6502
my $cmdmsg = "PromptUser: " . gettext("Unknown command") . " $cmd";
6503
fatal_error $cmdmsg unless $CMDS{$cmd};
6505
# grab the localized text to use for the menu for this command
6506
my $menutext = gettext($CMDS{$cmd});
6508
# figure out what the hotkey for this menu item is
6509
my $menumsg = "PromptUser: " .
6510
gettext("Invalid hotkey in") .
6512
$menutext =~ /\((\S)\)/ or fatal_error $menumsg;
6514
# we want case insensitive comparisons so we'll force things to
6518
# check if we're already using this hotkey for this prompt
6519
my $hotkeymsg = "PromptUser: " .
6520
gettext("Duplicate hotkey for") .
6522
fatal_error $hotkeymsg if $keys{$key};
6524
# keep track of which command they're picking if they hit this hotkey
6527
if ($default && $default eq $cmd) {
6528
$menutext = "[$menutext]";
6531
push @menu_items, $menutext;
6534
# figure out the key for the default option
6536
if ($default && $CMDS{$default}) {
6537
my $defaulttext = gettext($CMDS{$default});
6539
# figure out what the hotkey for this menu item is
6540
my $defmsg = "PromptUser: " .
6541
gettext("Invalid hotkey in default item") .
6543
$defaulttext =~ /\((\S)\)/ or fatal_error $defmsg;
6545
# we want case insensitive comparisons so we'll force things to
6547
$default_key = lc($1);
6549
my $defkeymsg = "PromptUser: " .
6550
gettext("Invalid default") .
6552
fatal_error $defkeymsg unless $keys{$default_key};
6557
while (my $header = shift @poo) {
6558
my $value = shift @poo;
6559
$widest = length($header) if length($header) > $widest;
6563
my $format = '%-' . $widest . "s \%s\n";
6565
my $function_regexp = '^(';
6566
$function_regexp .= join("|", keys %keys);
6567
$function_regexp .= '|\d' if $options;
6568
$function_regexp .= ')$';
6570
my $ans = "XXXINVALIDXXX";
6571
while ($ans !~ /$function_regexp/i) {
6572
# build up the prompt...
6575
$prompt .= "= $title =\n\n" if $title;
6579
while (my $header = shift @poo) {
6580
my $value = shift @poo;
6581
$prompt .= sprintf($format, "$header:", $value);
6587
$prompt .= "$explanation\n\n";
6591
for (my $i = 0; $options->[$i]; $i++) {
6592
my $f = ($selected == $i) ? ' [%d - %s]' : ' %d - %s ';
6593
$prompt .= sprintf("$f\n", $i + 1, $options->[$i]);
6597
$prompt .= join(" / ", @menu_items);
6600
# get their input...
6601
$ans = lc(getkey());
6604
# handle escape sequences so you can up/down in the list
6607
if ($options && ($selected > 0)) {
6610
$ans = "XXXINVALIDXXX";
6612
} elsif ($ans eq "down") {
6614
if ($options && ($selected < (scalar(@$options) - 1))) {
6617
$ans = "XXXINVALIDXXX";
6619
} elsif ($keys{$ans} && $keys{$ans} eq "CMD_HELP") {
6621
print "\n$helptext\n";
6622
$ans = "XXXINVALIDXXX";
6624
} elsif (ord($ans) == 10) {
6626
# pick the default if they hit return...
6627
$ans = $default_key;
6629
} elsif ($options && ($ans =~ /^\d$/)) {
6632
if ($ans > 0 && $ans <= scalar(@$options)) {
6633
$selected = $ans - 1;
6635
$ans = "XXXINVALIDXXX";
6639
if ($keys{$ans} && $keys{$ans} eq "CMD_HELP") {
6640
print "\n$helptext\n";
6645
# pull our command back from our hotkey map
6646
$ans = $keys{$ans} if $keys{$ans};
6647
return ($ans, $selected);
6651
# Parse event record into key-value pairs
6652
sub parse_event($) {
6656
my $event = LibAppArmor::parse_record($msg);
6657
my ($rmask, $dmask);
6659
$DEBUGGING && debug("parse_event: $msg");
6661
$ev{'resource'} = LibAppArmor::aa_log_record::swig_info_get($event);
6662
$ev{'active_hat'} = LibAppArmor::aa_log_record::swig_active_hat_get($event);
6663
$ev{'sdmode'} = LibAppArmor::aa_log_record::swig_event_get($event);
6664
$ev{'time'} = LibAppArmor::aa_log_record::swig_epoch_get($event);
6665
$ev{'operation'} = LibAppArmor::aa_log_record::swig_operation_get($event);
6666
$ev{'profile'} = LibAppArmor::aa_log_record::swig_profile_get($event);
6667
$ev{'name'} = LibAppArmor::aa_log_record::swig_name_get($event);
6668
$ev{'name2'} = LibAppArmor::aa_log_record::swig_name2_get($event);
6669
$ev{'attr'} = LibAppArmor::aa_log_record::swig_attribute_get($event);
6670
$ev{'parent'} = LibAppArmor::aa_log_record::swig_parent_get($event);
6671
$ev{'pid'} = LibAppArmor::aa_log_record::swig_pid_get($event);
6672
$ev{'task'} = LibAppArmor::aa_log_record::swig_task_get($event);
6673
$ev{'info'} = LibAppArmor::aa_log_record::swig_info_get($event);
6674
$dmask = LibAppArmor::aa_log_record::swig_denied_mask_get($event);
6675
$rmask = LibAppArmor::aa_log_record::swig_requested_mask_get($event);
6676
$ev{'magic_token'} =
6677
LibAppArmor::aa_log_record::swig_magic_token_get($event);
6680
if ( $ev{'operation'} && optype($ev{'operation'}) eq "net" ) {
6682
LibAppArmor::aa_log_record::swig_net_family_get($event);
6684
LibAppArmor::aa_log_record::swig_net_protocol_get($event);
6686
LibAppArmor::aa_log_record::swig_net_sock_type_get($event);
6689
LibAppArmor::free_record($event);
6691
#map new c and d to w as logprof doesn't support them yet
6701
if ($rmask && !validate_log_mode(hide_log_mode($rmask))) {
6702
fatal_error(sprintf(gettext('Log contains unknown mode %s.'),
6706
if ($dmask && !validate_log_mode(hide_log_mode($dmask))) {
6707
fatal_error(sprintf(gettext('Log contains unknown mode %s.'),
6710
#print "str_to_mode deny $dmask = " . str_to_mode($dmask) . "\n" if ($dmask);
6711
#print "str_to_mode req $rmask = " . str_to_mode($rmask) . "\n" if ($rmask);
6714
($mask, $name) = log_str_to_mode($ev{profile}, $dmask, $ev{name2});
6715
$ev{'denied_mask'} = $mask;
6718
($mask, $name) = log_str_to_mode($ev{profile}, $rmask, $ev{name2});
6719
$ev{'request_mask'} = $mask;
6722
if ( ! $ev{'time'} ) { $ev{'time'} = time; }
6724
# remove null responses
6726
if ( ! $ev{$_} || $ev{$_} !~ /[\/\w]+/) { delete($ev{$_}); }
6729
if ( $ev{'sdmode'} ) {
6730
#0 = invalid, 1 = error, 2 = AUDIT, 3 = ALLOW/PERMIT,
6731
#4 = DENIED/REJECTED, 5 = HINT, 6 = STATUS/config change
6732
if ( $ev{'sdmode'} == 0 ) { $ev{'sdmode'} = "UNKNOWN"; }
6733
elsif ( $ev{'sdmode'} == 1 ) { $ev{'sdmode'} = "ERROR"; }
6734
elsif ( $ev{'sdmode'} == 2 ) { $ev{'sdmode'} = "AUDITING"; }
6735
elsif ( $ev{'sdmode'} == 3 ) { $ev{'sdmode'} = "PERMITTING"; }
6736
elsif ( $ev{'sdmode'} == 4 ) { $ev{'sdmode'} = "REJECTING"; }
6737
elsif ( $ev{'sdmode'} == 5 ) { $ev{'sdmode'} = "HINT"; }
6738
elsif ( $ev{'sdmode'} == 6 ) { $ev{'sdmode'} = "STATUS"; }
6739
else { delete($ev{'sdmode'}); }
6741
if ( $ev{sdmode} ) {
6742
$DEBUGGING && debug( Data::Dumper->Dump([%ev], [qw(*event)]));
6749
###############################################################################
6750
# required initialization
6752
$cfg = read_config("logprof.conf");
6753
if ((not defined $cfg->{settings}{default_owner_prompt})) {
6754
$cfg->{settings}{default_owner_prompt} = 0;
6757
$profiledir = find_first_dir($cfg->{settings}{profiledir}) || "/etc/apparmor.d";
6758
unless (-d $profiledir) { fatal_error "Can't find AppArmor profiles."; }
6760
$extraprofiledir = find_first_dir($cfg->{settings}{inactive_profiledir}) ||
6761
"/etc/apparmor/profiles/extras/";
6763
$parser = find_first_file($cfg->{settings}{parser}) || "/sbin/apparmor_parser";
6764
unless (-x $parser) { fatal_error "Can't find apparmor_parser."; }
6766
$filename = find_first_file($cfg->{settings}{logfiles}) || "/var/log/syslog";
6767
unless (-f $filename) { fatal_error "Can't find system log."; }
6769
$ldd = find_first_file($cfg->{settings}{ldd}) || "/usr/bin/ldd";
6770
unless (-x $ldd) { fatal_error "Can't find ldd."; }
6772
$logger = find_first_file($cfg->{settings}{logger}) || "/bin/logger";
6773
unless (-x $logger) { fatal_error "Can't find logger."; }