~ubuntu-branches/ubuntu/raring/apparmor/raring

« back to all changes in this revision

Viewing changes to utils/SubDomain.pm

  • Committer: Bazaar Package Importer
  • Author(s): Kees Cook
  • Date: 2007-03-23 16:42:01 UTC
  • Revision ID: james.westby@ubuntu.com-20070323164201-jkax6f0oku087b7l
Tags: upstream-2.0.1+510.dfsg
ImportĀ upstreamĀ versionĀ 2.0.1+510.dfsg

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# $Id: SubDomain.pm 509 2007-03-30 17:04:04Z jmichael-at-suse-de $
 
2
#
 
3
# ----------------------------------------------------------------------
 
4
#    Copyright (c) 2006 Novell, Inc. All Rights Reserved.
 
5
#
 
6
#    This program is free software; you can redistribute it and/or
 
7
#    modify it under the terms of version 2 of the GNU General Public
 
8
#    License as published by the Free Software Foundation.
 
9
#
 
10
#    This program is distributed in the hope that it will be useful,
 
11
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
#    GNU General Public License for more details.
 
14
#
 
15
#    You should have received a copy of the GNU General Public License
 
16
#    along with this program; if not, contact Novell, Inc.
 
17
#
 
18
#    To contact Novell about this file by physical or electronic mail,
 
19
#    you may find current contact information at www.novell.com.
 
20
# ----------------------------------------------------------------------
 
21
 
 
22
package Immunix::SubDomain;
 
23
 
 
24
use strict;
 
25
use warnings;
 
26
 
 
27
use Carp;
 
28
use Cwd qw(cwd realpath);
 
29
use File::Basename;
 
30
use Data::Dumper;
 
31
 
 
32
use Locale::gettext;
 
33
use POSIX;
 
34
 
 
35
use Immunix::Severity;
 
36
 
 
37
require Exporter;
 
38
our @ISA    = qw(Exporter);
 
39
our @EXPORT = qw(
 
40
    %sd
 
41
    %qualifiers
 
42
    %include
 
43
    %helpers
 
44
 
 
45
    $filename
 
46
    $profiledir
 
47
    $parser
 
48
    $UI_Mode
 
49
    $running_under_genprof
 
50
 
 
51
    which
 
52
    getprofilefilename
 
53
    get_full_path
 
54
    fatal_error
 
55
 
 
56
    getprofileflags
 
57
    setprofileflags
 
58
    complain
 
59
    enforce
 
60
 
 
61
    autodep
 
62
    reload
 
63
 
 
64
    UI_GetString
 
65
    UI_GetFile
 
66
    UI_YesNo
 
67
    UI_Important
 
68
    UI_Info
 
69
    UI_PromptUser
 
70
 
 
71
    getkey
 
72
 
 
73
    do_logprof_pass
 
74
 
 
75
    readconfig
 
76
    loadincludes
 
77
    readprofile
 
78
    readprofiles
 
79
    writeprofile
 
80
 
 
81
    check_for_subdomain
 
82
 
 
83
    setup_yast
 
84
    shutdown_yast
 
85
    GetDataFromYast
 
86
    SendDataToYast
 
87
 
 
88
    checkProfileSyntax
 
89
    checkIncludeSyntax
 
90
 
 
91
    isSkippableFile
 
92
);
 
93
 
 
94
our $confdir = "/etc/apparmor";
 
95
 
 
96
our $running_under_genprof = 0;
 
97
our $finishing             = 0;
 
98
 
 
99
our $DEBUGGING;
 
100
 
 
101
our $unimplemented_warning = 0;
 
102
 
 
103
# keep track of if we're running under yast or not - default to text mode
 
104
our $UI_Mode = "text";
 
105
 
 
106
our $sevdb;
 
107
 
 
108
# initialize Term::ReadLine if it's available
 
109
our $term;
 
110
eval {
 
111
    require Term::ReadLine;
 
112
    import Term::ReadLine;
 
113
    $term = new Term::ReadLine 'AppArmor';
 
114
};
 
115
 
 
116
# initialize the local poo
 
117
setlocale(LC_MESSAGES, "");
 
118
textdomain("apparmor-utils");
 
119
 
 
120
# where do we get our log messages from?
 
121
our $filename;
 
122
if (-f "/var/log/audit/audit.log") {
 
123
    $filename = "/var/log/audit/audit.log";
 
124
} elsif (-f "/etc/slackware-version") {
 
125
    $filename = "/var/log/syslog";
 
126
} else {
 
127
    $filename = "/var/log/messages";
 
128
}
 
129
 
 
130
our $profiledir = "/etc/apparmor.d";
 
131
 
 
132
# we keep track of the included profile fragments with %include
 
133
my %include;
 
134
 
 
135
my %existing_profiles;
 
136
 
 
137
our $ldd    = "/usr/bin/ldd";
 
138
our $parser = "/sbin/subdomain_parser";
 
139
$parser = "/sbin/apparmor_parser" if -f "/sbin/apparmor_parser";
 
140
 
 
141
our $seenevents = 0;
 
142
 
 
143
# behaviour tweaking
 
144
our %qualifiers;
 
145
our %required_hats;
 
146
our %defaulthat;
 
147
our %globmap;
 
148
our @custom_includes;
 
149
 
 
150
# these are globs that the user specifically entered.  we'll keep track of
 
151
# them so that if one later matches, we'll suggest it again.
 
152
our @userglobs;
 
153
 
 
154
### THESE VARIABLES ARE USED WITHIN LOGPROF
 
155
our %t;
 
156
our %transitions;
 
157
our %sd;    # we keep track of the original profiles in %sd
 
158
 
 
159
my @log;
 
160
my %pid;
 
161
 
 
162
my %seen;
 
163
my %profilechanges;
 
164
my %prelog;
 
165
my %log;
 
166
my %changed;
 
167
my %skip;
 
168
our %helpers;    # we want to preserve this one between passes
 
169
 
 
170
my %variables;   # variables in config files
 
171
 
 
172
### THESE VARIABLES ARE USED WITHIN LOGPROF
 
173
 
 
174
sub debug ($) {
 
175
    my $message = shift;
 
176
 
 
177
    print DEBUG "$message\n" if $DEBUGGING;
 
178
}
 
179
 
 
180
BEGIN {
 
181
    use POSIX qw(:termios_h);
 
182
 
 
183
    my ($term, $oterm, $echo, $noecho, $fd_stdin);
 
184
 
 
185
    $fd_stdin = fileno(STDIN);
 
186
 
 
187
    $term = POSIX::Termios->new();
 
188
    $term->getattr($fd_stdin);
 
189
    $oterm = $term->getlflag();
 
190
 
 
191
    $echo   = ECHO | ECHOK | ICANON;
 
192
    $noecho = $oterm & ~$echo;
 
193
 
 
194
    sub cbreak {
 
195
        $term->setlflag($noecho);
 
196
        $term->setcc(VTIME, 1);
 
197
        $term->setattr($fd_stdin, TCSANOW);
 
198
    }
 
199
 
 
200
    sub cooked {
 
201
        $term->setlflag($oterm);
 
202
        $term->setcc(VTIME, 0);
 
203
        $term->setattr($fd_stdin, TCSANOW);
 
204
    }
 
205
 
 
206
    sub getkey {
 
207
        my $key = '';
 
208
        cbreak();
 
209
        sysread(STDIN, $key, 1);
 
210
        cooked();
 
211
        return $key;
 
212
    }
 
213
 
 
214
    # set things up to log extra info if they want...
 
215
    if ($ENV{LOGPROF_DEBUG}) {
 
216
        $DEBUGGING = 1;
 
217
        open(DEBUG, ">/tmp/logprof_debug_$$.log");
 
218
        my $oldfd = select(DEBUG);
 
219
        $| = 1;
 
220
        select($oldfd);
 
221
    } else {
 
222
        $DEBUGGING = 0;
 
223
    }
 
224
}
 
225
 
 
226
END {
 
227
    # reset the terminal state
 
228
    cooked();
 
229
 
 
230
    $DEBUGGING && debug "Exiting...";
 
231
 
 
232
    # close the debug log if necessary
 
233
    close(DEBUG) if $DEBUGGING;
 
234
}
 
235
 
 
236
# returns true if the specified program contains references to LD_PRELOAD or
 
237
# LD_LIBRARY_PATH to give the PX/UX code better suggestions
 
238
sub check_for_LD_XXX ($) {
 
239
    my $file = shift;
 
240
 
 
241
    return undef unless -f $file;
 
242
 
 
243
    # limit our checking to programs/scripts under 10k to speed things up a bit
 
244
    my $size = -s $file;
 
245
    return undef unless ($size && $size < 10000);
 
246
 
 
247
    my $found = undef;
 
248
    if (open(F, $file)) {
 
249
        while (<F>) {
 
250
            $found = 1 if /LD_(PRELOAD|LIBRARY_PATH)/;
 
251
        }
 
252
        close(F);
 
253
    }
 
254
 
 
255
    return $found;
 
256
}
 
257
 
 
258
sub fatal_error ($) {
 
259
    my $message = shift;
 
260
 
 
261
    my $details = "$message\n";
 
262
 
 
263
    if ($DEBUGGING) {
 
264
 
 
265
        # we'll include the stack backtrace if we're debugging...
 
266
        $details = Carp::longmess($message);
 
267
 
 
268
        # write the error to the log
 
269
        print DEBUG $details;
 
270
    }
 
271
 
 
272
    # we'll just shoot ourselves in the head if it was one of the yast
 
273
    # interface functions that ran into an error.  it gets really ugly if
 
274
    # the yast frontend goes away and we try to notify the user of that
 
275
    # problem by trying to send the yast frontend a pretty dialog box
 
276
    my $caller = (caller(1))[3];
 
277
    exit 1 if $caller =~ /::(Send|Get)Data(To|From)Yast$/;
 
278
 
 
279
    # tell the user what the hell happened
 
280
    UI_Important($details);
 
281
 
 
282
    # make sure the frontend exits cleanly...
 
283
    shutdown_yast();
 
284
 
 
285
    # die a horrible flaming death
 
286
    exit 1;
 
287
}
 
288
 
 
289
sub setup_yast {
 
290
 
 
291
    # set up the yast connection if we're running under yast...
 
292
    if ($ENV{YAST_IS_RUNNING}) {
 
293
 
 
294
        # load the yast module if available.
 
295
        eval { require ycp; };
 
296
        unless ($@) {
 
297
            import ycp;
 
298
 
 
299
            $UI_Mode = "yast";
 
300
 
 
301
            # let the frontend know that we're starting
 
302
            SendDataToYast({
 
303
                type   => "initial_handshake",
 
304
                status => "backend_starting"
 
305
            });
 
306
 
 
307
            # see if the frontend is just starting up also...
 
308
            my ($ypath, $yarg) = GetDataFromYast();
 
309
            unless ($yarg
 
310
                && (ref($yarg)      eq "HASH")
 
311
                && ($yarg->{type}   eq "initial_handshake")
 
312
                && ($yarg->{status} eq "frontend_starting"))
 
313
            {
 
314
 
 
315
                # something's broken, die a horrible, painful death
 
316
                fatal_error "Yast frontend is out of sync from backend agent.";
 
317
            }
 
318
 
 
319
            # the yast connection seems to be working okay
 
320
            return 1;
 
321
        }
 
322
 
 
323
    }
 
324
 
 
325
    # couldn't init yast
 
326
    return 0;
 
327
}
 
328
 
 
329
sub shutdown_yast {
 
330
    if ($UI_Mode eq "yast") {
 
331
        SendDataToYast({ type => "final_shutdown" });
 
332
        my ($ypath, $yarg) = GetDataFromYast();
 
333
    }
 
334
}
 
335
 
 
336
sub check_for_subdomain () {
 
337
 
 
338
    my ($support_subdomainfs, $support_securityfs);
 
339
    if (open(MOUNTS, "/proc/filesystems")) {
 
340
        while (<MOUNTS>) {
 
341
            $support_subdomainfs = 1 if m/subdomainfs/;
 
342
            $support_securityfs  = 1 if m/securityfs/;
 
343
        }
 
344
        close(MOUNTS);
 
345
    }
 
346
 
 
347
    my $sd_mountpoint = "";
 
348
    if (open(MOUNTS, "/proc/mounts")) {
 
349
        while (<MOUNTS>) {
 
350
            if ($support_subdomainfs) {
 
351
                $sd_mountpoint = $1 if m/^\S+\s+(\S+)\s+subdomainfs\s/;
 
352
            } elsif ($support_securityfs) {
 
353
                if (m/^\S+\s+(\S+)\s+securityfs\s/) {
 
354
                    if (-e "$1/apparmor") {
 
355
                        $sd_mountpoint = "$1/apparmor";
 
356
                    } elsif (-e "$1/subdomain") {
 
357
                        $sd_mountpoint = "$1/subdomain";
 
358
                    }
 
359
                }
 
360
            }
 
361
        }
 
362
        close(MOUNTS);
 
363
    }
 
364
 
 
365
    # make sure that subdomain is actually mounted there
 
366
    $sd_mountpoint = undef unless -f "$sd_mountpoint/profiles";
 
367
 
 
368
    return $sd_mountpoint;
 
369
}
 
370
 
 
371
sub which ($) {
 
372
    my $file = shift;
 
373
 
 
374
    foreach my $dir (split(/:/, $ENV{PATH})) {
 
375
        return "$dir/$file" if -x "$dir/$file";
 
376
    }
 
377
 
 
378
    return undef;
 
379
}
 
380
 
 
381
# we need to convert subdomain regexps to perl regexps
 
382
sub convert_regexp ($) {
 
383
    my $regexp = shift;
 
384
 
 
385
    # escape regexp-special characters we don't support
 
386
    $regexp =~ s/(?<!\\)(\+|\$)/\\$1/g;
 
387
 
 
388
    # escape . characters
 
389
    $regexp =~ s/(?<!\\)\./SDPROF_INTERNAL_DOT/g;
 
390
 
 
391
    # convert ** globs to match anything
 
392
    $regexp =~ s/(?<!\\)\*\*/.SDPROF_INTERNAL_GLOB/g;
 
393
 
 
394
    # convert * globs to match anything at current path level
 
395
    $regexp =~ s/(?<!\\)\*/[^\/]SDPROF_INTERNAL_GLOB/g;
 
396
 
 
397
    # convert ? globs to match a single character at current path level
 
398
    $regexp =~ s/(?<!\\)\?/[^\/]/g;
 
399
 
 
400
    # convert {foo,baz} to (foo|baz)
 
401
    $regexp =~ y/\{\}\,/\(\)\|/ if $regexp =~ /\{.*\,.*\}/;
 
402
 
 
403
    # twiddle the escaped * chars back
 
404
    $regexp =~ s/SDPROF_INTERNAL_GLOB/\*/g;
 
405
 
 
406
    # twiddle the escaped . chars back
 
407
    $regexp =~ s/SDPROF_INTERNAL_DOT/\\./g;
 
408
 
 
409
    return $regexp;
 
410
}
 
411
 
 
412
sub get_full_path ($) {
 
413
    my $originalpath = shift;
 
414
 
 
415
    my $path = $originalpath;
 
416
 
 
417
    # keep track so we can break out of loops
 
418
    my $linkcount = 0;
 
419
 
 
420
    # if we don't have any directory foo, look in the current dir
 
421
    $path = cwd() . "/$path" if $path !~ m/\//;
 
422
 
 
423
    # beat symlinks into submission
 
424
    while (-l $path) {
 
425
 
 
426
        if ($linkcount++ > 64) {
 
427
            fatal_error "Followed too many symlinks resolving $originalpath";
 
428
        }
 
429
 
 
430
        # split out the directory/file components
 
431
        if ($path =~ m/^(.*)\/(.+)$/) {
 
432
            my ($dir, $file) = ($1, $2);
 
433
 
 
434
            # figure out where the link is pointing...
 
435
            my $link = readlink($path);
 
436
            if ($link =~ /^\//) {
 
437
                # if it's an absolute link, just replace it
 
438
                $path = $link;
 
439
            } else {
 
440
                # if it's relative, let abs_path handle it
 
441
                $path = $dir . "/$link";
 
442
            }
 
443
        }
 
444
    }
 
445
 
 
446
    if (-f $path) {
 
447
        my ($dir, $file) = $path =~ m/^(.*)\/(.+)$/;
 
448
        $path = realpath($dir) . "/$file";
 
449
    } else {
 
450
        $path = realpath($path);
 
451
    }
 
452
 
 
453
    return $path;
 
454
}
 
455
 
 
456
sub findexecutable ($) {
 
457
    my $bin = shift;
 
458
 
 
459
    my $fqdbin;
 
460
    if (-e $bin) {
 
461
        $fqdbin = get_full_path($bin);
 
462
        chomp($fqdbin);
 
463
    } else {
 
464
        if ($bin !~ /\//) {
 
465
            my $which = which($bin);
 
466
            if ($which) {
 
467
                $fqdbin = get_full_path($which);
 
468
            }
 
469
        }
 
470
    }
 
471
 
 
472
    unless ($fqdbin && -e $fqdbin) {
 
473
        return undef;
 
474
    }
 
475
 
 
476
    return $fqdbin;
 
477
}
 
478
 
 
479
sub complain ($) {
 
480
    my $bin    = shift;
 
481
    my $fqdbin = findexecutable($bin)
 
482
      or fatal_error(sprintf(gettext('Can\'t find %s.'), $bin));
 
483
 
 
484
    # skip directories
 
485
    return unless -f $fqdbin;
 
486
 
 
487
    UI_Info(sprintf(gettext('Setting %s to complain mode.'), $fqdbin));
 
488
 
 
489
    my $filename = getprofilefilename($fqdbin);
 
490
    setprofileflags($filename, "complain");
 
491
}
 
492
 
 
493
sub enforce ($) {
 
494
    my $bin = shift;
 
495
 
 
496
    my $fqdbin = findexecutable($bin)
 
497
      or fatal_error(sprintf(gettext('Can\'t find %s.'), $bin));
 
498
 
 
499
    # skip directories
 
500
    return unless -f $fqdbin;
 
501
 
 
502
    UI_Info(sprintf(gettext('Setting %s to enforce mode.'), $fqdbin));
 
503
 
 
504
    my $filename = getprofilefilename($fqdbin);
 
505
    setprofileflags($filename, "");
 
506
}
 
507
 
 
508
sub head ($) {
 
509
    my $file = shift;
 
510
 
 
511
    my $first = "";
 
512
    if (open(FILE, $file)) {
 
513
        $first = <FILE>;
 
514
        close(FILE);
 
515
    }
 
516
 
 
517
    return $first;
 
518
}
 
519
 
 
520
sub get_output (@) {
 
521
    my ($program, @args) = @_;
 
522
 
 
523
    my $ret = -1;
 
524
 
 
525
    my $pid;
 
526
    my @output;
 
527
 
 
528
    if (-x $program) {
 
529
        $pid = open(KID_TO_READ, "-|");
 
530
        unless (defined $pid) {
 
531
            fatal_error "can't fork: $!";
 
532
        }
 
533
 
 
534
        if ($pid) {
 
535
            while (<KID_TO_READ>) {
 
536
                chomp;
 
537
                push @output, $_;
 
538
            }
 
539
            close(KID_TO_READ);
 
540
            $ret = $?;
 
541
        } else {
 
542
            ($>, $)) = ($<, $();
 
543
            open(STDERR, ">&STDOUT")
 
544
              || fatal_error "can't dup stdout to stderr";
 
545
            exec($program, @args) || fatal_error "can't exec program: $!";
 
546
 
 
547
            # NOTREACHED
 
548
        }
 
549
    }
 
550
 
 
551
    return ($ret, @output);
 
552
}
 
553
 
 
554
sub get_reqs ($) {
 
555
    my $file = shift;
 
556
 
 
557
    my @reqs;
 
558
    my ($ret, @ldd) = get_output($ldd, $file);
 
559
 
 
560
    if ($ret == 0) {
 
561
        for my $line (@ldd) {
 
562
            last if $line =~ /not a dynamic executable/;
 
563
            last if $line =~ /cannot read header/;
 
564
            last if $line =~ /statically linked/;
 
565
 
 
566
            # avoid new kernel 2.6 poo
 
567
            next if $line =~ /linux-(gate|vdso(32|64)).so/;
 
568
 
 
569
            if ($line =~ /^\s*\S+ => (\/\S+)/) {
 
570
                push @reqs, $1;
 
571
            } elsif ($line =~ /^\s*(\/\S+)/) {
 
572
                push @reqs, $1;
 
573
            }
 
574
        }
 
575
    }
 
576
 
 
577
    return @reqs;
 
578
}
 
579
 
 
580
sub handle_binfmt ($$) {
 
581
    my ($profile, $fqdbin) = @_;
 
582
 
 
583
    my %reqs;
 
584
    my @reqs = get_reqs($fqdbin);
 
585
 
 
586
    while (my $library = shift @reqs) {
 
587
 
 
588
        $library = get_full_path($library);
 
589
 
 
590
        push @reqs, get_reqs($library) unless $reqs{$library}++;
 
591
 
 
592
        # does path match anything pulled in by includes in original profile?
 
593
        my $combinedmode = matchincludes($profile, $library);
 
594
 
 
595
        # if we found any matching entries, do the modes match?
 
596
        next if $combinedmode;
 
597
 
 
598
        $library = globcommon($library);
 
599
        chomp $library;
 
600
        next unless $library;
 
601
 
 
602
        $profile->{path}->{$library} = "mr";
 
603
    }
 
604
 
 
605
    return $profile;
 
606
}
 
607
 
 
608
sub autodep ($) {
 
609
    my $bin = shift;
 
610
 
 
611
    # findexecutable() might fail if we're running on a different system
 
612
    # than the logs were collected on.  ugly.  we'll just hope for the best.
 
613
    my $fqdbin = findexecutable($bin) || $bin;
 
614
 
 
615
    # try to make sure we have a full path in case findexecutable failed
 
616
    return unless $fqdbin =~ /^\//;
 
617
 
 
618
    # ignore directories
 
619
    return if -d $fqdbin;
 
620
 
 
621
    my $profile = {
 
622
        flags   => "complain",
 
623
        include => { "abstractions/base" => 1 },
 
624
        path    => { $fqdbin => "mr" }
 
625
    };
 
626
 
 
627
    # if the executable exists on this system, pull in extra dependencies
 
628
    if (-f $fqdbin) {
 
629
        my $hashbang = head($fqdbin);
 
630
        if ($hashbang =~ /^#!\s*(\S+)/) {
 
631
            my $interpreter = get_full_path($1);
 
632
            $profile->{path}->{$interpreter} = "ix";
 
633
            if ($interpreter =~ /perl/) {
 
634
                $profile->{include}->{"abstractions/perl"} = 1;
 
635
            } elsif ($interpreter =~ m/\/bin\/(bash|sh)/) {
 
636
                $profile->{include}->{"abstractions/bash"} = 1;
 
637
            }
 
638
            $profile = handle_binfmt($profile, $interpreter);
 
639
        } else {
 
640
            $profile = handle_binfmt($profile, $fqdbin);
 
641
        }
 
642
    }
 
643
 
 
644
    # stick the profile into our data structure.
 
645
    $sd{$fqdbin}{$fqdbin} = $profile;
 
646
 
 
647
    # instantiate the required infrastructure hats for this changehat app
 
648
    for my $hatglob (keys %required_hats) {
 
649
        if ($fqdbin =~ /$hatglob/) {
 
650
            for my $hat (split(/\s+/, $required_hats{$hatglob})) {
 
651
                $sd{$fqdbin}{$hat} = { flags => "complain" };
 
652
            }
 
653
        }
 
654
    }
 
655
 
 
656
    if (-f "$profiledir/tunables/global") {
 
657
        my $file = getprofilefilename($fqdbin);
 
658
 
 
659
        unless (exists $variables{$file}) {
 
660
            $variables{$file} = {};
 
661
        }
 
662
        $variables{$file}{"#tunables/global"} = 1;    # sorry
 
663
    }
 
664
 
 
665
    # write out the profile...
 
666
    writeprofile($fqdbin);
 
667
}
 
668
 
 
669
sub getprofilefilename ($) {
 
670
    my $profile = shift;
 
671
 
 
672
    my $filename = $profile;
 
673
    $filename =~ s/\///;                              # strip leading /
 
674
    $filename =~ s/\//./g;                            # convert /'s to .'s
 
675
 
 
676
    return "$profiledir/$filename";
 
677
}
 
678
 
 
679
sub setprofileflags ($$) {
 
680
    my $filename = shift;
 
681
    my $newflags = shift;
 
682
 
 
683
    if (open(PROFILE, "$filename")) {
 
684
        if (open(NEWPROFILE, ">$filename.new")) {
 
685
            while (<PROFILE>) {
 
686
                if (m/^\s*("??\/.+?"??)\s+(flags=\(.+\)\s+)*\{\s*$/) {
 
687
                    my ($binary, $flags) = ($1, $2);
 
688
 
 
689
                    if ($newflags) {
 
690
                        $_ = "$binary flags=($newflags) {\n";
 
691
                    } else {
 
692
                        $_ = "$binary {\n";
 
693
                    }
 
694
                } elsif (m/^(\s*\^\S+)\s+(flags=\(.+\)\s+)*\{\s*$/) {
 
695
                    my ($hat, $flags) = ($1, $2);
 
696
 
 
697
                    if ($newflags) {
 
698
                        $_ = "$hat flags=($newflags) {\n";
 
699
                    } else {
 
700
                        $_ = "$hat {\n";
 
701
                    }
 
702
                }
 
703
                print NEWPROFILE;
 
704
            }
 
705
            close(NEWPROFILE);
 
706
            rename("$filename.new", "$filename");
 
707
        }
 
708
        close(PROFILE);
 
709
    }
 
710
}
 
711
 
 
712
sub profile_exists($) {
 
713
    my $program = shift || return 0;
 
714
 
 
715
    # if it's already in the cache, return true
 
716
    return 1 if $existing_profiles{$program};
 
717
 
 
718
    # if the profile exists, mark it in the cache and return true
 
719
    my $profile = getprofilefilename($program);
 
720
    if (-e $profile) {
 
721
        $existing_profiles{$program} = 1;
 
722
        return 1;
 
723
    }
 
724
 
 
725
    # couldn't find a profile, so we'll return false
 
726
    return 0;
 
727
}
 
728
 
 
729
##########################################################################
 
730
# Here are the console/yast interface functions
 
731
 
 
732
sub UI_Info ($) {
 
733
    my $text = shift;
 
734
 
 
735
    $DEBUGGING && debug "UI_Info: $UI_Mode: $text";
 
736
 
 
737
    if ($UI_Mode eq "text") {
 
738
        print "$text\n";
 
739
    } else {
 
740
        ycp::y2milestone($text);
 
741
    }
 
742
}
 
743
 
 
744
sub UI_Important ($) {
 
745
    my $text = shift;
 
746
 
 
747
    $DEBUGGING && debug "UI_Important: $UI_Mode: $text";
 
748
 
 
749
    if ($UI_Mode eq "text") {
 
750
        print "\n$text\n";
 
751
    } else {
 
752
        SendDataToYast({ type => "dialog-error", message => $text });
 
753
        my ($path, $yarg) = GetDataFromYast();
 
754
    }
 
755
}
 
756
 
 
757
sub UI_YesNo ($$) {
 
758
    my $text    = shift;
 
759
    my $default = shift;
 
760
 
 
761
    $DEBUGGING && debug "UI_YesNo: $UI_Mode: $text $default";
 
762
 
 
763
    my $ans;
 
764
    if ($UI_Mode eq "text") {
 
765
 
 
766
        my $yes = gettext("(Y)es");
 
767
        my $no  = gettext("(N)o");
 
768
 
 
769
        # figure out our localized hotkeys
 
770
        my $usrmsg = "PromptUser: " . gettext("Invalid hotkey for");
 
771
        $yes =~ /\((\S)\)/ or fatal_error "$usrmsg '$yes'";
 
772
        my $yeskey = lc($1);
 
773
        $no =~ /\((\S)\)/ or fatal_error "$usrmsg '$no'";
 
774
        my $nokey = lc($1);
 
775
 
 
776
        print "\n$text\n";
 
777
        if ($default eq "y") {
 
778
            print "\n[$yes] / $no\n";
 
779
        } else {
 
780
            print "\n$yes / [$no]\n";
 
781
        }
 
782
        $ans = getkey() || (($default eq "y") ? $yeskey : $nokey);
 
783
 
 
784
        # convert back from a localized answer to english y or n
 
785
        $ans = (lc($ans) eq $yeskey) ? "y" : "n";
 
786
    } else {
 
787
 
 
788
        SendDataToYast({ type => "dialog-yesno", question => $text });
 
789
        my ($ypath, $yarg) = GetDataFromYast();
 
790
        $ans = $yarg->{answer} || $default;
 
791
 
 
792
    }
 
793
 
 
794
    return $ans;
 
795
}
 
796
 
 
797
sub UI_YesNoCancel ($$) {
 
798
    my $text    = shift;
 
799
    my $default = shift;
 
800
 
 
801
    $DEBUGGING && debug "UI_YesNoCancel: $UI_Mode: $text $default";
 
802
 
 
803
    my $ans;
 
804
    if ($UI_Mode eq "text") {
 
805
 
 
806
        my $yes    = gettext("(Y)es");
 
807
        my $no     = gettext("(N)o");
 
808
        my $cancel = gettext("(C)ancel");
 
809
 
 
810
        # figure out our localized hotkeys
 
811
        my $usrmsg = "PromptUser: " . gettext("Invalid hotkey for");
 
812
        $yes =~ /\((\S)\)/ or fatal_error "$usrmsg '$yes'";
 
813
        my $yeskey = lc($1);
 
814
        $no =~ /\((\S)\)/ or fatal_error "$usrmsg '$no'";
 
815
        my $nokey = lc($1);
 
816
        $cancel =~ /\((\S)\)/ or fatal_error "$usrmsg '$cancel'";
 
817
        my $cancelkey = lc($1);
 
818
 
 
819
        $ans = "XXXINVALIDXXX";
 
820
        while ($ans !~ /^(y|n|c)$/) {
 
821
            print "\n$text\n";
 
822
            if ($default eq "y") {
 
823
                print "\n[$yes] / $no / $cancel\n";
 
824
            } elsif ($default eq "n") {
 
825
                print "\n$yes / [$no] / $cancel\n";
 
826
            } else {
 
827
                print "\n$yes / $no / [$cancel]\n";
 
828
            }
 
829
 
 
830
            $ans = getkey();
 
831
 
 
832
            if ($ans) {
 
833
                # convert back from a localized answer to english y or n
 
834
                $ans = lc($ans);
 
835
                if ($ans eq $yeskey) {
 
836
                    $ans = "y";
 
837
                } elsif ($ans eq $nokey) {
 
838
                    $ans = "n";
 
839
                } elsif ($ans eq $cancelkey) {
 
840
                    $ans = "c";
 
841
                }
 
842
            } else {
 
843
                $ans = $default;
 
844
            }
 
845
        }
 
846
    } else {
 
847
 
 
848
        SendDataToYast({ type => "dialog-yesnocancel", question => $text });
 
849
        my ($ypath, $yarg) = GetDataFromYast();
 
850
        $ans = $yarg->{answer} || $default;
 
851
 
 
852
    }
 
853
 
 
854
    return $ans;
 
855
}
 
856
 
 
857
sub UI_GetString ($$) {
 
858
    my $text    = shift;
 
859
    my $default = shift;
 
860
 
 
861
    $DEBUGGING && debug "UI_GetString: $UI_Mode: $text $default";
 
862
 
 
863
    my $string;
 
864
    if ($UI_Mode eq "text") {
 
865
 
 
866
        if ($term) {
 
867
            $string = $term->readline($text, $default);
 
868
        } else {
 
869
            local $| = 1;
 
870
            print "$text";
 
871
            $string = <STDIN>;
 
872
            chomp($string);
 
873
        }
 
874
 
 
875
    } else {
 
876
 
 
877
        SendDataToYast({
 
878
            type    => "dialog-getstring",
 
879
            label   => $text,
 
880
            default => $default
 
881
        });
 
882
        my ($ypath, $yarg) = GetDataFromYast();
 
883
        $string = $yarg->{string};
 
884
 
 
885
    }
 
886
    return $string;
 
887
}
 
888
 
 
889
sub UI_GetFile ($) {
 
890
    my $f = shift;
 
891
 
 
892
    $DEBUGGING && debug "UI_GetFile: $UI_Mode";
 
893
 
 
894
    my $filename;
 
895
    if ($UI_Mode eq "text") {
 
896
 
 
897
        local $| = 1;
 
898
        print "$f->{description}\n";
 
899
        $filename = <STDIN>;
 
900
        chomp($filename);
 
901
 
 
902
    } else {
 
903
 
 
904
        $f->{type} = "dialog-getfile";
 
905
 
 
906
        SendDataToYast($f);
 
907
        my ($ypath, $yarg) = GetDataFromYast();
 
908
        if ($yarg->{answer} eq "okay") {
 
909
            $filename = $yarg->{filename};
 
910
        }
 
911
    }
 
912
 
 
913
    return $filename;
 
914
}
 
915
 
 
916
my %CMDS = (
 
917
    CMD_ALLOW            => "(A)llow",
 
918
    CMD_DENY             => "(D)eny",
 
919
    CMD_ABORT            => "Abo(r)t",
 
920
    CMD_FINISHED         => "(F)inish",
 
921
    CMD_INHERIT          => "(I)nherit",
 
922
    CMD_PROFILE          => "(P)rofile",
 
923
    CMD_PROFILE_CLEAN    => "(P)rofile Clean Exec",
 
924
    CMD_UNCONFINED       => "(U)nconfined",
 
925
    CMD_UNCONFINED_CLEAN => "(U)nconfined Clean Exec",
 
926
    CMD_NEW              => "(N)ew",
 
927
    CMD_GLOB             => "(G)lob",
 
928
    CMD_GLOBEXT          => "Glob w/(E)xt",
 
929
    CMD_ADDHAT           => "(A)dd Requested Hat",
 
930
    CMD_USEDEFAULT       => "(U)se Default Hat",
 
931
    CMD_SCAN             => "(S)can system log for SubDomain events",
 
932
    CMD_HELP             => "(H)elp",
 
933
);
 
934
 
 
935
sub UI_PromptUser ($) {
 
936
    my $q = shift;
 
937
 
 
938
    my ($cmd, $arg);
 
939
    if ($UI_Mode eq "text") {
 
940
 
 
941
        ($cmd, $arg) = Text_PromptUser($q);
 
942
 
 
943
    } else {
 
944
 
 
945
        $q->{type} = "wizard";
 
946
 
 
947
        SendDataToYast($q);
 
948
        my ($ypath, $yarg) = GetDataFromYast();
 
949
 
 
950
        $cmd = $yarg->{selection} || "CMD_ABORT";
 
951
        $arg = $yarg->{selected};
 
952
    }
 
953
 
 
954
    return ($cmd, $arg);
 
955
}
 
956
 
 
957
##########################################################################
 
958
# here are the interface functions to send data back and forth between
 
959
# the yast frontend and the perl backend
 
960
 
 
961
# this is super ugly, but waits for the next ycp Read command and sends data
 
962
# back to the ycp front end.
 
963
 
 
964
sub SendDataToYast {
 
965
    my $data = shift;
 
966
 
 
967
    $DEBUGGING && debug "SendDataToYast: Waiting for YCP command";
 
968
 
 
969
    while (<STDIN>) {
 
970
        $DEBUGGING && debug "SendDataToYast: YCP: $_";
 
971
        my ($ycommand, $ypath, $yargument) = ycp::ParseCommand($_);
 
972
 
 
973
        if ($ycommand && $ycommand eq "Read") {
 
974
 
 
975
            if ($DEBUGGING) {
 
976
                my $debugmsg = Data::Dumper->Dump([$data], [qw(*data)]);
 
977
                debug "SendDataToYast: Sending--\n$debugmsg";
 
978
            }
 
979
 
 
980
            ycp::Return($data);
 
981
            return 1;
 
982
 
 
983
        } else {
 
984
 
 
985
            $DEBUGGING && debug "SendDataToYast: Expected 'Read' but got-- $_";
 
986
 
 
987
        }
 
988
    }
 
989
 
 
990
    # if we ever break out here, something's horribly wrong.
 
991
    fatal_error "SendDataToYast: didn't receive YCP command before connection died";
 
992
}
 
993
 
 
994
# this is super ugly, but waits for the next ycp Write command and grabs
 
995
# whatever the ycp front end gives us
 
996
 
 
997
sub GetDataFromYast {
 
998
 
 
999
    $DEBUGGING && debug "GetDataFromYast: Waiting for YCP command";
 
1000
 
 
1001
    while (<STDIN>) {
 
1002
        $DEBUGGING && debug "GetDataFromYast: YCP: $_";
 
1003
        my ($ycmd, $ypath, $yarg) = ycp::ParseCommand($_);
 
1004
 
 
1005
        if ($DEBUGGING) {
 
1006
            my $debugmsg = Data::Dumper->Dump([$yarg], [qw(*data)]);
 
1007
            debug "GetDataFromYast: Received--\n$debugmsg";
 
1008
        }
 
1009
 
 
1010
        if ($ycmd && $ycmd eq "Write") {
 
1011
 
 
1012
            ycp::Return("true");
 
1013
            return ($ypath, $yarg);
 
1014
 
 
1015
        } else {
 
1016
            $DEBUGGING && debug "GetDataFromYast: Expected 'Write' but got-- $_";
 
1017
        }
 
1018
    }
 
1019
 
 
1020
    # if we ever break out here, something's horribly wrong.
 
1021
    fatal_error "GetDataFromYast: didn't receive YCP command before connection died";
 
1022
}
 
1023
 
 
1024
##########################################################################
 
1025
# this is the hideously ugly function that descends down the flow/event
 
1026
# trees that we've generated by parsing the logfile
 
1027
 
 
1028
sub handlechildren {
 
1029
    my $profile = shift;
 
1030
    my $hat     = shift;
 
1031
    my $root    = shift;
 
1032
 
 
1033
    my @entries = @$root;
 
1034
    for my $entry (@entries) {
 
1035
        fatal_error "$entry is not a ref" if not ref($entry);
 
1036
 
 
1037
        if (ref($entry->[0])) {
 
1038
            handlechildren($profile, $hat, $entry);
 
1039
        } else {
 
1040
 
 
1041
            my @entry = @$entry;
 
1042
            my $type  = shift @entry;
 
1043
 
 
1044
            if ($type eq "fork") {
 
1045
                my ($pid, $p, $h) = @entry;
 
1046
 
 
1047
                if (   ($p !~ /null(-complain)*-profile/)
 
1048
                    && ($h !~ /null(-complain)*-profile/))
 
1049
                {
 
1050
                    $profile = $p;
 
1051
                    $hat     = $h;
 
1052
                }
 
1053
 
 
1054
                $profilechanges{$pid} = $profile;
 
1055
 
 
1056
            } elsif ($type eq "unknown_hat") {
 
1057
                my ($pid, $p, $h, $sdmode, $uhat) = @entry;
 
1058
 
 
1059
                if ($p !~ /null(-complain)*-profile/) {
 
1060
                    $profile = $p;
 
1061
                }
 
1062
 
 
1063
                if ($sd{$profile}{$uhat}) {
 
1064
                    $hat = $uhat;
 
1065
                    next;
 
1066
                }
 
1067
 
 
1068
                # figure out what our default hat for this application is.
 
1069
                my $defaulthat;
 
1070
                for my $hatglob (keys %defaulthat) {
 
1071
                    $defaulthat = $defaulthat{$hatglob}
 
1072
                      if $profile =~ /$hatglob/;
 
1073
                }
 
1074
 
 
1075
                # keep track of previous answers for this run...
 
1076
                my $context = $profile;
 
1077
                $context .= " -> ^$uhat";
 
1078
                my $ans = $transitions{$context} || "";
 
1079
 
 
1080
                unless ($ans) {
 
1081
                    my $q = {};
 
1082
                    $q->{headers} = [];
 
1083
                    push @{ $q->{headers} }, gettext("Profile"), $profile;
 
1084
                    if ($defaulthat) {
 
1085
                        push @{ $q->{headers} }, gettext("Default Hat"), $defaulthat;
 
1086
                    }
 
1087
                    push @{ $q->{headers} }, gettext("Requested Hat"), $uhat;
 
1088
 
 
1089
                    $q->{functions} = [];
 
1090
                    push @{ $q->{functions} }, "CMD_ADDHAT";
 
1091
                    push @{ $q->{functions} }, "CMD_USEDEFAULT" if $defaulthat;
 
1092
                    push @{ $q->{functions} }, "CMD_DENY";
 
1093
                    push @{ $q->{functions} }, "CMD_ABORT";
 
1094
                    push @{ $q->{functions} }, "CMD_FINISHED";
 
1095
 
 
1096
                    $q->{default} = ($sdmode eq "PERMITTING") ? "CMD_ADDHAT" : "CMD_DENY";
 
1097
 
 
1098
                    $seenevents++;
 
1099
 
 
1100
                    my $arg;
 
1101
                    ($ans, $arg) = UI_PromptUser($q);
 
1102
 
 
1103
                    $transitions{$context} = $ans;
 
1104
                }
 
1105
 
 
1106
                # ugh, there's a bug here.  if they pick "abort" or "finish"
 
1107
                # and then say "well, no, I didn't really mean that", we need
 
1108
                # to ask the question again, but we currently go on to the
 
1109
                # next one.  oops.
 
1110
                if ($ans eq "CMD_ADDHAT") {
 
1111
                    $hat = $uhat;
 
1112
                    $sd{$profile}{$hat}{flags} = $sd{$profile}{$profile}{flags};
 
1113
                } elsif ($ans eq "CMD_USEDEFAULT") {
 
1114
                    $hat = $defaulthat;
 
1115
                } elsif ($ans eq "CMD_DENY") {
 
1116
                    return;
 
1117
                } elsif ($ans eq "CMD_ABORT") {
 
1118
                    my $ans = UI_YesNo(gettext("Are you sure you want to abandon this set of profile changes and exit?"), "n");
 
1119
                    if ($ans eq "y") {
 
1120
                        UI_Info(gettext("Abandoning all changes."));
 
1121
                        shutdown_yast();
 
1122
                        exit 0;
 
1123
                    }
 
1124
                } elsif ($ans eq "CMD_FINISHED") {
 
1125
                    my $ans = UI_YesNo(gettext("Are you sure you want to save the current set of profile changes and exit?"), "n");
 
1126
                    if ($ans eq "y") {
 
1127
                        UI_Info(gettext("Saving all changes."));
 
1128
                        $finishing = 1;
 
1129
 
 
1130
                        # XXX - BUGBUG - this is REALLY nasty, but i'm in
 
1131
                        # a hurry...
 
1132
                        goto SAVE_PROFILES;
 
1133
                    }
 
1134
                }
 
1135
 
 
1136
            } elsif ($type eq "capability") {
 
1137
               my ($pid, $p, $h, $prog, $sdmode, $capability) = @entry;
 
1138
 
 
1139
                if (   ($p !~ /null(-complain)*-profile/)
 
1140
                    && ($h !~ /null(-complain)*-profile/))
 
1141
                {
 
1142
                    $profile = $p;
 
1143
                    $hat     = $h;
 
1144
                }
 
1145
 
 
1146
                # print "$pid $profile $hat $prog $sdmode capability $capability\n";
 
1147
 
 
1148
                next unless $profile && $hat;
 
1149
 
 
1150
                $prelog{$sdmode}{$profile}{$hat}{capability}{$capability} = 1;
 
1151
            } elsif (($type eq "path") || ($type eq "exec")) {
 
1152
                my ($pid, $p, $h, $prog, $sdmode, $mode, $detail) = @entry;
 
1153
 
 
1154
                if (   ($p !~ /null(-complain)*-profile/)
 
1155
                    && ($h !~ /null(-complain)*-profile/))
 
1156
                {
 
1157
                    $profile = $p;
 
1158
                    $hat     = $h;
 
1159
                }
 
1160
 
 
1161
                next unless $profile && $hat;
 
1162
 
 
1163
                my $domainchange = ($type eq "exec") ? "change" : "nochange";
 
1164
 
 
1165
                # escape special characters that show up in literal paths
 
1166
                $detail =~ s/(\[|\]|\+|\*|\{|\})/\\$1/g;
 
1167
 
 
1168
                # we need to give the Execute dialog if they're requesting x
 
1169
                # access for something that's not a directory - we'll force
 
1170
                # a "ix" Path dialog for directories
 
1171
                my $do_execute  = 0;
 
1172
                my $exec_target = $detail;
 
1173
                if ($mode =~ s/x//g) {
 
1174
                    if (-d $exec_target) {
 
1175
                        $mode .= "ix";
 
1176
                    } else {
 
1177
                        $do_execute = 1;
 
1178
                    }
 
1179
                }
 
1180
 
 
1181
                if ($mode eq "link") {
 
1182
                    $mode = "l";
 
1183
                    if ($detail =~ m/^from (.+) to (.+)$/) {
 
1184
                        my ($path, $target) = ($1, $2);
 
1185
 
 
1186
                        my $frommode = "lr";
 
1187
                        if (defined $prelog{$sdmode}{$profile}{$hat}{path}{$path}) {
 
1188
                            $frommode .= $prelog{$sdmode}{$profile}{$hat}{path}{$path};
 
1189
                        }
 
1190
                        $frommode = collapsemode($frommode);
 
1191
                        $prelog{$sdmode}{$profile}{$hat}{path}{$path} = $frommode;
 
1192
 
 
1193
                        my $tomode = "lr";
 
1194
                        if (defined $prelog{$sdmode}{$profile}{$hat}{path}{$target}) {
 
1195
                            $tomode .= $prelog{$sdmode}{$profile}{$hat}{path}{$target};
 
1196
                        }
 
1197
                        $tomode = collapsemode($tomode);
 
1198
                        $prelog{$sdmode}{$profile}{$hat}{path}{$target} = $tomode;
 
1199
 
 
1200
                        # print "$pid $profile $hat $prog $sdmode $path:$frommode -> $target:$tomode\n";
 
1201
                    } else {
 
1202
                        next;
 
1203
                    }
 
1204
                } elsif ($mode) {
 
1205
                    my $path = $detail;
 
1206
 
 
1207
                    if (defined $prelog{$sdmode}{$profile}{$hat}{path}{$path}) {
 
1208
                        $mode .= $prelog{$sdmode}{$profile}{$hat}{path}{$path};
 
1209
                        $mode = collapsemode($mode);
 
1210
                    }
 
1211
 
 
1212
                    $prelog{$sdmode}{$profile}{$hat}{path}{$path} = $mode;
 
1213
 
 
1214
                    # print "$pid $profile $hat $prog $sdmode $mode $path\n";
 
1215
                }
 
1216
 
 
1217
                if ($do_execute) {
 
1218
 
 
1219
                    my $context = $profile;
 
1220
                    $context .= "^$hat" if $profile ne $hat;
 
1221
                    $context .= " -> $exec_target";
 
1222
                    my $ans = $transitions{$context} || "";
 
1223
 
 
1224
                    my ($combinedmode, $cm, @m);
 
1225
 
 
1226
                    # does path match any regexps in original profile?
 
1227
                    ($cm, @m) = rematchfrag($sd{$profile}{$hat}, $exec_target);
 
1228
                    $combinedmode .= $cm if $cm;
 
1229
 
 
1230
                    # does path match anything pulled in by includes in
 
1231
                    # original profile?
 
1232
                    ($cm, @m) = matchincludes($sd{$profile}{$hat}, $exec_target);
 
1233
                    $combinedmode .= $cm if $cm;
 
1234
 
 
1235
                    my $exec_mode;
 
1236
                    if (contains($combinedmode, "ix")) {
 
1237
                        $ans       = "CMD_INHERIT";
 
1238
                        $exec_mode = "ixr";
 
1239
                    } elsif (contains($combinedmode, "px")) {
 
1240
                        $ans       = "CMD_PROFILE";
 
1241
                        $exec_mode = "px";
 
1242
                    } elsif (contains($combinedmode, "ux")) {
 
1243
                        $ans       = "CMD_UNCONFINED";
 
1244
                        $exec_mode = "ux";
 
1245
                    } elsif (contains($combinedmode, "Px")) {
 
1246
                        $ans       = "CMD_PROFILE_CLEAN";
 
1247
                        $exec_mode = "Px";
 
1248
                    } elsif (contains($combinedmode, "Ux")) {
 
1249
                        $ans       = "CMD_UNCONFINED_CLEAN";
 
1250
                        $exec_mode = "Ux";
 
1251
                    } else {
 
1252
                        my $options = $qualifiers{$exec_target} || "ipu";
 
1253
 
 
1254
                        # force "ix" as the only option when the profiled
 
1255
                        # program executes itself
 
1256
                        $options = "i" if $exec_target eq $profile;
 
1257
 
 
1258
                        # we always need deny...
 
1259
                        $options .= "d";
 
1260
 
 
1261
                        # figure out what our default option should be...
 
1262
                        my $default;
 
1263
                        if ($options =~ /p/
 
1264
                            && -e getprofilefilename($exec_target))
 
1265
                        {
 
1266
                            $default = "CMD_PROFILE";
 
1267
                        } elsif ($options =~ /i/) {
 
1268
                            $default = "CMD_INHERIT";
 
1269
                        } else {
 
1270
                            $default = "CMD_DENY";
 
1271
                        }
 
1272
 
 
1273
                        # ugh, this doesn't work if someone does an ix before
 
1274
                        # calling this particular child process.  at least
 
1275
                        # it's only a hint instead of mandatory to get this
 
1276
                        # right.
 
1277
                        my $parent_uses_ld_xxx = check_for_LD_XXX($profile);
 
1278
 
 
1279
                        my $severity = $sevdb->rank($exec_target, "x");
 
1280
 
 
1281
                        # build up the prompt...
 
1282
                        my $q = {};
 
1283
                        $q->{headers} = [];
 
1284
                        push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
 
1285
                        if ($prog && $prog ne "HINT") {
 
1286
                            push @{ $q->{headers} }, gettext("Program"), $prog;
 
1287
                        }
 
1288
                        push @{ $q->{headers} }, gettext("Execute"),  $exec_target;
 
1289
                        push @{ $q->{headers} }, gettext("Severity"), $severity;
 
1290
 
 
1291
                        $q->{functions} = [];
 
1292
 
 
1293
                        my $prompt = "\n$context\n";
 
1294
                        push @{ $q->{functions} }, "CMD_INHERIT"
 
1295
                          if $options =~ /i/;
 
1296
                        push @{ $q->{functions} }, "CMD_PROFILE"
 
1297
                          if $options =~ /p/;
 
1298
                        push @{ $q->{functions} }, "CMD_UNCONFINED"
 
1299
                          if $options =~ /u/;
 
1300
                        push @{ $q->{functions} }, "CMD_DENY";
 
1301
                        push @{ $q->{functions} }, "CMD_ABORT";
 
1302
                        push @{ $q->{functions} }, "CMD_FINISHED";
 
1303
 
 
1304
                        $q->{default} = $default;
 
1305
 
 
1306
                        $options = join("|", split(//, $options));
 
1307
 
 
1308
                        $seenevents++;
 
1309
 
 
1310
                        my $arg;
 
1311
                        while ($ans !~ m/^CMD_(INHERIT|PROFILE|PROFILE_CLEAN|UNCONFINED|UNCONFINED_CLEAN|DENY)$/) {
 
1312
                            ($ans, $arg) = UI_PromptUser($q);
 
1313
 
 
1314
                            # check for Abort or Finish
 
1315
                            if ($ans eq "CMD_ABORT") {
 
1316
                                my $ans = UI_YesNo(gettext("Are you sure you want to abandon this set of profile changes and exit?"), "n");
 
1317
                                $DEBUGGING && debug "back from abort yesno";
 
1318
                                if ($ans eq "y") {
 
1319
                                    UI_Info(gettext("Abandoning all changes."));
 
1320
                                    shutdown_yast();
 
1321
                                    exit 0;
 
1322
                                }
 
1323
                            } elsif ($ans eq "CMD_FINISHED") {
 
1324
                                my $ans = UI_YesNo(gettext("Are you sure you want to save the current set of profile changes and exit?"), "n");
 
1325
                                if ($ans eq "y") {
 
1326
                                    UI_Info(gettext("Saving all changes."));
 
1327
                                    $finishing = 1;
 
1328
 
 
1329
                                    # XXX - BUGBUG - this is REALLY nasty,
 
1330
                                    # but i'm in a hurry...
 
1331
                                    goto SAVE_PROFILES;
 
1332
                                }
 
1333
                            } elsif ($ans eq "CMD_PROFILE") {
 
1334
                                my $px_default = "n";
 
1335
                                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.");
 
1336
                                if ($parent_uses_ld_xxx) {
 
1337
                                    $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.");
 
1338
                                }
 
1339
                                my $ynans = UI_YesNo($px_mesg, $px_default);
 
1340
                                if ($ynans eq "y") {
 
1341
                                    $ans = "CMD_PROFILE_CLEAN";
 
1342
                                }
 
1343
                            } elsif ($ans eq "CMD_UNCONFINED") {
 
1344
                                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");
 
1345
                                if ($ynans eq "y") {
 
1346
                                    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");
 
1347
                                    if ($ynans eq "y") {
 
1348
                                        $ans = "CMD_UNCONFINED_CLEAN";
 
1349
                                    }
 
1350
                                } else {
 
1351
                                    $ans = "INVALID";
 
1352
                                }
 
1353
                            }
 
1354
                        }
 
1355
                        $transitions{$context} = $ans;
 
1356
 
 
1357
                        # if we're inheriting, things'll bitch unless we have r
 
1358
                        if ($ans eq "CMD_INHERIT") {
 
1359
                            $exec_mode = "ixr";
 
1360
                        } elsif ($ans eq "CMD_PROFILE") {
 
1361
                            $exec_mode = "px";
 
1362
                        } elsif ($ans eq "CMD_UNCONFINED") {
 
1363
                            $exec_mode = "ux";
 
1364
                        } elsif ($ans eq "CMD_PROFILE_CLEAN") {
 
1365
                            $exec_mode = "Px";
 
1366
                        } elsif ($ans eq "CMD_UNCONFINED_CLEAN") {
 
1367
                            $exec_mode = "Ux";
 
1368
                        } else {
 
1369
 
 
1370
                            # skip all remaining events if they say to deny
 
1371
                            # the exec
 
1372
                            return if $domainchange eq "change";
 
1373
                        }
 
1374
 
 
1375
                        unless ($ans eq "CMD_DENY") {
 
1376
                            if (defined $prelog{PERMITTING}{$profile}{$hat}{path}{$exec_target}) {
 
1377
                                $exec_mode .= $prelog{PERMITTING}{$profile}{$hat}{path}{$exec_target};
 
1378
                                $exec_mode = collapsemode($exec_mode);
 
1379
                            }
 
1380
                            $prelog{PERMITTING}{$profile}{$hat}{path}{$exec_target} = $exec_mode;
 
1381
                            $log{PERMITTING}{$profile}              = {};
 
1382
                            $sd{$profile}{$hat}{path}{$exec_target} = $exec_mode;
 
1383
 
 
1384
                            # mark this profile as changed
 
1385
                            $changed{$profile} = 1;
 
1386
 
 
1387
                            if ($ans eq "CMD_INHERIT") {
 
1388
                                if ($exec_target =~ /perl/) {
 
1389
                                    $sd{$profile}{$hat}{include}{"abstractions/perl"} = 1;
 
1390
                                } elsif ($detail =~ m/\/bin\/(bash|sh)/) {
 
1391
                                    $sd{$profile}{$hat}{include}{"abstractions/bash"} = 1;
 
1392
                                }
 
1393
                                my $hashbang = head($exec_target);
 
1394
                                if ($hashbang =~ /^#!\s*(\S+)/) {
 
1395
                                    my $interpreter = get_full_path($1);
 
1396
                                    $sd{$profile}{$hat}{path}->{$interpreter} = "ix";
 
1397
                                    if ($interpreter =~ /perl/) {
 
1398
                                        $sd{$profile}{$hat}{include}{"abstractions/perl"} = 1;
 
1399
                                    } elsif ($interpreter =~ m/\/bin\/(bash|sh)/) {
 
1400
                                        $sd{$profile}{$hat}{include}{"abstractions/bash"} = 1;
 
1401
                                    }
 
1402
                                }
 
1403
                            } elsif ($ans =~ /^CMD_PROFILE/) {
 
1404
 
 
1405
                                # if they want to use px, make sure a profile
 
1406
                                # exists for the target.
 
1407
                                unless (-e getprofilefilename($exec_target)) {
 
1408
                                    $helpers{$exec_target} = "enforce";
 
1409
                                    autodep($exec_target);
 
1410
                                    reload($exec_target);
 
1411
                                }
 
1412
                            }
 
1413
                        }
 
1414
                    }
 
1415
 
 
1416
                    # print "$pid $profile $hat EXEC $exec_target $ans $exec_mode\n";
 
1417
 
 
1418
                    # update our tracking info based on what kind of change
 
1419
                    # this is...
 
1420
                    if ($ans eq "CMD_INHERIT") {
 
1421
                        $profilechanges{$pid} = $profile;
 
1422
                    } elsif ($ans =~ /^CMD_PROFILE/) {
 
1423
                        if ($sdmode eq "PERMITTING") {
 
1424
                            if ($domainchange eq "change") {
 
1425
                                $profile              = $exec_target;
 
1426
                                $hat                  = $exec_target;
 
1427
                                $profilechanges{$pid} = $profile;
 
1428
                            }
 
1429
                        }
 
1430
                    } elsif ($ans =~ /^CMD_UNCONFINED/) {
 
1431
                        $profilechanges{$pid} = "unconstrained";
 
1432
                        return if $domainchange eq "change";
 
1433
                    }
 
1434
                }
 
1435
            }
 
1436
        }
 
1437
    }
 
1438
}
 
1439
 
 
1440
sub add_to_tree ($@) {
 
1441
    my ($pid, $type, @event) = @_;
 
1442
 
 
1443
    unless (exists $pid{$pid}) {
 
1444
        my $arrayref = [];
 
1445
        push @log, $arrayref;
 
1446
        $pid{$pid} = $arrayref;
 
1447
    }
 
1448
 
 
1449
    push @{ $pid{$pid} }, [ $type, $pid, @event ];
 
1450
}
 
1451
 
 
1452
sub do_logprof_pass {
 
1453
    my $logmark = shift || "";
 
1454
 
 
1455
    # zero out the state variables for this pass...
 
1456
    %t              = ();
 
1457
    %transitions    = ();
 
1458
    %seen           = ();
 
1459
    %sd             = ();
 
1460
    %profilechanges = ();
 
1461
    %prelog         = ();
 
1462
    %log            = ();
 
1463
    %changed        = ();
 
1464
    %skip           = ();
 
1465
    %variables      = ();
 
1466
 
 
1467
    UI_Info(sprintf(gettext('Reading log entries from %s.'),      $filename));
 
1468
    UI_Info(sprintf(gettext('Updating AppArmor profiles in %s.'), $profiledir));
 
1469
 
 
1470
    readprofiles();
 
1471
 
 
1472
    my $seenmark = $logmark ? 0 : 1;
 
1473
 
 
1474
    $sevdb = new Immunix::Severity("$confdir/severity.db", gettext("unknown"));
 
1475
 
 
1476
    my $stuffed = undef;
 
1477
    my $last;
 
1478
 
 
1479
    # okay, done loading the previous profiles, get on to the good stuff...
 
1480
    open(LOG, $filename)
 
1481
      or fatal_error "Can't read AppArmor logfile $filename: $!";
 
1482
    while (($_ = $stuffed) || ($_ = <LOG>)) {
 
1483
        chomp;
 
1484
 
 
1485
        $stuffed = undef;
 
1486
 
 
1487
        $seenmark = 1 if /$logmark/;
 
1488
 
 
1489
        next unless $seenmark;
 
1490
 
 
1491
        # all we care about is subdomain messages
 
1492
        next
 
1493
          unless (/^.* audit\(/
 
1494
            || /type=(APPARMOR|UNKNOWN\[1500\]) msg=audit\([\d\.\:]+\):/
 
1495
            || /SubDomain/);
 
1496
 
 
1497
        # workaround for syslog uglyness.
 
1498
        if (s/(PERMITTING|REJECTING)-SYSLOGFIX/$1/) {
 
1499
            s/%%/%/g;
 
1500
        }
 
1501
 
 
1502
        if (m/LOGPROF-HINT unknown_hat (\S+) pid=(\d+) profile=(.+) active=(.+)/) {
 
1503
            my ($uhat, $pid, $profile, $hat) = ($1, $2, $3, $4);
 
1504
 
 
1505
            $last = $&;
 
1506
 
 
1507
            # we want to ignore entries for profiles that don't exist - they're
 
1508
            # most likely broken entries or old entries for deleted profiles
 
1509
            next
 
1510
              if ( ($profile ne 'null-complain-profile')
 
1511
                && (!profile_exists($profile)));
 
1512
 
 
1513
            add_to_tree($pid, "unknown_hat", $profile, $hat, "PERMITTING", $uhat);
 
1514
        } elsif (m/LOGPROF-HINT (unknown_profile|missing_mandatory_profile) image=(.+) pid=(\d+) profile=(.+) active=(.+)/) {
 
1515
            my ($image, $pid, $profile, $hat) = ($2, $3, $4, $5);
 
1516
 
 
1517
            next if $last =~ /PERMITTING x access to $image/;
 
1518
            $last = $&;
 
1519
 
 
1520
            # we want to ignore entries for profiles that don't exist - they're
 
1521
            # most likely broken entries or old entries for deleted profiles
 
1522
            next
 
1523
              if ( ($profile ne 'null-complain-profile')
 
1524
                && (!profile_exists($profile)));
 
1525
 
 
1526
            add_to_tree($pid, "exec", $profile, $hat, "HINT", "PERMITTING", "x", $image);
 
1527
 
 
1528
        } elsif (m/(PERMITTING|REJECTING) (\S+) access (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
 
1529
            my ($sdmode, $mode, $detail, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6, $7);
 
1530
 
 
1531
            my $domainchange = "nochange";
 
1532
            if ($mode =~ /x/) {
 
1533
 
 
1534
                # we need to try to check if we're doing a domain transition
 
1535
                if ($sdmode eq "PERMITTING") {
 
1536
                    do {
 
1537
                        $stuffed = <LOG>;
 
1538
                    } until ((! $stuffed) || ($stuffed =~ /AppArmor|audit/));
 
1539
 
 
1540
                    if ($stuffed && ($stuffed =~ m/changing_profile/)) {
 
1541
                        $domainchange = "change";
 
1542
                        $stuffed      = undef;
 
1543
                    }
 
1544
                }
 
1545
            } else {
 
1546
 
 
1547
                # we want to ignore duplicates for things other than executes...
 
1548
                next if $seen{$&};
 
1549
                $seen{$&} = 1;
 
1550
            }
 
1551
 
 
1552
            $last = $&;
 
1553
 
 
1554
            # we want to ignore entries for profiles that don't exist - they're
 
1555
            # most likely broken entries or old entries for deleted profiles
 
1556
            if (   ($profile ne 'null-complain-profile')
 
1557
                && (!profile_exists($profile)))
 
1558
            {
 
1559
                $stuffed = undef;
 
1560
                next;
 
1561
            }
 
1562
 
 
1563
            # currently no way to stick pipe mediation in a profile, ignore
 
1564
            # any messages like this
 
1565
            next if $detail =~ /to pipe:/;
 
1566
 
 
1567
            # strip out extra extended attribute info since we don't currently
 
1568
            # have a way to specify it in the profile and instead just need to
 
1569
            # provide the access to the base filename
 
1570
            $detail =~ s/\s+extended attribute \S+//;
 
1571
 
 
1572
            # kerberos code checks to see if the krb5.conf file is world
 
1573
            # writable in a stupid way so we'll ignore any w accesses to
 
1574
            # krb5.conf
 
1575
            next if (($detail eq "to /etc/krb5.conf") && contains($mode, "w"));
 
1576
 
 
1577
            # strip off the (deleted) tag that gets added if it's a deleted file
 
1578
            $detail =~ s/\s+\(deleted\)$//;
 
1579
 
 
1580
#            next if (($detail =~ /to \/lib\/ld-/) && ($mode =~ /x/));
 
1581
 
 
1582
            $detail =~ s/^to\s+//;
 
1583
 
 
1584
            if ($domainchange eq "change") {
 
1585
                add_to_tree($pid, "exec", $profile, $hat, $prog, $sdmode, $mode, $detail);
 
1586
            } else {
 
1587
                add_to_tree($pid, "path", $profile, $hat, $prog, $sdmode, $mode, $detail);
 
1588
            }
 
1589
 
 
1590
        } elsif (m/(PERMITTING|REJECTING) (?:mk|rm)dir on (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
 
1591
            my ($sdmode, $path, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6);
 
1592
 
 
1593
            # we want to ignore duplicates for things other than executes...
 
1594
            next if $seen{$&}++;
 
1595
 
 
1596
            $last = $&;
 
1597
 
 
1598
            # we want to ignore entries for profiles that don't exist - they're
 
1599
            # most likely broken entries or old entries for deleted profiles
 
1600
            next
 
1601
              if ( ($profile ne 'null-complain-profile')
 
1602
                && (!profile_exists($profile)));
 
1603
 
 
1604
            add_to_tree($pid, "path", $profile, $hat, $prog, $sdmode, "w", $path);
 
1605
 
 
1606
        } elsif (m/(PERMITTING|REJECTING) xattr (\S+) on (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
 
1607
            my ($sdmode, $xattr_op, $path, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6, $7);
 
1608
 
 
1609
            # we want to ignore duplicates for things other than executes...
 
1610
            next if $seen{$&}++;
 
1611
 
 
1612
            $last = $&;
 
1613
 
 
1614
            # we want to ignore entries for profiles that don't exist - they're
 
1615
            # most likely broken entries or old entries for deleted profiles
 
1616
            next
 
1617
              if ( ($profile ne 'null-complain-profile')
 
1618
                && (!profile_exists($profile)));
 
1619
 
 
1620
            my $xattrmode;
 
1621
            if ($xattr_op eq "get" || $xattr_op eq "list") {
 
1622
                $xattrmode = "r";
 
1623
            } elsif ($xattr_op eq "set" || $xattr_op eq "remove") {
 
1624
                $xattrmode = "w";
 
1625
            }
 
1626
 
 
1627
            if ($xattrmode) {
 
1628
                add_to_tree($pid, "path", $profile, $hat, $prog, $sdmode, $xattrmode, $path);
 
1629
            }
 
1630
 
 
1631
        } elsif (m/(PERMITTING|REJECTING) attribute \((.*?)\) change to (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
 
1632
            my ($sdmode, $change, $path, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6, $7);
 
1633
 
 
1634
            # we want to ignore duplicates for things other than executes...
 
1635
            next if $seen{$&};
 
1636
            $seen{$&} = 1;
 
1637
 
 
1638
            $last = $&;
 
1639
 
 
1640
            # we want to ignore entries for profiles that don't exist - they're
 
1641
            # most likely broken entries or old entries for deleted profiles
 
1642
            next
 
1643
              if ( ($profile ne 'null-complain-profile')
 
1644
                && (!profile_exists($profile)));
 
1645
 
 
1646
            # kerberos code checks to see if the krb5.conf file is world
 
1647
            # writable in a stupid way so we'll ignore any w accesses to
 
1648
            # krb5.conf
 
1649
            next if $path eq "/etc/krb5.conf";
 
1650
 
 
1651
            add_to_tree($pid, "path", $profile, $hat, $prog, $sdmode, "w", $path);
 
1652
 
 
1653
        } elsif (m/(PERMITTING|REJECTING) access to capability '(\S+)' \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
 
1654
            my ($sdmode, $capability, $prog, $pid, $profile, $hat) = ($1, $2, $3, $4, $5, $6);
 
1655
 
 
1656
            next if $seen{$&};
 
1657
 
 
1658
            $seen{$&} = 1;
 
1659
            $last = $&;
 
1660
 
 
1661
            # we want to ignore entries for profiles that don't exist - they're
 
1662
            # most likely broken entries or old entries for deleted profiles
 
1663
            next
 
1664
              if ( ($profile ne 'null-complain-profile')
 
1665
                && (!profile_exists($profile)));
 
1666
 
 
1667
            add_to_tree($pid, "capability", $profile, $hat, $prog, $sdmode, $capability);
 
1668
 
 
1669
        } elsif (m/Fork parent (\d+) child (\d+) profile (.+) active (.+)/
 
1670
            || m/LOGPROF-HINT fork pid=(\d+) child=(\d+) profile=(.+) active=(.+)/
 
1671
            || m/LOGPROF-HINT fork pid=(\d+) child=(\d+)/)
 
1672
        {
 
1673
            my ($parent, $child, $profile, $hat) = ($1, $2, $3, $4);
 
1674
 
 
1675
            $profile ||= "null-complain-profile";
 
1676
            $hat     ||= "null-complain-profile";
 
1677
 
 
1678
            $last = $&;
 
1679
 
 
1680
            # we want to ignore entries for profiles that don't exist - they're
 
1681
            # most likely broken entries or old entries for deleted profiles
 
1682
            next
 
1683
              if ( ($profile ne 'null-complain-profile')
 
1684
                && (!profile_exists($profile)));
 
1685
 
 
1686
            my $arrayref = [];
 
1687
            if (exists $pid{$parent}) {
 
1688
                push @{ $pid{$parent} }, $arrayref;
 
1689
            } else {
 
1690
                push @log, $arrayref;
 
1691
            }
 
1692
            $pid{$child} = $arrayref;
 
1693
            push @{$arrayref}, [ "fork", $child, $profile, $hat ];
 
1694
        } else {
 
1695
            $DEBUGGING && debug "UNHANDLED: $_";
 
1696
        }
 
1697
    }
 
1698
    close(LOG);
 
1699
 
 
1700
    for my $root (@log) {
 
1701
        handlechildren(undef, undef, $root);
 
1702
    }
 
1703
 
 
1704
    for my $pid (sort { $a <=> $b } keys %profilechanges) {
 
1705
        setprocess($pid, $profilechanges{$pid});
 
1706
    }
 
1707
 
 
1708
    collapselog();
 
1709
 
 
1710
    my $found;
 
1711
 
 
1712
    # do the magic foo-foo
 
1713
    for my $sdmode (sort keys %log) {
 
1714
 
 
1715
        # let them know what sort of changes we're about to list...
 
1716
        if ($sdmode eq "PERMITTING") {
 
1717
            UI_Info(gettext("Complain-mode changes:"));
 
1718
        } elsif ($sdmode eq "REJECTING") {
 
1719
            UI_Info(gettext("Enforce-mode changes:"));
 
1720
        } else {
 
1721
 
 
1722
            # if we're not permitting and not rejecting, something's broken.
 
1723
            # most likely  the code we're using to build the hash tree of log
 
1724
            # entries - this should never ever happen
 
1725
            fatal_error(sprintf(gettext('Invalid mode found: %s'), $sdmode));
 
1726
        }
 
1727
 
 
1728
        for my $profile (sort keys %{ $log{$sdmode} }) {
 
1729
 
 
1730
            $found++;
 
1731
 
 
1732
            # this sorts the list of hats, but makes sure that the containing
 
1733
            # profile shows up in the list first to keep the question order
 
1734
            # rational
 
1735
            my @hats =
 
1736
              grep { $_ ne $profile } keys %{ $log{$sdmode}{$profile} };
 
1737
            unshift @hats, $profile
 
1738
              if defined $log{$sdmode}{$profile}{$profile};
 
1739
 
 
1740
            for my $hat (@hats) {
 
1741
 
 
1742
                # step through all the capabilities first...
 
1743
                for my $capability (sort keys %{ $log{$sdmode}{$profile}{$hat}{capability} }) {
 
1744
 
 
1745
                    # we don't care about it if we've already added it to the
 
1746
                    # profile
 
1747
                    next if $sd{$profile}{$hat}{capability}{$capability};
 
1748
 
 
1749
                    my $severity = $sevdb->rank(uc("cap_$capability"));
 
1750
 
 
1751
                    my $q = {};
 
1752
                    $q->{headers} = [];
 
1753
                    push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
 
1754
                    push @{ $q->{headers} }, gettext("Capability"), $capability;
 
1755
                    push @{ $q->{headers} }, gettext("Severity"),   $severity;
 
1756
 
 
1757
                    $q->{functions} = [ "CMD_ALLOW", "CMD_DENY", "CMD_ABORT", "CMD_FINISHED" ];
 
1758
 
 
1759
                    # complain-mode events default to allow - enforce defaults
 
1760
                    # to deny
 
1761
                    $q->{default} = ($sdmode eq "PERMITTING") ? "CMD_ALLOW" : "CMD_DENY";
 
1762
 
 
1763
                    $seenevents++;
 
1764
 
 
1765
                    # what did the grand exalted master tell us to do?
 
1766
                    my ($ans, $arg) = UI_PromptUser($q);
 
1767
 
 
1768
                    if ($ans eq "CMD_ALLOW") {
 
1769
 
 
1770
                        # they picked (a)llow, so...
 
1771
 
 
1772
                        # stick the capability into the profile
 
1773
                        $sd{$profile}{$hat}{capability}{$capability} = 1;
 
1774
 
 
1775
                        # mark this profile as changed
 
1776
                        $changed{$profile} = 1;
 
1777
 
 
1778
                        # give a little feedback to the user
 
1779
                        UI_Info(sprintf(gettext('Adding capability %s to profile.'), $capability));
 
1780
                    } elsif ($ans eq "CMD_DENY") {
 
1781
                        UI_Info(sprintf(gettext('Denying capability %s to profile.'), $capability));
 
1782
                    } elsif ($ans eq "CMD_ABORT") {
 
1783
 
 
1784
                        # if we're in yast, they've already been asked for
 
1785
                        # confirmation
 
1786
                        if ($UI_Mode eq "yast") {
 
1787
                            UI_Info(gettext("Abandoning all changes."));
 
1788
                            shutdown_yast();
 
1789
                            exit 0;
 
1790
                        }
 
1791
                        my $ans = UI_YesNo(gettext("Are you sure you want to abandon this set of profile changes and exit?"), "n");
 
1792
                        if ($ans eq "y") {
 
1793
                            UI_Info(gettext("Abandoning all changes."));
 
1794
                            shutdown_yast();
 
1795
                            exit 0;
 
1796
                        } else {
 
1797
                            redo;
 
1798
                        }
 
1799
                    } elsif ($ans eq "CMD_FINISHED") {
 
1800
 
 
1801
                        # if we're in yast, they've already been asked for
 
1802
                        # confirmation
 
1803
                        if ($UI_Mode eq "yast") {
 
1804
                            UI_Info(gettext("Saving all changes."));
 
1805
                            $finishing = 1;
 
1806
 
 
1807
                            # XXX - BUGBUG - this is REALLY nasty, but i'm in
 
1808
                            # a hurry...
 
1809
                            goto SAVE_PROFILES;
 
1810
                        }
 
1811
                        my $ans = UI_YesNo(gettext("Are you sure you want to save the current set of profile changes and exit?"), "n");
 
1812
                        if ($ans eq "y") {
 
1813
                            UI_Info(gettext("Saving all changes."));
 
1814
                            $finishing = 1;
 
1815
 
 
1816
                            # XXX - BUGBUG - this is REALLY nasty, but i'm in
 
1817
                            # a hurry...
 
1818
                            goto SAVE_PROFILES;
 
1819
                        } else {
 
1820
                            redo;
 
1821
                        }
 
1822
                    }
 
1823
                }
 
1824
 
 
1825
                # and then step through all of the path entries...
 
1826
                for my $path (sort keys %{ $log{$sdmode}{$profile}{$hat}{path} }) {
 
1827
 
 
1828
                    my $mode = $log{$sdmode}{$profile}{$hat}{path}{$path};
 
1829
 
 
1830
                    # if we had an access(X_OK) request or some other kind of
 
1831
                    # event that generates a "PERMITTING x" syslog entry,
 
1832
                    # first check if it was already dealt with by a i/p/x
 
1833
                    # question due to a exec().  if not, ask about adding ix
 
1834
                    # permission.
 
1835
                    if ($mode =~ /X/) {
 
1836
 
 
1837
                        # get rid of the access() markers.
 
1838
                        $mode =~ s/X//g;
 
1839
 
 
1840
                        my $combinedmode = "";
 
1841
 
 
1842
                        my ($cm, @m);
 
1843
 
 
1844
                        # does path match any regexps in original profile?
 
1845
                        ($cm, @m) = rematchfrag($sd{$profile}{$hat}, $path);
 
1846
                        $combinedmode .= $cm if $cm;
 
1847
 
 
1848
                        # does path match anything pulled in by includes in
 
1849
                        # original profile?
 
1850
                        ($cm, @m) = matchincludes($sd{$profile}{$hat}, $path);
 
1851
                        $combinedmode .= $cm if $cm;
 
1852
 
 
1853
                        if ($combinedmode) {
 
1854
                            if (   contains($combinedmode, "ix")
 
1855
                                || contains($combinedmode, "px")
 
1856
                                || contains($combinedmode, "ux")
 
1857
                                || contains($combinedmode, "Px")
 
1858
                                || contains($combinedmode, "Ux"))
 
1859
                            {
 
1860
                            } else {
 
1861
                                $mode .= "ix";
 
1862
                            }
 
1863
                        } else {
 
1864
                            $mode .= "ix";
 
1865
                        }
 
1866
                    }
 
1867
 
 
1868
                    # if we had an mmap(PROT_EXEC) request, first check if we
 
1869
                    # already have added an ix rule to the profile
 
1870
                    if ($mode =~ /m/) {
 
1871
                        my $combinedmode = "";
 
1872
                        my ($cm, @m);
 
1873
 
 
1874
                        # does path match any regexps in original profile?
 
1875
                        ($cm, @m) = rematchfrag($sd{$profile}{$hat}, $path);
 
1876
                        $combinedmode .= $cm if $cm;
 
1877
 
 
1878
                        # does path match anything pulled in by includes in
 
1879
                        # original profile?
 
1880
                        ($cm, @m) = matchincludes($sd{$profile}{$hat}, $path);
 
1881
                        $combinedmode .= $cm if $cm;
 
1882
 
 
1883
                        # ix implies m.  don't ask if they want to add an "m"
 
1884
                        # rule when we already have a matching ix rule.
 
1885
                        if ($combinedmode && contains($combinedmode, "ix")) {
 
1886
                            $mode =~ s/m//g;
 
1887
                        }
 
1888
                    }
 
1889
 
 
1890
                    next unless $mode;
 
1891
 
 
1892
                    my $combinedmode = "";
 
1893
                    my @matches;
 
1894
 
 
1895
                    my ($cm, @m);
 
1896
 
 
1897
                    # does path match any regexps in original profile?
 
1898
                    ($cm, @m) = rematchfrag($sd{$profile}{$hat}, $path);
 
1899
                    if ($cm) {
 
1900
                        $combinedmode .= $cm;
 
1901
                        push @matches, @m;
 
1902
                    }
 
1903
 
 
1904
                    # does path match anything pulled in by includes in
 
1905
                    # original profile?
 
1906
                    ($cm, @m) = matchincludes($sd{$profile}{$hat}, $path);
 
1907
                    if ($cm) {
 
1908
                        $combinedmode .= $cm;
 
1909
                        push @matches, @m;
 
1910
                    }
 
1911
 
 
1912
                    unless ($combinedmode && contains($combinedmode, $mode)) {
 
1913
 
 
1914
                        my $defaultoption = 1;
 
1915
                        my @options       = ();
 
1916
 
 
1917
                        # check the path against the available set of include
 
1918
                        # files
 
1919
                        my @newincludes;
 
1920
                        my $includevalid;
 
1921
                        for my $incname (keys %include) {
 
1922
                            $includevalid = 0;
 
1923
 
 
1924
                            # don't suggest it if we're already including it,
 
1925
                            # that's dumb
 
1926
                            next if $sd{$profile}{$hat}{$incname};
 
1927
 
 
1928
                            # only match includes that can be suggested to
 
1929
                            # the user
 
1930
                            for my $incmatch (@custom_includes) {
 
1931
                                $includevalid = 1 if $incname =~ /$incmatch/;
 
1932
                            }
 
1933
                            $includevalid = 1 if $incname =~ /abstractions/;
 
1934
                            next if ($includevalid == 0);
 
1935
 
 
1936
                            ($cm, @m) = matchinclude($incname, $path);
 
1937
                            if ($cm && contains($cm, $mode)) {
 
1938
                                unless (grep { $_ eq "/**" } @m) {
 
1939
                                    push @newincludes, $incname;
 
1940
                                }
 
1941
                            }
 
1942
                        }
 
1943
 
 
1944
                        # did any match?  add them to the option list...
 
1945
                        if (@newincludes) {
 
1946
                            push @options,
 
1947
                              map { "#include <$_>" }
 
1948
                              sort(uniq(@newincludes));
 
1949
                        }
 
1950
 
 
1951
                        # include the literal path in the option list...
 
1952
                        push @options, $path;
 
1953
 
 
1954
                        # match the current path against the globbing list in
 
1955
                        # logprof.conf
 
1956
                        my @globs = globcommon($path);
 
1957
                        if (@globs) {
 
1958
                            push @matches, @globs;
 
1959
                        }
 
1960
 
 
1961
                        # suggest any matching globs the user manually entered
 
1962
                        for my $userglob (@userglobs) {
 
1963
                            push @matches, $userglob
 
1964
                              if matchliteral($userglob, $path);
 
1965
                        }
 
1966
 
 
1967
                        # we'll take the cheesy way and order the suggested
 
1968
                        # globbing list by length, which is usually right,
 
1969
                        # but not always always
 
1970
                        push @options,
 
1971
                          sort { length($b) <=> length($a) }
 
1972
                          grep { $_ ne $path }
 
1973
                          uniq(@matches);
 
1974
                        $defaultoption = $#options + 1;
 
1975
 
 
1976
                        my $severity = $sevdb->rank($path, $mode);
 
1977
 
 
1978
                        my $done = 0;
 
1979
                        while (not $done) {
 
1980
 
 
1981
                            my $q = {};
 
1982
                            $q->{headers} = [];
 
1983
                            push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
 
1984
                            push @{ $q->{headers} }, gettext("Path"), $path;
 
1985
 
 
1986
                            # merge in any previous modes from this run
 
1987
                            if ($combinedmode) {
 
1988
                                $combinedmode = collapsemode($combinedmode);
 
1989
                                push @{ $q->{headers} }, gettext("Old Mode"), $combinedmode;
 
1990
                                $mode = collapsemode("$mode$combinedmode");
 
1991
                                push @{ $q->{headers} }, gettext("New Mode"), $mode;
 
1992
                            } else {
 
1993
                                push @{ $q->{headers} }, gettext("Mode"), $mode;
 
1994
                            }
 
1995
                            push @{ $q->{headers} }, gettext("Severity"), $severity;
 
1996
 
 
1997
                            $q->{options}  = [@options];
 
1998
                            $q->{selected} = $defaultoption - 1;
 
1999
 
 
2000
                            $q->{functions} = [ "CMD_ALLOW", "CMD_DENY", "CMD_GLOB", "CMD_GLOBEXT", "CMD_NEW", "CMD_ABORT", "CMD_FINISHED" ];
 
2001
 
 
2002
                            $q->{default} =
 
2003
                              ($sdmode eq "PERMITTING")
 
2004
                              ? "CMD_ALLOW"
 
2005
                              : "CMD_DENY";
 
2006
 
 
2007
                            $seenevents++;
 
2008
 
 
2009
                            # if they just hit return, use the default answer
 
2010
                            my ($ans, $selected) = UI_PromptUser($q);
 
2011
 
 
2012
                            if ($ans eq "CMD_ALLOW") {
 
2013
                                $path = $selected;
 
2014
                                $done = 1;
 
2015
                                if ($path =~ m/^#include <(.+)>$/) {
 
2016
                                    my $inc = $1;
 
2017
 
 
2018
                                    my $deleted = 0;
 
2019
                                    for my $entry (keys %{ $sd{$profile}{$hat}{path} }) {
 
2020
 
 
2021
                                        next if $path eq $entry;
 
2022
 
 
2023
                                        my $cm = matchinclude($inc, $entry);
 
2024
                                        if ($cm
 
2025
                                            && contains($cm, $sd{$profile}{$hat}{path}{$entry}))
 
2026
                                        {
 
2027
                                            delete $sd{$profile}{$hat}{path}{$entry};
 
2028
                                            $deleted++;
 
2029
                                        }
 
2030
                                    }
 
2031
 
 
2032
                                    # record the new entry
 
2033
                                    $sd{$profile}{$hat}{include}{$inc} = 1;
 
2034
 
 
2035
                                    $changed{$profile} = 1;
 
2036
                                    UI_Info(sprintf(gettext('Adding #include <%s> to profile.'), $inc));
 
2037
                                    UI_Info(sprintf(gettext('Deleted %s previous matching profile entries.'), $deleted)) if $deleted;
 
2038
                                } else {
 
2039
                                    if ($sd{$profile}{$hat}{path}{$path}) {
 
2040
                                        $mode = collapsemode($mode . $sd{$profile}{$hat}{path}{$path});
 
2041
                                    }
 
2042
 
 
2043
                                    my $deleted = 0;
 
2044
                                    for my $entry (keys %{ $sd{$profile}{$hat}{path} }) {
 
2045
 
 
2046
                                        next if $path eq $entry;
 
2047
 
 
2048
                                        if (matchregexp($path, $entry)) {
 
2049
 
 
2050
                                            # regexp matches, add it's mode to
 
2051
                                            # the list to check against
 
2052
                                            if (contains($mode, $sd{$profile}{$hat}{path}{$entry})) {
 
2053
                                                delete $sd{$profile}{$hat}{path}{$entry};
 
2054
                                                $deleted++;
 
2055
                                            }
 
2056
                                        }
 
2057
                                    }
 
2058
 
 
2059
                                    # record the new entry
 
2060
                                    $sd{$profile}{$hat}{path}{$path} = $mode;
 
2061
 
 
2062
                                    $changed{$profile} = 1;
 
2063
                                    UI_Info(sprintf(gettext('Adding %s %s to profile.'), $path, $mode));
 
2064
                                    UI_Info(sprintf(gettext('Deleted %s previous matching profile entries.'), $deleted)) if $deleted;
 
2065
                                }
 
2066
                            } elsif ($ans eq "CMD_DENY") {
 
2067
 
 
2068
                                # go on to the next entry without saving this
 
2069
                                # one
 
2070
                                $done = 1;
 
2071
                            } elsif ($ans eq "CMD_NEW") {
 
2072
                                if ($selected !~ /^#include/) {
 
2073
                                    $ans = UI_GetString(gettext("Enter new path: "), $selected);
 
2074
                                    if ($ans) {
 
2075
                                        unless (matchliteral($ans, $path)) {
 
2076
                                            my $ynprompt = gettext("The specified path does not match this log entry:") . "\n\n";
 
2077
                                            $ynprompt .= "  " . gettext("Log Entry") . ":    $path\n";
 
2078
                                            $ynprompt .= "  " . gettext("Entered Path") . ": $ans\n\n";
 
2079
                                            $ynprompt .= gettext("Do you really want to use this path?") . "\n";
 
2080
 
 
2081
                                            # we default to no if they just hit return...
 
2082
                                            my $key = UI_YesNo($ynprompt, "n");
 
2083
 
 
2084
                                            next if $key eq "n";
 
2085
                                        }
 
2086
 
 
2087
                                        # save this one for later
 
2088
                                        push @userglobs, $ans;
 
2089
 
 
2090
                                        push @options, $ans;
 
2091
                                        $defaultoption = $#options + 1;
 
2092
                                    }
 
2093
                                }
 
2094
                            } elsif ($ans eq "CMD_GLOB") {
 
2095
 
 
2096
                                # do globbing if they don't have an include
 
2097
                                # selected
 
2098
                                unless ($selected =~ /^#include/) {
 
2099
                                    my $newpath = $selected;
 
2100
 
 
2101
                                    # do we collapse to /* or /**?
 
2102
                                    if ($newpath =~ m/\/\*{1,2}$/) {
 
2103
                                        $newpath =~ s/\/[^\/]+\/\*{1,2}$/\/\*\*/;
 
2104
                                    } else {
 
2105
                                        $newpath =~ s/\/[^\/]+$/\/\*/;
 
2106
                                    }
 
2107
                                    if ($newpath ne $selected) {
 
2108
                                        push @options, $newpath;
 
2109
                                        $defaultoption = $#options + 1;
 
2110
                                    }
 
2111
                                }
 
2112
                            } elsif ($ans eq "CMD_GLOBEXT") {
 
2113
 
 
2114
                                # do globbing if they don't have an include
 
2115
                                # selected
 
2116
                                unless ($selected =~ /^#include/) {
 
2117
                                    my $newpath = $selected;
 
2118
 
 
2119
                                    # do we collapse to /*.ext or /**.ext?
 
2120
                                    if ($newpath =~ m/\/\*{1,2}\.[^\/]+$/) {
 
2121
                                        $newpath =~ s/\/[^\/]+\/\*{1,2}(\.[^\/]+)$/\/\*\*$1/;
 
2122
                                    } else {
 
2123
                                        $newpath =~ s/\/[^\/]+(\.[^\/]+)$/\/\*$1/;
 
2124
                                    }
 
2125
                                    if ($newpath ne $selected) {
 
2126
                                        push @options, $newpath;
 
2127
                                        $defaultoption = $#options + 1;
 
2128
                                    }
 
2129
                                }
 
2130
                            } elsif ($ans =~ /\d/) {
 
2131
                                $defaultoption = $ans;
 
2132
                            } elsif ($ans eq "CMD_ABORT") {
 
2133
                                $ans = UI_YesNo(gettext("Are you sure you want to abandon this set of profile changes and exit?"), "n");
 
2134
                                if ($ans eq "y") {
 
2135
                                    UI_Info(gettext("Abandoning all changes."));
 
2136
                                    shutdown_yast();
 
2137
                                    exit 0;
 
2138
                                }
 
2139
                            } elsif ($ans eq "CMD_FINISHED") {
 
2140
                                $ans = UI_YesNo(gettext("Are you sure you want to save the current set of profile changes and exit?"), "n");
 
2141
                                if ($ans eq "y") {
 
2142
                                    UI_Info(gettext("Saving all changes."));
 
2143
                                    $finishing = 1;
 
2144
 
 
2145
                                    # XXX - BUGBUG - this is REALLY nasty, but
 
2146
                                    # i'm in a hurry...
 
2147
                                    goto SAVE_PROFILES;
 
2148
                                }
 
2149
                            }
 
2150
                        }
 
2151
                    }
 
2152
                }
 
2153
            }
 
2154
        }
 
2155
    }
 
2156
 
 
2157
    if ($UI_Mode eq "yast") {
 
2158
        if (not $running_under_genprof) {
 
2159
            if ($seenevents) {
 
2160
                my $w = { type => "wizard" };
 
2161
                $w->{explanation} = gettext("The profile analyzer has completed processing the log files.\nAll updated profiles will be reloaded");
 
2162
                $w->{functions} = [ "CMD_ABORT", "CMD_FINISHED" ];
 
2163
                SendDataToYast($w);
 
2164
                my $foo = GetDataFromYast();
 
2165
            } else {
 
2166
                my $w = { type => "wizard" };
 
2167
                $w->{explanation} = gettext("No unhandled AppArmor events were found in the system log.");
 
2168
                $w->{functions} = [ "CMD_ABORT", "CMD_FINISHED" ];
 
2169
                SendDataToYast($w);
 
2170
                my $foo = GetDataFromYast();
 
2171
            }
 
2172
        }
 
2173
    }
 
2174
 
 
2175
  SAVE_PROFILES:
 
2176
 
 
2177
    # make sure the profile changes we've made are saved to disk...
 
2178
    for my $profile (sort keys %changed) {
 
2179
        writeprofile($profile);
 
2180
        reload($profile);
 
2181
    }
 
2182
 
 
2183
    # if they hit "Finish" we need to tell the caller that so we can exit
 
2184
    # all the way instead of just going back to the genprof prompt
 
2185
    return $finishing ? "FINISHED" : "NORMAL";
 
2186
}
 
2187
 
 
2188
sub setprocess ($$) {
 
2189
    my ($pid, $profile) = @_;
 
2190
 
 
2191
    # don't do anything if the process exited already...
 
2192
    return unless -e "/proc/$pid/attr/current";
 
2193
 
 
2194
    return unless open(CURR, "/proc/$pid/attr/current");
 
2195
    my $current = <CURR>;
 
2196
    chomp $current;
 
2197
    close(CURR);
 
2198
 
 
2199
    # only change null profiles
 
2200
    return unless $current =~ /null(-complain)*-profile/;
 
2201
 
 
2202
    return unless open(STAT, "/proc/$pid/stat");
 
2203
    my $stat = <STAT>;
 
2204
    chomp $stat;
 
2205
    close(STAT);
 
2206
 
 
2207
    return unless $stat =~ /^\d+ \((\S+)\) /;
 
2208
    my $currprog = $1;
 
2209
 
 
2210
    open(CURR, ">/proc/$pid/attr/current") or return;
 
2211
    print CURR "setprofile $profile";
 
2212
    close(CURR);
 
2213
}
 
2214
 
 
2215
sub collapselog () {
 
2216
    for my $sdmode (keys %prelog) {
 
2217
        for my $profile (keys %{ $prelog{$sdmode} }) {
 
2218
            for my $hat (keys %{ $prelog{$sdmode}{$profile} }) {
 
2219
                for my $path (keys %{ $prelog{$sdmode}{$profile}{$hat}{path} }) {
 
2220
 
 
2221
                    my $mode = $prelog{$sdmode}{$profile}{$hat}{path}{$path};
 
2222
 
 
2223
                    # we want to ignore anything from the log that's already
 
2224
                    # in the profile
 
2225
                    my $combinedmode = "";
 
2226
 
 
2227
                    # is it in the original profile?
 
2228
                    if ($sd{$profile}{$hat}{path}{$path}) {
 
2229
                        $combinedmode .= $sd{$profile}{$hat}{path}{$path};
 
2230
                    }
 
2231
 
 
2232
                    # does path match any regexps in original profile?
 
2233
                    $combinedmode .= rematchfrag($sd{$profile}{$hat}, $path);
 
2234
 
 
2235
                    # does path match anything pulled in by includes in
 
2236
                    # original profile?
 
2237
                    $combinedmode .= matchincludes($sd{$profile}{$hat}, $path);
 
2238
 
 
2239
                    # if we found any matching entries, do the modes match?
 
2240
                    unless ($combinedmode && contains($combinedmode, $mode)) {
 
2241
 
 
2242
                        # merge in any previous modes from this run
 
2243
                        if ($log{$sdmode}{$profile}{$hat}{path}{$path}) {
 
2244
                            $mode = collapsemode($mode . $log{$sdmode}{$profile}{$hat}{path}{$path});
 
2245
                        }
 
2246
 
 
2247
                        # record the new entry
 
2248
                        $log{$sdmode}{$profile}{$hat}{path}{$path} = collapsemode($mode);
 
2249
                    }
 
2250
                }
 
2251
 
 
2252
                for my $capability (keys %{ $prelog{$sdmode}{$profile}{$hat}{capability} }) {
 
2253
 
 
2254
                    # if we don't already have this capability in the profile,
 
2255
                    # add it
 
2256
                    unless ($sd{$profile}{$hat}{capability}{$capability}) {
 
2257
                        $log{$sdmode}{$profile}{$hat}{capability}{$capability} = 1;
 
2258
                    }
 
2259
                }
 
2260
            }
 
2261
        }
 
2262
    }
 
2263
}
 
2264
 
 
2265
sub profilemode ($) {
 
2266
    my $mode = shift;
 
2267
 
 
2268
    my $modifier = ($mode =~ m/[iupUP]/)[0];
 
2269
    if ($modifier) {
 
2270
        $mode =~ s/[iupUPx]//g;
 
2271
        $mode .= $modifier . "x";
 
2272
    }
 
2273
 
 
2274
    return $mode;
 
2275
}
 
2276
 
 
2277
# kinky.
 
2278
sub commonprefix (@) { (join("\0", @_) =~ m/^([^\0]*)[^\0]*(\0\1[^\0]*)*$/)[0] }
 
2279
sub commonsuffix (@) { reverse(((reverse join("\0", @_)) =~ m/^([^\0]*)[^\0]*(\0\1[^\0]*)*$/)[0]); }
 
2280
 
 
2281
sub uniq (@) {
 
2282
    my %seen;
 
2283
    my @result = sort grep { !$seen{$_}++ } @_;
 
2284
    return @result;
 
2285
}
 
2286
 
 
2287
sub collapsemode ($) {
 
2288
    my $old = shift;
 
2289
 
 
2290
    my %seen;
 
2291
    my $new = join "", sort
 
2292
      grep { !$seen{$_}++ } $old =~ m/\G(r|w|l|m|ix|px|ux|Px|Ux)/g;
 
2293
    return $new;
 
2294
}
 
2295
 
 
2296
sub contains ($$) {
 
2297
    my ($glob, $single) = @_;
 
2298
 
 
2299
    $glob = "" unless defined $glob;
 
2300
 
 
2301
    my %h;
 
2302
    $h{$_}++ for ($glob =~ m/\G(r|w|l|m|ix|px|ux|Px|Ux)/g);
 
2303
 
 
2304
    for my $mode ($single =~ m/\G(r|w|l|m|ix|px|ux|Px|Ux)/g) {
 
2305
        return 0 unless $h{$mode};
 
2306
    }
 
2307
 
 
2308
    return 1;
 
2309
}
 
2310
 
 
2311
# isSkippableFile - return true if filename matches something that
 
2312
# should be skipped (rpm backup files, dotfiles, emacs backup files
 
2313
sub isSkippableFile($) {
 
2314
    my $path = shift;
 
2315
 
 
2316
    return ($path =~ /(^|\/)\.[^\/]*$/
 
2317
            || $path =~ /\.rpm(save|new)$/
 
2318
            || $path =~ /\~$/);
 
2319
}
 
2320
 
 
2321
sub checkIncludeSyntax($) {
 
2322
    my $errors = shift;
 
2323
 
 
2324
    if (opendir(SDDIR, $profiledir)) {
 
2325
        my @incdirs = grep { (!/^\./) && (-d "$profiledir/$_") } readdir(SDDIR);
 
2326
        close(SDDIR);
 
2327
        while (my $id = shift @incdirs) {
 
2328
            if (opendir(SDDIR, "$profiledir/$id")) {
 
2329
                for my $path (grep { !/^\./ } readdir(SDDIR)) {
 
2330
                    chomp($path);
 
2331
                    next if isSkippableFile($path);
 
2332
                    if (-f "$profiledir/$id/$path") {
 
2333
                        my $file = "$id/$path";
 
2334
                        $file =~ s/$profiledir\///;
 
2335
                        my $err = loadinclude($file, \&printMessageErrorHandler);
 
2336
                        if ($err ne 0) {
 
2337
                            push @$errors, $err;
 
2338
                        }
 
2339
                    } elsif (-d "$id/$path") {
 
2340
                        push @incdirs, "$id/$path";
 
2341
                    }
 
2342
                }
 
2343
                closedir(SDDIR);
 
2344
            }
 
2345
        }
 
2346
    }
 
2347
    return $errors;
 
2348
}
 
2349
 
 
2350
sub checkProfileSyntax ($) {
 
2351
    my $errors = shift;
 
2352
 
 
2353
    # Check the syntax of profiles
 
2354
 
 
2355
    opendir(SDDIR, $profiledir)
 
2356
      or fatal_error "Can't read AppArmor profiles in $profiledir.";
 
2357
    for my $file (grep { -f "$profiledir/$_" } readdir(SDDIR)) {
 
2358
        next if isSkippableFile($file);
 
2359
        my $err = readprofile("$profiledir/$file", \&printMessageErrorHandler);
 
2360
        if (defined $err and $err ne 1) {
 
2361
            push @$errors, $err;
 
2362
        }
 
2363
    }
 
2364
    closedir(SDDIR);
 
2365
    return $errors;
 
2366
}
 
2367
 
 
2368
sub printMessageErrorHandler ($) {
 
2369
    my $message = shift;
 
2370
    return $message;
 
2371
}
 
2372
 
 
2373
sub readprofiles () {
 
2374
    opendir(SDDIR, $profiledir)
 
2375
      or fatal_error "Can't read AppArmor profiles in $profiledir.";
 
2376
    for my $file (grep { -f "$profiledir/$_" } readdir(SDDIR)) {
 
2377
        next if isSkippableFile($file);
 
2378
        readprofile("$profiledir/$file", \&fatal_error);
 
2379
    }
 
2380
    closedir(SDDIR);
 
2381
}
 
2382
 
 
2383
sub readprofile ($$) {
 
2384
    my $file          = shift;
 
2385
    my $error_handler = shift;
 
2386
    if (open(SDPROF, "$file")) {
 
2387
        my ($profile, $hat, $in_contained_hat);
 
2388
        my $initial_comment = "";
 
2389
        while (<SDPROF>) {
 
2390
            chomp;
 
2391
 
 
2392
            # we don't care about blank lines
 
2393
            next if /^\s*$/;
 
2394
 
 
2395
            # start of a profile...
 
2396
            if (m/^\s*("??\/.+?"??)\s+(flags=\(.+\)\s+)*\{\s*$/) {
 
2397
 
 
2398
                # if we run into the start of a profile while we're already in a
 
2399
                # profile, something's wrong...
 
2400
                if ($profile) {
 
2401
                    return &$error_handler("$profile profile in $file contains syntax errors.");
 
2402
                }
 
2403
 
 
2404
                # we hit the start of a profile, keep track of it...
 
2405
                $profile = $1;
 
2406
                my $flags = $2;
 
2407
                $in_contained_hat = 0;
 
2408
 
 
2409
                # hat is same as profile name if we're not in a hat
 
2410
                ($profile, $hat) = split /\^/, $profile;
 
2411
 
 
2412
                # deal with whitespace in profile and hat names.
 
2413
                $profile = $1 if $profile =~ /^"(.+)"$/;
 
2414
                $hat     = $1 if $hat && $hat =~ /^"(.+)"$/;
 
2415
 
 
2416
                # if we run into old-style hat declarations mark the profile as
 
2417
                # changed so we'll write it out as new-style
 
2418
                if ($hat && $hat ne $profile) {
 
2419
                    $changed{$profile} = 1;
 
2420
                }
 
2421
 
 
2422
                $hat ||= $profile;
 
2423
 
 
2424
                # keep track of profile flags
 
2425
                if ($flags && $flags =~ /^flags=\((.+)\)\s*$/) {
 
2426
                    $flags = $1;
 
2427
                    $sd{$profile}{$hat}{flags} = $flags;
 
2428
                }
 
2429
 
 
2430
                $sd{$profile}{$hat}{netdomain} = [];
 
2431
 
 
2432
                # store off initial comment if they have one
 
2433
                $sd{$profile}{$hat}{initial_comment} = $initial_comment
 
2434
                  if $initial_comment;
 
2435
                $initial_comment = "";
 
2436
 
 
2437
            } elsif (m/^\s*\}\s*$/) {    # end of a profile...
 
2438
 
 
2439
                # if we hit the end of a profile when we're not in one,
 
2440
                # something's wrong...
 
2441
                if (not $profile) {
 
2442
                    return &$error_handler(sprintf(gettext('%s contains syntax errors.'), $file));
 
2443
                }
 
2444
 
 
2445
                if ($in_contained_hat) {
 
2446
                    $hat              = $profile;
 
2447
                    $in_contained_hat = 0;
 
2448
                } else {
 
2449
 
 
2450
                    # if we're finishing a profile, make sure that any required
 
2451
                    # infrastructure hats for this changehat application exist
 
2452
                    for my $hatglob (keys %required_hats) {
 
2453
                        if ($profile =~ /$hatglob/) {
 
2454
                            for my $hat (split(/\s+/, $required_hats{$hatglob})) {
 
2455
                                unless ($sd{$profile}{$hat}) {
 
2456
                                    $sd{$profile}{$hat} = {};
 
2457
 
 
2458
                                    # if we had to auto-instantiate a hat, we
 
2459
                                    # want to write out an updated version of
 
2460
                                    # the profile
 
2461
                                    $changed{$profile} = 1;
 
2462
                                }
 
2463
                            }
 
2464
                        }
 
2465
                    }
 
2466
 
 
2467
                    # mark that we're outside of a profile now...
 
2468
                    $profile         = undef;
 
2469
                    $initial_comment = "";
 
2470
                }
 
2471
 
 
2472
            } elsif (m/^\s*capability\s+(\S+)\s*,\s*$/) {    # capability entry
 
2473
                if (not $profile) {
 
2474
                    return &$error_handler(sprintf(gettext('%s contains syntax errors.'), $file));
 
2475
                }
 
2476
 
 
2477
                my $capability = $1;
 
2478
                $sd{$profile}{$hat}{capability}{$capability} = 1;
 
2479
 
 
2480
            } elsif (/^\s*(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*=\s*(true|false)\s*$/i) {              # boolean definition
 
2481
            } elsif (/^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\+=\s*(.+)\s*$/) {                      # variable additions
 
2482
            } elsif (/^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*=\s*(.+)\s*$/) {                        # variable definitions
 
2483
            } elsif (m/^\s*if\s+(not\s+)?(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*\{\s*$/) {              # conditional -- boolean
 
2484
            } elsif (m/^\s*if\s+(not\s+)?defined\s+(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\{\s*$/) {     # conditional -- variable defined
 
2485
            } elsif (m/^\s*if\s+(not\s+)?defined\s+(\$\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\{\s*$/) {    # conditional -- boolean defined
 
2486
            } elsif (m/^\s*([\"\@\/].*)\s+(\S+)\s*,\s*$/) {                                           # path entry
 
2487
                if (not $profile) {
 
2488
                    return &$error_handler(sprintf(gettext('%s contains syntax errors.'), $file));
 
2489
                }
 
2490
 
 
2491
                my ($path, $mode) = ($1, $2);
 
2492
 
 
2493
                # strip off any trailing spaces.
 
2494
                $path =~ s/\s+$//;
 
2495
 
 
2496
                $path = $1 if $path =~ /^"(.+)"$/;
 
2497
 
 
2498
                # make sure they don't have broken regexps in the profile
 
2499
                my $p_re = convert_regexp($path);
 
2500
                eval { "foo" =~ m/^$p_re$/; };
 
2501
                if ($@) {
 
2502
                    return &$error_handler(sprintf(gettext('Profile %s contains invalid regexp %s.'), $file, $path));
 
2503
                }
 
2504
 
 
2505
                $sd{$profile}{$hat}{path}{$path} = $mode;
 
2506
 
 
2507
            } elsif (m/^\s*#include <(.+)>\s*$/) {    # include stuff
 
2508
                my $include = $1;
 
2509
 
 
2510
                if ($profile) {
 
2511
                    $sd{$profile}{$hat}{include}{$include} = 1;
 
2512
                } else {
 
2513
                    unless (exists $variables{$file}) {
 
2514
                        $variables{$file} = {};
 
2515
                    }
 
2516
                    $variables{$file}{ "#" . $include } = 1;    # sorry
 
2517
                }
 
2518
                my $ret = loadinclude($include, $error_handler);
 
2519
                return $ret if ($ret != 0);
 
2520
 
 
2521
            } elsif (/^\s*(tcp_connect|tcp_accept|udp_send|udp_receive)/) {
 
2522
                if (not $profile) {
 
2523
                    return &$error_handler(sprintf(gettext('%s contains syntax errors.'), $file));
 
2524
                }
 
2525
 
 
2526
                # XXX - BUGBUGBUG - don't strip netdomain entries
 
2527
 
 
2528
                unless ($sd{$profile}{$hat}{netdomain}) {
 
2529
                    $sd{$profile}{$hat}{netdomain} = [];
 
2530
                }
 
2531
 
 
2532
                # strip leading spaces and trailing comma
 
2533
                s/^\s+//;
 
2534
                s/,\s*$//;
 
2535
 
 
2536
                # keep track of netdomain entries...
 
2537
                push @{ $sd{$profile}{$hat}{netdomain} }, $_;
 
2538
 
 
2539
            } elsif (m/^\s*\^(\"?.+?)\s+(flags=\(.+\)\s+)*\{\s*$/) {
 
2540
                # start of a hat
 
2541
 
 
2542
                # if we hit the start of a contained hat when we're not
 
2543
                # in a profile something is wrong...
 
2544
                if (not $profile) {
 
2545
                    return &$error_handler(sprintf(gettext('%s contains syntax errors.'), $file));
 
2546
                }
 
2547
 
 
2548
                $in_contained_hat = 1;
 
2549
 
 
2550
                # we hit the start of a hat inside the current profile
 
2551
                $hat = $1;
 
2552
                my $flags = $2;
 
2553
 
 
2554
                # deal with whitespace in hat names.
 
2555
                $hat = $1 if $hat =~ /^"(.+)"$/;
 
2556
 
 
2557
                # keep track of profile flags
 
2558
                if ($flags && $flags =~ /^flags=\((.+)\)\s*$/) {
 
2559
                    $flags = $1;
 
2560
                    $sd{$profile}{$hat}{flags} = $flags;
 
2561
                }
 
2562
 
 
2563
                $sd{$profile}{$hat}{path}      = {};
 
2564
                $sd{$profile}{$hat}{netdomain} = [];
 
2565
 
 
2566
                # store off initial comment if they have one
 
2567
                $sd{$profile}{$hat}{initial_comment} = $initial_comment
 
2568
                  if $initial_comment;
 
2569
                $initial_comment = "";
 
2570
 
 
2571
            } elsif (/^\s*\#/) {
 
2572
 
 
2573
                # we only currently handle initial comments
 
2574
                if (not $profile) {
 
2575
 
 
2576
                    # ignore vim syntax highlighting lines
 
2577
                    next if /^\s*\# vim:syntax/;
 
2578
 
 
2579
                    # ignore Last Modified: lines
 
2580
                    next if /^\s*\# Last Modified:/;
 
2581
                    $initial_comment .= "$_\n";
 
2582
                }
 
2583
            } else {
 
2584
 
 
2585
                # we hit something we don't understand in a profile...
 
2586
                return &$error_handler(sprintf(gettext('%s contains syntax errors.'), $file));
 
2587
            }
 
2588
        }
 
2589
 
 
2590
        # if we're still in a profile when we hit the end of the file, it's bad
 
2591
        if ($profile) {
 
2592
            return &$error_handler("Reached the end of $file while we were still inside the $profile profile.");
 
2593
        }
 
2594
 
 
2595
        close(SDPROF);
 
2596
    } else {
 
2597
        $DEBUGGING && debug "readprofile: can't read $file - skipping";
 
2598
    }
 
2599
}
 
2600
 
 
2601
sub escape ($) {
 
2602
    my $dangerous = shift;
 
2603
 
 
2604
    if ($dangerous =~ m/^"(.+)"$/) {
 
2605
        $dangerous = $1;
 
2606
    }
 
2607
    $dangerous =~ s/((?<!\\))"/$1\\"/g;
 
2608
    if ($dangerous =~ m/(\s|^$|")/) {
 
2609
        $dangerous = "\"$dangerous\"";
 
2610
    }
 
2611
 
 
2612
    return $dangerous;
 
2613
}
 
2614
 
 
2615
sub writeheader ($$$$) {
 
2616
    my ($fh, $profile, $hat, $indent) = @_;
 
2617
 
 
2618
    # deal with whitespace in profile names...
 
2619
    my $p = $profile;
 
2620
    $p = "\"$p\"" if $p =~ /\s/;
 
2621
 
 
2622
    if ($sd{$profile}{$hat}{flags}) {
 
2623
        print $fh "$p flags=($sd{$profile}{$hat}{flags}) {\n";
 
2624
    } else {
 
2625
        print $fh "$p {\n";
 
2626
    }
 
2627
}
 
2628
 
 
2629
sub writeincludes ($$$$) {
 
2630
    my ($fh, $profile, $hat, $indent) = @_;
 
2631
 
 
2632
    # dump out the includes
 
2633
    if (exists $sd{$profile}{$hat}{include}) {
 
2634
        for my $include (sort keys %{ $sd{$profile}{$hat}{include} }) {
 
2635
            print $fh "$indent  #include <$include>\n";
 
2636
        }
 
2637
        print $fh "\n" if keys %{ $sd{$profile}{$hat}{include} };
 
2638
    }
 
2639
}
 
2640
 
 
2641
sub writecapabilities ($$$$) {
 
2642
    my ($fh, $profile, $hat, $indent) = @_;
 
2643
 
 
2644
    # dump out the capability entries...
 
2645
    if (exists $sd{$profile}{$hat}{capability}) {
 
2646
        for my $capability (sort keys %{ $sd{$profile}{$hat}{capability} }) {
 
2647
            print $fh "$indent  capability $capability,\n";
 
2648
        }
 
2649
        print $fh "\n" if keys %{ $sd{$profile}{$hat}{capability} };
 
2650
    }
 
2651
}
 
2652
 
 
2653
sub writenetdomain ($$$$) {
 
2654
    my ($fh, $profile, $hat, $indent) = @_;
 
2655
 
 
2656
    # dump out the netdomain entries...
 
2657
    if (exists $sd{$profile}{$hat}{netdomain}) {
 
2658
        for my $nd (sort @{ $sd{$profile}{$hat}{netdomain} }) {
 
2659
            print $fh "$indent  $nd,\n";
 
2660
        }
 
2661
        print $fh "\n" if @{ $sd{$profile}{$hat}{netdomain} };
 
2662
    }
 
2663
}
 
2664
 
 
2665
sub writepaths ($$$$) {
 
2666
    my ($fh, $profile, $hat, $indent) = @_;
 
2667
 
 
2668
    if (exists $sd{$profile}{$hat}{path}) {
 
2669
        for my $path (sort keys %{ $sd{$profile}{$hat}{path} }) {
 
2670
            my $mode = $sd{$profile}{$hat}{path}{$path};
 
2671
 
 
2672
            # strip out any fake access() modes that might have slipped through
 
2673
            $mode =~ s/X//g;
 
2674
 
 
2675
            # deal with whitespace in path names
 
2676
            if ($path =~ /\s/) {
 
2677
                print $fh "$indent  \"$path\" $mode,\n";
 
2678
            } else {
 
2679
                print $fh "$indent  $path $mode,\n";
 
2680
            }
 
2681
        }
 
2682
    }
 
2683
}
 
2684
 
 
2685
sub writepiece ($$) {
 
2686
    my ($sdprof, $profile) = @_;
 
2687
 
 
2688
    writeheader($sdprof, $profile, $profile, "");
 
2689
    writeincludes($sdprof, $profile, $profile, "");
 
2690
    writecapabilities($sdprof, $profile, $profile, "");
 
2691
    writenetdomain($sdprof, $profile, $profile, "");
 
2692
    writepaths($sdprof, $profile, $profile, "");
 
2693
 
 
2694
    for my $hat (grep { $_ ne $profile } sort keys %{ $sd{$profile} }) {
 
2695
 
 
2696
        # deal with whitespace in profile names...
 
2697
        my $h = $hat;
 
2698
        $h = "\"$h\"" if $h =~ /\s/;
 
2699
 
 
2700
        if ($sd{$profile}{$hat}{flags}) {
 
2701
            print $sdprof "\n  ^$h flags=($sd{$profile}{$hat}{flags}) {\n";
 
2702
        } else {
 
2703
            print $sdprof "\n  ^$h {\n";
 
2704
        }
 
2705
 
 
2706
        writeincludes($sdprof, $profile, $hat, "  ");
 
2707
        writecapabilities($sdprof, $profile, $hat, "  ");
 
2708
        writenetdomain($sdprof, $profile, $hat, "  ");
 
2709
        writepaths($sdprof, $profile, $hat, "  ");
 
2710
 
 
2711
        print $sdprof "  }\n";
 
2712
    }
 
2713
 
 
2714
    print $sdprof "}\n";
 
2715
}
 
2716
 
 
2717
sub writeprofile ($) {
 
2718
    my $profile = shift;
 
2719
 
 
2720
    UI_Info(sprintf(gettext('Writing updated profile for %s.'), $profile));
 
2721
 
 
2722
    my $filename = getprofilefilename($profile);
 
2723
 
 
2724
    open(SDPROF, ">$filename")
 
2725
      or fatal_error "Can't write new AppArmor profile $filename: $!";
 
2726
 
 
2727
    # stick in a vim mode line to turn on AppArmor syntax highlighting
 
2728
    print SDPROF "# vim:syntax=apparmor\n";
 
2729
 
 
2730
    # keep track of when the file was last updated
 
2731
    print SDPROF "# Last Modified: " . localtime(time) . "\n";
 
2732
 
 
2733
    # print out initial comment
 
2734
    if ($sd{$profile}{$profile}{initial_comment}) {
 
2735
        $sd{$profile}{$profile}{initial_comment} =~ s/\\n/\n/g;
 
2736
        print SDPROF $sd{$profile}{$profile}{initial_comment};
 
2737
        print SDPROF "\n";
 
2738
    }
 
2739
 
 
2740
    # dump variables defined in this file
 
2741
    if ($variables{$filename}) {
 
2742
        for my $var (sort keys %{ $variables{$filename} }) {
 
2743
            if ($var =~ m/^@/) {
 
2744
                my @values = sort @{ $variables{$filename}{$var} };
 
2745
                @values = map { escape($_) } @values;
 
2746
                my $values = join(" ", @values);
 
2747
                print SDPROF "$var = ";
 
2748
                print SDPROF $values;
 
2749
            } elsif ($var =~ m/^\$/) {
 
2750
                print SDPROF "$var = ";
 
2751
                print SDPROF ${ $variables{$filename}{$var} };
 
2752
            } elsif ($var =~ m/^\#/) {
 
2753
                my $inc = $var;
 
2754
                $inc =~ s/^\#//;
 
2755
                print SDPROF "#include <$inc>";
 
2756
            }
 
2757
            print SDPROF "\n";
 
2758
        }
 
2759
    }
 
2760
 
 
2761
    print SDPROF "\n";
 
2762
 
 
2763
    writepiece(\*SDPROF, $profile);
 
2764
 
 
2765
    close(SDPROF);
 
2766
}
 
2767
 
 
2768
sub getprofileflags {
 
2769
    my $filename = shift;
 
2770
 
 
2771
    my $flags = "enforce";
 
2772
 
 
2773
    if (open(PROFILE, "$filename")) {
 
2774
        while (<PROFILE>) {
 
2775
            if (m/^\s*\/\S+\s+(flags=\(.+\)\s+)*{\s*$/) {
 
2776
                $flags = $1;
 
2777
                close(PROFILE);
 
2778
                $flags =~ s/flags=\((.+)\)/$1/;
 
2779
                return $flags;
 
2780
            }
 
2781
        }
 
2782
        close(PROFILE);
 
2783
    }
 
2784
 
 
2785
    return $flags;
 
2786
}
 
2787
 
 
2788
sub matchliteral {
 
2789
    my ($sd_regexp, $literal) = @_;
 
2790
 
 
2791
    my $p_regexp = convert_regexp($sd_regexp);
 
2792
 
 
2793
    # check the log entry against our converted regexp...
 
2794
    my $matches = eval { $literal =~ /^$p_regexp$/; };
 
2795
 
 
2796
    # doesn't match if we've got a broken regexp
 
2797
    return undef if $@;
 
2798
 
 
2799
    return $matches;
 
2800
}
 
2801
 
 
2802
sub reload ($) {
 
2803
    my $bin = shift;
 
2804
 
 
2805
    # don't try to reload profile if AppArmor is not running
 
2806
    return unless check_for_subdomain();
 
2807
 
 
2808
    # don't reload the profile if the corresponding executable doesn't exist
 
2809
    my $fqdbin = findexecutable($bin) or return;
 
2810
 
 
2811
    my $filename = getprofilefilename($fqdbin);
 
2812
 
 
2813
    system("/bin/cat '$filename' | $parser -I$profiledir -r >/dev/null 2>&1");
 
2814
}
 
2815
 
 
2816
sub loadinclude {
 
2817
    my $which         = shift;
 
2818
    my $error_handler = shift;
 
2819
 
 
2820
    # don't bother loading it again if we already have
 
2821
    return 0 if $include{$which};
 
2822
 
 
2823
    my @loadincludes = ($which);
 
2824
    while (my $incfile = shift @loadincludes) {
 
2825
 
 
2826
        # load the include from the directory we found earlier...
 
2827
        open(INCLUDE, "$profiledir/$incfile")
 
2828
          or fatal_error "Can't find include file $incfile: $!";
 
2829
 
 
2830
        while (<INCLUDE>) {
 
2831
            chomp;
 
2832
 
 
2833
            if (/^\s*(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*=\s*(true|false)\s*$/i) {
 
2834
                # boolean definition
 
2835
            } elsif (/^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\+=\s*(.+)\s*$/) {
 
2836
                # variable additions
 
2837
            } elsif (/^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*=\s*(.+)\s*$/) {
 
2838
                # variable definitions
 
2839
            } elsif (m/^\s*if\s+(not\s+)?(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*\{\s*$/) {
 
2840
                # conditional -- boolean
 
2841
            } elsif (m/^\s*if\s+(not\s+)?defined\s+(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\{\s*$/) {
 
2842
                # conditional -- variable defined
 
2843
            } elsif (m/^\s*if\s+(not\s+)?defined\s+(\$\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\{\s*$/) {
 
2844
                # conditional -- boolean defined
 
2845
            } elsif (m/^\s*\}\s*$/) {
 
2846
                # end of a profile or conditional
 
2847
            } elsif (m/^\s*([\"\@\/].*)\s+(\S+)\s*,\s*$/) {
 
2848
                # path entry
 
2849
 
 
2850
                my ($path, $mode) = ($1, $2);
 
2851
 
 
2852
                # strip off any trailing spaces.
 
2853
                $path =~ s/\s+$//;
 
2854
 
 
2855
                $path = $1 if $path =~ /^"(.+)"$/;
 
2856
 
 
2857
                # make sure they don't have broken regexps in the profile
 
2858
                my $p_re = convert_regexp($path);
 
2859
                eval { "foo" =~ m/^$p_re$/; };
 
2860
                if ($@) {
 
2861
                    return &$error_handler(sprintf(gettext('Include file %s contains invalid regexp %s.'), $incfile, $path));
 
2862
                }
 
2863
 
 
2864
                $include{$incfile}{path}{$path} = $mode;
 
2865
            } elsif (/^\s*capability\s+(.+)\s*,\s*$/) {
 
2866
 
 
2867
                my $capability = $1;
 
2868
                $include{$incfile}{capability}{$capability} = 1;
 
2869
 
 
2870
            } elsif (/^\s*#include <(.+)>\s*$/) {
 
2871
                # include stuff
 
2872
 
 
2873
                my $newinclude = $1;
 
2874
                push @loadincludes, $newinclude unless $include{$newinclude};
 
2875
                $include{$incfile}{include}{$newinclude} = 1;
 
2876
 
 
2877
            } elsif (/^\s*(tcp_connect|tcp_accept|udp_send|udp_receive)/) {
 
2878
            } else {
 
2879
 
 
2880
                # we don't care about blank lines or comments
 
2881
                next if /^\s*$/;
 
2882
                next if /^\s*\#/;
 
2883
 
 
2884
                # we hit something we don't understand in a profile...
 
2885
                return &$error_handler(sprintf(gettext('Include file %s contains syntax errors or is not a valid #include file.'), $incfile));
 
2886
            }
 
2887
        }
 
2888
        close(INCLUDE);
 
2889
    }
 
2890
 
 
2891
    return 0;
 
2892
}
 
2893
 
 
2894
sub rematchfrag {
 
2895
    my ($frag, $path) = @_;
 
2896
 
 
2897
    my $combinedmode = "";
 
2898
    my @matches;
 
2899
 
 
2900
    for my $entry (keys %{ $frag->{path} }) {
 
2901
 
 
2902
        my $regexp = convert_regexp($entry);
 
2903
 
 
2904
        # check the log entry against our converted regexp...
 
2905
        if ($path =~ /^$regexp$/) {
 
2906
 
 
2907
            # regexp matches, add it's mode to the list to check against
 
2908
            $combinedmode .= $frag->{path}{$entry};
 
2909
            push @matches, $entry;
 
2910
        }
 
2911
    }
 
2912
 
 
2913
    return wantarray ? ($combinedmode, @matches) : $combinedmode;
 
2914
}
 
2915
 
 
2916
sub matchincludes {
 
2917
    my ($frag, $path) = @_;
 
2918
 
 
2919
    my $combinedmode = "";
 
2920
    my @matches;
 
2921
 
 
2922
    # scan the include fragments for this profile looking for matches
 
2923
    my @includelist = keys %{ $frag->{include} };
 
2924
    while (my $include = shift @includelist) {
 
2925
        loadinclude($include, \&fatal_error);
 
2926
        my ($cm, @m) = rematchfrag($include{$include}, $path);
 
2927
        if ($cm) {
 
2928
            $combinedmode .= $cm;
 
2929
            push @matches, @m;
 
2930
        }
 
2931
 
 
2932
        # check if a literal version is in the current include fragment
 
2933
        if ($include{$include}{path}{$path}) {
 
2934
            $combinedmode .= $include{$include}{path}{$path};
 
2935
        }
 
2936
 
 
2937
        # if this fragment includes others, check them too
 
2938
        if (keys %{ $include{$include}{include} }) {
 
2939
            push @includelist, keys %{ $include{$include}{include} };
 
2940
        }
 
2941
    }
 
2942
 
 
2943
    return wantarray ? ($combinedmode, @matches) : $combinedmode;
 
2944
}
 
2945
 
 
2946
sub matchinclude {
 
2947
    my ($incname, $path) = @_;
 
2948
 
 
2949
    my $combinedmode = "";
 
2950
    my @matches;
 
2951
 
 
2952
    # scan the include fragments for this profile looking for matches
 
2953
    my @includelist = ($incname);
 
2954
    while (my $include = shift @includelist) {
 
2955
        my ($cm, @m) = rematchfrag($include{$include}, $path);
 
2956
        if ($cm) {
 
2957
            $combinedmode .= $cm;
 
2958
            push @matches, @m;
 
2959
        }
 
2960
 
 
2961
        # check if a literal version is in the current include fragment
 
2962
        if ($include{$include}{path}{$path}) {
 
2963
            $combinedmode .= $include{$include}{path}{$path};
 
2964
        }
 
2965
 
 
2966
        # if this fragment includes others, check them too
 
2967
        if (keys %{ $include{$include}{include} }) {
 
2968
            push @includelist, keys %{ $include{$include}{include} };
 
2969
        }
 
2970
    }
 
2971
 
 
2972
    if ($combinedmode) {
 
2973
        return wantarray ? ($combinedmode, @matches) : $combinedmode;
 
2974
    } else {
 
2975
        return;
 
2976
    }
 
2977
}
 
2978
 
 
2979
sub readconfig () {
 
2980
 
 
2981
    my $which;
 
2982
 
 
2983
    if (open(LPCONF, "$confdir/logprof.conf")) {
 
2984
        while (<LPCONF>) {
 
2985
            chomp;
 
2986
 
 
2987
            next if /^\s*#/;
 
2988
 
 
2989
            if (m/^\[(\S+)\]/) {
 
2990
                $which = $1;
 
2991
            } elsif (m/^\s*(\S+)\s*=\s*(.+)\s*$/) {
 
2992
                my ($key, $value) = ($1, $2);
 
2993
                if ($which eq "defaulthat") {
 
2994
                    $defaulthat{$key} = $value;
 
2995
                } elsif ($which eq "qualifiers") {
 
2996
                    $qualifiers{$key} = $value;
 
2997
                } elsif ($which eq "globs") {
 
2998
                    $globmap{$key} = $value;
 
2999
                } elsif ($which eq "required_hats") {
 
3000
                    $required_hats{$key} = $value;
 
3001
                }
 
3002
            } elsif (m/^\s*(\S+)\s*$/) {
 
3003
                my $val = $1;
 
3004
                if ($which eq "custom_includes") {
 
3005
                    push @custom_includes, $val;
 
3006
                }
 
3007
            }
 
3008
        }
 
3009
        close(LPCONF);
 
3010
    }
 
3011
}
 
3012
 
 
3013
sub loadincludes {
 
3014
    if (opendir(SDDIR, $profiledir)) {
 
3015
        my @incdirs = grep { (!/^\./) && (-d "$profiledir/$_") } readdir(SDDIR);
 
3016
        close(SDDIR);
 
3017
 
 
3018
        while (my $id = shift @incdirs) {
 
3019
            if (opendir(SDDIR, "$profiledir/$id")) {
 
3020
                for my $path (readdir(SDDIR)) {
 
3021
                    chomp($path);
 
3022
                    next if isSkippableFile($path);
 
3023
                    if (-f "$profiledir/$id/$path") {
 
3024
                        my $file = "$id/$path";
 
3025
                        $file =~ s/$profiledir\///;
 
3026
                        loadinclude($file, \&fatal_error);
 
3027
                    } elsif (-d "$id/$path") {
 
3028
                        push @incdirs, "$id/$path";
 
3029
                    }
 
3030
                }
 
3031
                closedir(SDDIR);
 
3032
            }
 
3033
        }
 
3034
    }
 
3035
}
 
3036
 
 
3037
sub globcommon ($) {
 
3038
    my $path = shift;
 
3039
 
 
3040
    my @globs;
 
3041
 
 
3042
    # glob library versions in both foo-5.6.so and baz.so.9.2 form
 
3043
    if ($path =~ m/[\d\.]+\.so$/ || $path =~ m/\.so\.[\d\.]+$/) {
 
3044
        my $libpath = $path;
 
3045
        $libpath =~ s/[\d\.]+\.so$/*.so/;
 
3046
        $libpath =~ s/\.so\.[\d\.]+$/.so.*/;
 
3047
        push @globs, $libpath if $libpath ne $path;
 
3048
    }
 
3049
 
 
3050
    for my $glob (keys %globmap) {
 
3051
        if ($path =~ /$glob/) {
 
3052
            my $globbedpath = $path;
 
3053
            $globbedpath =~ s/$glob/$globmap{$glob}/g;
 
3054
            push @globs, $globbedpath if $globbedpath ne $path;
 
3055
        }
 
3056
    }
 
3057
 
 
3058
    if (wantarray) {
 
3059
        return sort { length($b) <=> length($a) } uniq(@globs);
 
3060
    } else {
 
3061
        my @list = sort { length($b) <=> length($a) } uniq(@globs);
 
3062
        return $list[$#list];
 
3063
    }
 
3064
}
 
3065
 
 
3066
# this is an ugly, nasty function that attempts to see if one regexp
 
3067
# is a subset of another regexp
 
3068
sub matchregexp ($$) {
 
3069
    my ($new, $old) = @_;
 
3070
 
 
3071
    # bail out if old pattern has {foo,bar,baz} stuff in it
 
3072
    return undef if $old =~ /\{.*(\,.*)*\}/;
 
3073
 
 
3074
    # are there any regexps at all in the old pattern?
 
3075
    if ($old =~ /\[.+\]/ or $old =~ /\*/ or $old =~ /\?/) {
 
3076
 
 
3077
        # convert {foo,baz} to (foo|baz)
 
3078
        $new =~ y/\{\}\,/\(\)\|/ if $new =~ /\{.*\,.*\}/;
 
3079
 
 
3080
        # \001 == SD_GLOB_RECURSIVE
 
3081
        # \002 == SD_GLOB_SIBLING
 
3082
 
 
3083
        $new =~ s/\*\*/\001/g;
 
3084
        $new =~ s/\*/\002/g;
 
3085
 
 
3086
        $old =~ s/\*\*/\001/g;
 
3087
        $old =~ s/\*/\002/g;
 
3088
 
 
3089
        # strip common prefix
 
3090
        my $prefix = commonprefix($new, $old);
 
3091
        if ($prefix) {
 
3092
 
 
3093
            # make sure we don't accidentally gobble up a trailing * or **
 
3094
            $prefix =~ s/(\001|\002)$//;
 
3095
            $new    =~ s/^$prefix//;
 
3096
            $old    =~ s/^$prefix//;
 
3097
        }
 
3098
 
 
3099
        # strip common suffix
 
3100
        my $suffix = commonsuffix($new, $old);
 
3101
        if ($suffix) {
 
3102
 
 
3103
            # make sure we don't accidentally gobble up a leading * or **
 
3104
            $suffix =~ s/^(\001|\002)//;
 
3105
            $new    =~ s/$suffix$//;
 
3106
            $old    =~ s/$suffix$//;
 
3107
        }
 
3108
 
 
3109
        # if we boiled the differences down to a ** in the new entry, it matches
 
3110
        # whatever's in the old entry
 
3111
        return 1 if $new eq "\001";
 
3112
 
 
3113
        # if we've paired things down to a * in new, old matches if there are no
 
3114
        # slashes left in the path
 
3115
        return 1 if ($new eq "\002" && $old =~ /^[^\/]+$/);
 
3116
 
 
3117
        # we'll bail out if we have more globs in the old version
 
3118
        return undef if $old =~ /\001|\002/;
 
3119
 
 
3120
        # see if we can match * globs in new against literal elements in old
 
3121
        $new =~ s/\002/[^\/]*/g;
 
3122
 
 
3123
        return 1 if $old =~ /^$new$/;
 
3124
 
 
3125
    } else {
 
3126
 
 
3127
        my $new_regexp = convert_regexp($new);
 
3128
 
 
3129
        # check the log entry against our converted regexp...
 
3130
        return 1 if $old =~ /^$new_regexp$/;
 
3131
 
 
3132
    }
 
3133
 
 
3134
    return undef;
 
3135
}
 
3136
 
 
3137
sub combine_name($$) { return ($_[0] eq $_[1]) ? $_[0] : "$_[0]^$_[1]"; }
 
3138
sub split_name ($) { my ($p, $h) = split(/\^/, $_[0]); $h ||= $p; ($p, $h); }
 
3139
 
 
3140
##########################
 
3141
#
 
3142
# prompt_user($headers, $functions, $default, $options, $selected);
 
3143
#
 
3144
# $headers:
 
3145
#   a required arrayref made up of "key, value" pairs in the order you'd
 
3146
#   like them displayed to user
 
3147
#
 
3148
# $functions:
 
3149
#   a required arrayref of the different options to display at the bottom
 
3150
#   of the prompt like "(A)llow", "(D)eny", and "Ba(c)on".  the character
 
3151
#   contained by ( and ) will be used as the key to select the specified
 
3152
#   option.
 
3153
#
 
3154
# $default:
 
3155
#   a required character which is the default "key" to enter when they
 
3156
#   just hit enter
 
3157
#
 
3158
# $options:
 
3159
#   an optional arrayref of the choices like the glob suggestions to be
 
3160
#   presented to the user
 
3161
#
 
3162
# $selected:
 
3163
#   specifies which option is currently selected
 
3164
#
 
3165
# when prompt_user() is called without an $options list, it returns a
 
3166
# single value which is the key for the specified "function".
 
3167
#
 
3168
# when prompt_user() is called with an $options list, it returns an array
 
3169
# of two elements, the key for the specified function as well as which
 
3170
# option was currently selected
 
3171
#######################################################################
 
3172
 
 
3173
sub Text_PromptUser ($) {
 
3174
    my $question = shift;
 
3175
 
 
3176
    my @headers   = (@{ $question->{headers} });
 
3177
    my @functions = (@{ $question->{functions} });
 
3178
 
 
3179
    my $default  = $question->{default};
 
3180
    my $options  = $question->{options};
 
3181
    my $selected = $question->{selected};
 
3182
 
 
3183
    my $helptext = $question->{helptext};
 
3184
 
 
3185
    push @functions, "CMD_HELP" if $helptext;
 
3186
 
 
3187
    my %keys;
 
3188
    my @menu_items;
 
3189
    for my $cmd (@functions) {
 
3190
 
 
3191
        # make sure we know about this particular command
 
3192
        my $cmdmsg = "PromptUser: " . gettext("Unknown command") . " $cmd";
 
3193
        fatal_error $cmdmsg unless $CMDS{$cmd};
 
3194
 
 
3195
        # grab the localized text to use for the menu for this command
 
3196
        my $menutext = gettext($CMDS{$cmd});
 
3197
 
 
3198
        # figure out what the hotkey for this menu item is
 
3199
        my $menumsg = "PromptUser: " . gettext("Invalid hotkey in") . " '$menutext'";
 
3200
        $menutext =~ /\((\S)\)/ or fatal_error $menumsg;
 
3201
 
 
3202
        # we want case insensitive comparisons so we'll force things to
 
3203
        # lowercase
 
3204
        my $key = lc($1);
 
3205
 
 
3206
        # check if we're already using this hotkey for this prompt
 
3207
        my $hotkeymsg = "PromptUser: " . gettext("Duplicate hotkey for") . " $cmd: $menutext";
 
3208
        fatal_error $hotkeymsg if $keys{$key};
 
3209
 
 
3210
        # keep track of which command they're picking if they hit this hotkey
 
3211
        $keys{$key} = $cmd;
 
3212
 
 
3213
        if ($default && $default eq $cmd) {
 
3214
            $menutext = "[$menutext]";
 
3215
        }
 
3216
 
 
3217
        push @menu_items, $menutext;
 
3218
    }
 
3219
 
 
3220
    # figure out the key for the default option
 
3221
    my $default_key;
 
3222
    if ($default && $CMDS{$default}) {
 
3223
        my $defaulttext = gettext($CMDS{$default});
 
3224
 
 
3225
        # figure out what the hotkey for this menu item is
 
3226
        my $defmsg = "PromptUser: " . gettext("Invalid hotkey in default item") . " '$defaulttext'";
 
3227
        $defaulttext =~ /\((\S)\)/ or fatal_error $defmsg;
 
3228
 
 
3229
        # we want case insensitive comparisons so we'll force things to
 
3230
        # lowercase
 
3231
        $default_key = lc($1);
 
3232
 
 
3233
        my $defkeymsg = "PromptUser: " . gettext("Invalid default") . " $default";
 
3234
        fatal_error $defkeymsg unless $keys{$default_key};
 
3235
    }
 
3236
 
 
3237
    my $widest = 0;
 
3238
    my @poo    = @headers;
 
3239
    while (my $header = shift @poo) {
 
3240
        my $value = shift @poo;
 
3241
        $widest = length($header) if length($header) > $widest;
 
3242
    }
 
3243
    $widest++;
 
3244
 
 
3245
    my $format = '%-' . $widest . "s \%s\n";
 
3246
 
 
3247
    my $function_regexp = '^(';
 
3248
    $function_regexp .= join("|", keys %keys);
 
3249
    $function_regexp .= '|\d' if $options;
 
3250
    $function_regexp .= ')$';
 
3251
 
 
3252
    my $ans = "XXXINVALIDXXX";
 
3253
    while ($ans !~ /$function_regexp/i) {
 
3254
 
 
3255
        # build up the prompt...
 
3256
        my $prompt = "\n";
 
3257
        my @poo    = @headers;
 
3258
        while (my $header = shift @poo) {
 
3259
            my $value = shift @poo;
 
3260
            $prompt .= sprintf($format, "$header:", $value);
 
3261
        }
 
3262
        $prompt .= "\n";
 
3263
        if ($options) {
 
3264
            for (my $i = 0; $options->[$i]; $i++) {
 
3265
                my $f = ($selected == $i) ? ' [%d - %s]' : '  %d - %s ';
 
3266
                $prompt .= sprintf("$f\n", $i + 1, $options->[$i]);
 
3267
            }
 
3268
            $prompt .= "\n";
 
3269
        }
 
3270
        $prompt .= join(" / ", @menu_items);
 
3271
        print "$prompt\n";
 
3272
 
 
3273
        # get their input...
 
3274
        $ans = lc(getkey);
 
3275
 
 
3276
        # pick the default if they hit return...
 
3277
        $ans = $default_key if ord($ans) == 10;
 
3278
 
 
3279
        # ugly code to handle escape sequences so you can up/down in the list
 
3280
        if (ord($ans) == 27) {
 
3281
            $ans = getkey;
 
3282
            if (ord($ans) == 91) {
 
3283
                $ans = getkey;
 
3284
                if (ord($ans) == 65) {
 
3285
                    if ($options) {
 
3286
                        if ($selected > 0) {
 
3287
                            $ans = $selected;
 
3288
                        } else {
 
3289
                            $ans = "again";
 
3290
                        }
 
3291
                    } else {
 
3292
                        $ans = "again";
 
3293
                    }
 
3294
                } elsif (ord($ans) == 66) {
 
3295
                    if ($options) {
 
3296
                        if ($selected <= scalar(@$options)) {
 
3297
                            $ans = $selected + 2;
 
3298
                        } else {
 
3299
                            $ans = "again";
 
3300
                        }
 
3301
                    }
 
3302
                } else {
 
3303
                    $ans = "again";
 
3304
                }
 
3305
            } else {
 
3306
                $ans = "again";
 
3307
            }
 
3308
        }
 
3309
 
 
3310
        # handle option poo
 
3311
        if ($options && ($ans =~ /^\d$/)) {
 
3312
            if ($ans > 0 && $ans <= scalar(@$options)) {
 
3313
                $selected = $ans - 1;
 
3314
            }
 
3315
            $ans = "again";
 
3316
        }
 
3317
 
 
3318
        if ($keys{$ans} && $keys{$ans} eq "CMD_HELP") {
 
3319
            print "\n$helptext\n";
 
3320
            $ans = "again";
 
3321
        }
 
3322
    }
 
3323
 
 
3324
    # pull our command back from our hotkey map
 
3325
    $ans = $keys{$ans} if $keys{$ans};
 
3326
 
 
3327
#  if($options) {
 
3328
#    die "ERROR: not looking for array when options passed" unless wantarray;
 
3329
    if ($options) {
 
3330
        return ($ans, $options->[$selected]);
 
3331
    } else {
 
3332
        return ($ans, $selected);
 
3333
    }
 
3334
 
 
3335
#  } else {
 
3336
#    die "ERROR: looking for list when options not passed" if wantarray;
 
3337
#    return $ans;
 
3338
#  }
 
3339
}
 
3340
 
 
3341
unless (-x $ldd) {
 
3342
    $ldd = which("ldd") or fatal_error "Can't find ldd.";
 
3343
}
 
3344
 
 
3345
unless (-x $parser) {
 
3346
    $parser = which("apparmor_parser") || which("subdomain_parser")
 
3347
      or fatal_error "Can't find apparmor_parser.";
 
3348
}
 
3349
 
 
3350
1;
 
3351