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

Subject: apparmor-utils: Inherit flags in sub-profiles when generating profiles References: bnc#496204 When creating profiles with cx subprofiles, genprof will set the sub-profile in enforce mode. When genprof cycles multiple times, it prohibits the sub-profile from working correctly. e.g. # Last Modified: Mon Jan 24 13:52:26 2011 #include <tunables/global> /home/jeffm/mycat flags=(complain) { #include <abstractions/base> #include <abstractions/bash> #include <abstractions/consoles> /bin/bash ix, /bin/cat cx, /home/jeffm/mycat r, profile /bin/cat { #include <abstractions/base> /bin/cat r, /home/jeffm/mycat r, } } This patch allows sub-profiles to inherit the flags from the parent profile, which allows it to be created in complain mode (if appropriate). The temporary complain flags are cleaned up at genprof completion as expected. This issue was reported at: https://bugzilla.novell.com/show_bug.cgi?id=496204 Signed-off-by: Jeff Mahoney <jeffm@suse.com> Acked-By: Steve Beattie <sbeattie@ubuntu.com> Bug: https://launchpad.net/bugs/707092
6771 lines
216 KiB
Perl
Executable file
6771 lines
216 KiB
Perl
Executable file
# ----------------------------------------------------------------------
|
|
# Copyright (c) 2006 Novell, Inc. All Rights Reserved.
|
|
# Copyright (c) 2010 Canonical, Ltd.
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of version 2 of the GNU General Public
|
|
# License as published by the Free Software Foundation.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, contact Novell, Inc.
|
|
#
|
|
# To contact Novell about this file by physical or electronic mail,
|
|
# you may find current contact information at www.novell.com.
|
|
# ----------------------------------------------------------------------
|
|
|
|
package Immunix::AppArmor;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Carp;
|
|
use Cwd qw(cwd realpath);
|
|
use File::Basename;
|
|
use File::Temp qw/ tempfile tempdir /;
|
|
use Data::Dumper;
|
|
|
|
use Locale::gettext;
|
|
use POSIX;
|
|
use Storable qw(dclone);
|
|
|
|
use Term::ReadKey;
|
|
|
|
use Immunix::Severity;
|
|
use Immunix::Repository;
|
|
use Immunix::Config;
|
|
use LibAppArmor;
|
|
|
|
require Exporter;
|
|
our @ISA = qw(Exporter);
|
|
our @EXPORT = qw(
|
|
%sd
|
|
%qualifiers
|
|
%include
|
|
%helpers
|
|
|
|
$filename
|
|
$profiledir
|
|
$parser
|
|
$logger
|
|
$UI_Mode
|
|
$running_under_genprof
|
|
|
|
which
|
|
getprofilefilename
|
|
get_full_path
|
|
fatal_error
|
|
get_pager
|
|
|
|
getprofileflags
|
|
setprofileflags
|
|
complain
|
|
enforce
|
|
|
|
autodep
|
|
reload
|
|
|
|
UI_GetString
|
|
UI_GetFile
|
|
UI_YesNo
|
|
UI_ShortMessage
|
|
UI_LongMessage
|
|
|
|
UI_Important
|
|
UI_Info
|
|
UI_PromptUser
|
|
display_changes
|
|
getkey
|
|
|
|
do_logprof_pass
|
|
|
|
loadincludes
|
|
readprofile
|
|
readprofiles
|
|
writeprofile
|
|
serialize_profile
|
|
attach_profile_data
|
|
parse_repo_profile
|
|
activate_repo_profiles
|
|
|
|
check_for_subdomain
|
|
|
|
setup_yast
|
|
shutdown_yast
|
|
GetDataFromYast
|
|
SendDataToYast
|
|
|
|
checkProfileSyntax
|
|
checkIncludeSyntax
|
|
check_qualifiers
|
|
|
|
isSkippableFile
|
|
isSkippableDir
|
|
);
|
|
|
|
our $confdir = "/etc/apparmor";
|
|
|
|
our $running_under_genprof = 0;
|
|
|
|
our $DEBUGGING;
|
|
|
|
our $unimplemented_warning = 0;
|
|
|
|
# keep track of if we're running under yast or not - default to text mode
|
|
our $UI_Mode = "text";
|
|
|
|
our $sevdb;
|
|
|
|
# initialize Term::ReadLine if it's available
|
|
our $term;
|
|
eval {
|
|
require Term::ReadLine;
|
|
import Term::ReadLine;
|
|
$term = new Term::ReadLine 'AppArmor';
|
|
};
|
|
|
|
# initialize the local poo
|
|
setlocale(LC_MESSAGES, "")
|
|
unless defined(LC_MESSAGES);
|
|
textdomain("apparmor-utils");
|
|
|
|
# where do we get our log messages from?
|
|
our $filename;
|
|
|
|
our $cfg;
|
|
our $repo_cfg;
|
|
|
|
our $parser;
|
|
our $ldd;
|
|
our $logger;
|
|
our $profiledir;
|
|
our $extraprofiledir;
|
|
|
|
# we keep track of the included profile fragments with %include
|
|
my %include;
|
|
|
|
my %existing_profiles;
|
|
|
|
our $seenevents = 0;
|
|
|
|
|
|
# these are globs that the user specifically entered. we'll keep track of
|
|
# them so that if one later matches, we'll suggest it again.
|
|
our @userglobs;
|
|
|
|
### THESE VARIABLES ARE USED WITHIN LOGPROF
|
|
our %t;
|
|
our %transitions;
|
|
our %sd; # we keep track of the original profiles in %sd
|
|
our %original_sd;
|
|
our %extras; # inactive profiles from extras
|
|
|
|
my @log;
|
|
my %pid;
|
|
|
|
my %seen;
|
|
my %profilechanges;
|
|
my %prelog;
|
|
my %log;
|
|
my %changed;
|
|
my @created;
|
|
my %skip;
|
|
our %helpers; # we want to preserve this one between passes
|
|
|
|
### THESE VARIABLES ARE USED WITHIN LOGPROF
|
|
|
|
my %filelist; # file level stuff including variables in config files
|
|
|
|
my $AA_MAY_EXEC = 1;
|
|
my $AA_MAY_WRITE = 2;
|
|
my $AA_MAY_READ = 4;
|
|
my $AA_MAY_APPEND = 8;
|
|
my $AA_MAY_LINK = 16;
|
|
my $AA_MAY_LOCK = 32;
|
|
my $AA_EXEC_MMAP = 64;
|
|
my $AA_EXEC_UNSAFE = 128;
|
|
my $AA_EXEC_INHERIT = 256;
|
|
my $AA_EXEC_UNCONFINED = 512;
|
|
my $AA_EXEC_PROFILE = 1024;
|
|
my $AA_EXEC_CHILD = 2048;
|
|
my $AA_EXEC_NT = 4096;
|
|
my $AA_LINK_SUBSET = 8192;
|
|
|
|
my $AA_OTHER_SHIFT = 14;
|
|
my $AA_USER_MASK = 16384 -1;
|
|
|
|
my $AA_EXEC_TYPE = $AA_MAY_EXEC | $AA_EXEC_UNSAFE | $AA_EXEC_INHERIT |
|
|
$AA_EXEC_UNCONFINED | $AA_EXEC_PROFILE | $AA_EXEC_CHILD | $AA_EXEC_NT;
|
|
|
|
my $ALL_AA_EXEC_TYPE = $AA_EXEC_TYPE;
|
|
|
|
my %MODE_HASH = (
|
|
x => $AA_MAY_EXEC,
|
|
X => $AA_MAY_EXEC,
|
|
w => $AA_MAY_WRITE,
|
|
W => $AA_MAY_WRITE,
|
|
r => $AA_MAY_READ,
|
|
R => $AA_MAY_READ,
|
|
a => $AA_MAY_APPEND,
|
|
A => $AA_MAY_APPEND,
|
|
l => $AA_MAY_LINK,
|
|
L => $AA_MAY_LINK,
|
|
k => $AA_MAY_LOCK,
|
|
K => $AA_MAY_LOCK,
|
|
m => $AA_EXEC_MMAP,
|
|
M => $AA_EXEC_MMAP,
|
|
# Unsafe => 128,
|
|
i => $AA_EXEC_INHERIT,
|
|
I => $AA_EXEC_INHERIT,
|
|
u => $AA_EXEC_UNCONFINED + $AA_EXEC_UNSAFE, # U + Unsafe
|
|
U => $AA_EXEC_UNCONFINED,
|
|
p => $AA_EXEC_PROFILE + $AA_EXEC_UNSAFE, # P + Unsafe
|
|
P => $AA_EXEC_PROFILE,
|
|
c => $AA_EXEC_CHILD + $AA_EXEC_UNSAFE,
|
|
C => $AA_EXEC_CHILD,
|
|
n => $AA_EXEC_NT + $AA_EXEC_UNSAFE,
|
|
N => $AA_EXEC_NT,
|
|
);
|
|
|
|
|
|
# Currently only used by netdomain but there's no reason it couldn't
|
|
# be extended to support other types.
|
|
my %operation_types = (
|
|
|
|
# Old socket names
|
|
"socket_create", => "net",
|
|
"socket_post_create" => "net",
|
|
"socket_bind" => "net",
|
|
"socket_connect" => "net",
|
|
"socket_listen" => "net",
|
|
"socket_accept" => "net",
|
|
"socket_sendmsg" => "net",
|
|
"socket_recvmsg" => "net",
|
|
"socket_getsockname" => "net",
|
|
"socket_getpeername" => "net",
|
|
"socket_getsockopt" => "net",
|
|
"socket_setsockopt" => "net",
|
|
"socket_shutdown" => "net",
|
|
|
|
# New socket names
|
|
"create" => "net",
|
|
"post_create" => "net",
|
|
"bind" => "net",
|
|
"connect" => "net",
|
|
"listen" => "net",
|
|
"accept" => "net",
|
|
"sendmsg" => "net",
|
|
"recvmsg" => "net",
|
|
"getsockname" => "net",
|
|
"getpeername" => "net",
|
|
"getsockopt" => "net",
|
|
"setsockopt" => "net",
|
|
"sock_shutdown" => "net",
|
|
);
|
|
|
|
sub optype($) {
|
|
my $op = shift;
|
|
my $type = $operation_types{$op};
|
|
|
|
return "unknown" if !defined($type);
|
|
return $type;
|
|
}
|
|
|
|
sub debug ($) {
|
|
my $message = shift;
|
|
chomp($message);
|
|
|
|
print DEBUG "$message\n" if $DEBUGGING;
|
|
}
|
|
|
|
my %arrows = ( A => "UP", B => "DOWN", C => "RIGHT", D => "LEFT" );
|
|
|
|
sub getkey() {
|
|
# change to raw mode
|
|
ReadMode(4);
|
|
|
|
my $key = ReadKey(0);
|
|
|
|
# decode arrow key control sequences
|
|
if ($key eq "\x1B") {
|
|
$key = ReadKey(0);
|
|
if ($key eq "[") {
|
|
$key = ReadKey(0);
|
|
if ($arrows{$key}) {
|
|
$key = $arrows{$key};
|
|
}
|
|
}
|
|
}
|
|
|
|
# return to cooked mode
|
|
ReadMode(0);
|
|
return $key;
|
|
}
|
|
|
|
BEGIN {
|
|
# set things up to log extra info if they want...
|
|
if ($ENV{LOGPROF_DEBUG}) {
|
|
$DEBUGGING = 1;
|
|
open(DEBUG, ">>$ENV{LOGPROF_DEBUG}");
|
|
my $oldfd = select(DEBUG);
|
|
$| = 1;
|
|
select($oldfd);
|
|
} else {
|
|
$DEBUGGING = 0;
|
|
}
|
|
}
|
|
|
|
END {
|
|
$DEBUGGING && debug "Exiting...";
|
|
|
|
# close the debug log if necessary
|
|
close(DEBUG) if $DEBUGGING;
|
|
}
|
|
|
|
# returns true if the specified program contains references to LD_PRELOAD or
|
|
# LD_LIBRARY_PATH to give the PX/UX code better suggestions
|
|
sub check_for_LD_XXX ($) {
|
|
my $file = shift;
|
|
|
|
return undef unless -f $file;
|
|
|
|
# limit our checking to programs/scripts under 10k to speed things up a bit
|
|
my $size = -s $file;
|
|
return undef unless ($size && $size < 10000);
|
|
|
|
my $found = undef;
|
|
if (open(F, $file)) {
|
|
while (<F>) {
|
|
$found = 1 if /LD_(PRELOAD|LIBRARY_PATH)/;
|
|
}
|
|
close(F);
|
|
}
|
|
|
|
return $found;
|
|
}
|
|
|
|
sub fatal_error ($) {
|
|
my $message = shift;
|
|
|
|
my $details = "$message\n";
|
|
|
|
if ($DEBUGGING) {
|
|
|
|
# we'll include the stack backtrace if we're debugging...
|
|
$details = Carp::longmess($message);
|
|
|
|
# write the error to the log
|
|
print DEBUG $details;
|
|
}
|
|
|
|
# we'll just shoot ourselves in the head if it was one of the yast
|
|
# interface functions that ran into an error. it gets really ugly if
|
|
# the yast frontend goes away and we try to notify the user of that
|
|
# problem by trying to send the yast frontend a pretty dialog box
|
|
my $caller = (caller(1))[3];
|
|
|
|
exit 1 if defined($caller) && $caller =~ /::(Send|Get)Data(To|From)Yast$/;
|
|
|
|
# tell the user what the hell happened
|
|
UI_Important($details);
|
|
|
|
# make sure the frontend exits cleanly...
|
|
shutdown_yast();
|
|
|
|
# die a horrible flaming death
|
|
exit 1;
|
|
}
|
|
|
|
sub setup_yast() {
|
|
|
|
# set up the yast connection if we're running under yast...
|
|
if ($ENV{YAST_IS_RUNNING}) {
|
|
|
|
# load the yast module if available.
|
|
eval { require ycp; };
|
|
unless ($@) {
|
|
import ycp;
|
|
|
|
$UI_Mode = "yast";
|
|
|
|
# let the frontend know that we're starting
|
|
SendDataToYast({
|
|
type => "initial_handshake",
|
|
status => "backend_starting"
|
|
});
|
|
|
|
# see if the frontend is just starting up also...
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
unless ($yarg
|
|
&& (ref($yarg) eq "HASH")
|
|
&& ($yarg->{type} eq "initial_handshake")
|
|
&& ($yarg->{status} eq "frontend_starting"))
|
|
{
|
|
|
|
# something's broken, die a horrible, painful death
|
|
fatal_error "Yast frontend is out of sync from backend agent.";
|
|
}
|
|
$DEBUGGING && debug "Initial handshake ok";
|
|
|
|
# the yast connection seems to be working okay
|
|
return 1;
|
|
}
|
|
|
|
}
|
|
|
|
# couldn't init yast
|
|
return 0;
|
|
}
|
|
|
|
sub shutdown_yast() {
|
|
if ($UI_Mode eq "yast") {
|
|
SendDataToYast({ type => "final_shutdown" });
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
}
|
|
}
|
|
|
|
sub check_for_subdomain () {
|
|
|
|
my ($support_subdomainfs, $support_securityfs);
|
|
if (open(MOUNTS, "/proc/filesystems")) {
|
|
while (<MOUNTS>) {
|
|
$support_subdomainfs = 1 if m/subdomainfs/;
|
|
$support_securityfs = 1 if m/securityfs/;
|
|
}
|
|
close(MOUNTS);
|
|
}
|
|
|
|
my $sd_mountpoint = "";
|
|
if (open(MOUNTS, "/proc/mounts")) {
|
|
while (<MOUNTS>) {
|
|
if ($support_subdomainfs) {
|
|
$sd_mountpoint = $1 if m/^\S+\s+(\S+)\s+subdomainfs\s/;
|
|
} elsif ($support_securityfs) {
|
|
if (m/^\S+\s+(\S+)\s+securityfs\s/) {
|
|
if (-e "$1/apparmor") {
|
|
$sd_mountpoint = "$1/apparmor";
|
|
} elsif (-e "$1/subdomain") {
|
|
$sd_mountpoint = "$1/subdomain";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
close(MOUNTS);
|
|
}
|
|
|
|
# make sure that subdomain is actually mounted there
|
|
$sd_mountpoint = undef unless -f "$sd_mountpoint/profiles";
|
|
|
|
return $sd_mountpoint;
|
|
}
|
|
|
|
sub which ($) {
|
|
my $file = shift;
|
|
|
|
foreach my $dir (split(/:/, $ENV{PATH})) {
|
|
return "$dir/$file" if -x "$dir/$file";
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
# we need to convert subdomain regexps to perl regexps
|
|
sub convert_regexp ($) {
|
|
my $regexp = shift;
|
|
|
|
# escape regexp-special characters we don't support
|
|
$regexp =~ s/(?<!\\)(\.|\+|\$)/\\$1/g;
|
|
|
|
# * and ** globs can't collapse to match an empty string when they're
|
|
# the only part of the glob at a specific directory level, which
|
|
# complicates things a little.
|
|
|
|
# ** globs match multiple directory levels
|
|
$regexp =~ s{(?<!\\)\*\*+}{
|
|
my ($pre, $post) = ($`, $');
|
|
if (($pre =~ /\/$/) && (!$post || $post =~ /^\//)) {
|
|
'SD_INTERNAL_MULTI_REQUIRED';
|
|
} else {
|
|
'SD_INTERNAL_MULTI_OPTIONAL';
|
|
}
|
|
}gex;
|
|
|
|
# convert * globs to match anything at the current path level
|
|
$regexp =~ s{(?<!\\)\*}{
|
|
my ($pre, $post) = ($`, $');
|
|
if (($pre =~ /\/$/) && (!$post || $post =~ /^\//)) {
|
|
'SD_INTERNAL_SINGLE_REQUIRED';
|
|
} else {
|
|
'SD_INTERNAL_SINGLE_OPTIONAL';
|
|
}
|
|
}gex;
|
|
|
|
# convert ? globs to match a single character at current path level
|
|
$regexp =~ s/(?<!\\)\?/[^\/]/g;
|
|
|
|
# convert {foo,baz} to (foo|baz)
|
|
$regexp =~ y/\{\}\,/\(\)\|/ if $regexp =~ /\{.*\,.*\}/;
|
|
|
|
# convert internal markers to their appropriate regexp equivalents
|
|
$regexp =~ s/SD_INTERNAL_SINGLE_OPTIONAL/[^\/]*/g;
|
|
$regexp =~ s/SD_INTERNAL_SINGLE_REQUIRED/[^\/]+/g;
|
|
$regexp =~ s/SD_INTERNAL_MULTI_OPTIONAL/.*/g;
|
|
$regexp =~ s/SD_INTERNAL_MULTI_REQUIRED/[^\/].*/g;
|
|
|
|
return $regexp;
|
|
}
|
|
|
|
sub get_full_path ($) {
|
|
my $originalpath = shift;
|
|
|
|
my $path = $originalpath;
|
|
|
|
# keep track so we can break out of loops
|
|
my $linkcount = 0;
|
|
|
|
# if we don't have any directory foo, look in the current dir
|
|
$path = cwd() . "/$path" if $path !~ m/\//;
|
|
|
|
# beat symlinks into submission
|
|
while (-l $path) {
|
|
|
|
if ($linkcount++ > 64) {
|
|
fatal_error "Followed too many symlinks resolving $originalpath";
|
|
}
|
|
|
|
# split out the directory/file components
|
|
if ($path =~ m/^(.*)\/(.+)$/) {
|
|
my ($dir, $file) = ($1, $2);
|
|
|
|
# figure out where the link is pointing...
|
|
my $link = readlink($path);
|
|
if ($link =~ /^\//) {
|
|
# if it's an absolute link, just replace it
|
|
$path = $link;
|
|
} else {
|
|
# if it's relative, let abs_path handle it
|
|
$path = $dir . "/$link";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-f $path) {
|
|
my ($dir, $file) = $path =~ m/^(.*)\/(.+)$/;
|
|
$path = realpath($dir) . "/$file";
|
|
} else {
|
|
$path = realpath($path);
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
sub findexecutable ($) {
|
|
my $bin = shift;
|
|
|
|
my $fqdbin;
|
|
if (-e $bin) {
|
|
$fqdbin = get_full_path($bin);
|
|
chomp($fqdbin);
|
|
} else {
|
|
if ($bin !~ /\//) {
|
|
my $which = which($bin);
|
|
if ($which) {
|
|
$fqdbin = get_full_path($which);
|
|
}
|
|
}
|
|
}
|
|
|
|
unless ($fqdbin && -e $fqdbin) {
|
|
return undef;
|
|
}
|
|
|
|
return $fqdbin;
|
|
}
|
|
|
|
sub name_to_prof_filename($) {
|
|
my $bin = shift;
|
|
my $filename;
|
|
|
|
unless ($bin =~ /^($profiledir)/) {
|
|
my $fqdbin = findexecutable($bin);
|
|
if ($fqdbin) {
|
|
$filename = getprofilefilename($fqdbin);
|
|
return ($filename, $fqdbin) if -f $filename;
|
|
}
|
|
}
|
|
|
|
if ($bin =~ /^$profiledir(.*)/) {
|
|
my $profile = $1;
|
|
return ($bin, $profile);
|
|
} elsif ($bin =~ /^\//) {
|
|
$filename = getprofilefilename($bin);
|
|
return ($filename, $bin);
|
|
} else {
|
|
# not an absolute path try it as a profile_
|
|
$bin = $1 if ($bin !~ /^profile_(.*)/);
|
|
$filename = getprofilefilename($bin);
|
|
return ($filename, "profile_${bin}");
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
sub complain ($) {
|
|
my $bin = shift;
|
|
|
|
return if (!$bin);
|
|
|
|
my ($filename, $name) = name_to_prof_filename($bin)
|
|
or fatal_error(sprintf(gettext('Can\'t find %s.'), $bin));
|
|
|
|
UI_Info(sprintf(gettext('Setting %s to complain mode.'), $name));
|
|
|
|
setprofileflags($filename, "complain");
|
|
}
|
|
|
|
sub enforce ($) {
|
|
my $bin = shift;
|
|
|
|
return if (!$bin);
|
|
|
|
my ($filename, $name) = name_to_prof_filename($bin)
|
|
or fatal_error(sprintf(gettext('Can\'t find %s.'), $bin));
|
|
|
|
UI_Info(sprintf(gettext('Setting %s to enforce mode.'), $name));
|
|
|
|
setprofileflags($filename, "");
|
|
}
|
|
|
|
sub head ($) {
|
|
my $file = shift;
|
|
|
|
my $first = "";
|
|
if (open(FILE, $file)) {
|
|
$first = <FILE>;
|
|
close(FILE);
|
|
}
|
|
|
|
return $first;
|
|
}
|
|
|
|
sub get_output ($@) {
|
|
my ($program, @args) = @_;
|
|
|
|
my $ret = -1;
|
|
|
|
my $pid;
|
|
my @output;
|
|
|
|
if (-x $program) {
|
|
$pid = open(KID_TO_READ, "-|");
|
|
unless (defined $pid) {
|
|
fatal_error "can't fork: $!";
|
|
}
|
|
|
|
if ($pid) {
|
|
while (<KID_TO_READ>) {
|
|
chomp;
|
|
push @output, $_;
|
|
}
|
|
close(KID_TO_READ);
|
|
$ret = $?;
|
|
} else {
|
|
($>, $)) = ($<, $();
|
|
open(STDERR, ">&STDOUT")
|
|
|| fatal_error "can't dup stdout to stderr";
|
|
exec($program, @args) || fatal_error "can't exec program: $!";
|
|
|
|
# NOTREACHED
|
|
}
|
|
}
|
|
|
|
return ($ret, @output);
|
|
}
|
|
|
|
sub get_reqs ($) {
|
|
my $file = shift;
|
|
|
|
my @reqs;
|
|
my ($ret, @ldd) = get_output($ldd, $file);
|
|
|
|
if ($ret == 0) {
|
|
for my $line (@ldd) {
|
|
last if $line =~ /not a dynamic executable/;
|
|
last if $line =~ /cannot read header/;
|
|
last if $line =~ /statically linked/;
|
|
|
|
# avoid new kernel 2.6 poo
|
|
next if $line =~ /linux-(gate|vdso(32|64)).so/;
|
|
|
|
if ($line =~ /^\s*\S+ => (\/\S+)/) {
|
|
push @reqs, $1;
|
|
} elsif ($line =~ /^\s*(\/\S+)/) {
|
|
push @reqs, $1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return @reqs;
|
|
}
|
|
|
|
sub handle_binfmt ($$) {
|
|
my ($profile, $fqdbin) = @_;
|
|
|
|
my %reqs;
|
|
my @reqs = get_reqs($fqdbin);
|
|
|
|
while (my $library = shift @reqs) {
|
|
|
|
$library = get_full_path($library);
|
|
|
|
push @reqs, get_reqs($library) unless $reqs{$library}++;
|
|
|
|
# does path match anything pulled in by includes in original profile?
|
|
my $combinedmode = match_prof_incs_to_path($profile, 'allow', $library);
|
|
|
|
# if we found any matching entries, do the modes match?
|
|
next if $combinedmode;
|
|
|
|
$library = globcommon($library);
|
|
chomp $library;
|
|
next unless $library;
|
|
|
|
$profile->{allow}{path}->{$library}{mode} |= str_to_mode("mr");
|
|
$profile->{allow}{path}->{$library}{audit} |= 0;
|
|
}
|
|
}
|
|
|
|
sub get_inactive_profile($) {
|
|
my $fqdbin = shift;
|
|
if ( $extras{$fqdbin} ) {
|
|
return {$fqdbin => $extras{$fqdbin}};
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub create_new_profile($) {
|
|
my $fqdbin = shift;
|
|
|
|
my $profile;
|
|
if ($fqdbin =~ /^\// ) {
|
|
$profile = {
|
|
$fqdbin => {
|
|
flags => "complain",
|
|
include => { "abstractions/base" => 1 },
|
|
path => { $fqdbin => { mode => str_to_mode("mr") } },
|
|
}
|
|
};
|
|
} else {
|
|
$profile = {
|
|
$fqdbin => {
|
|
flags => "complain",
|
|
include => { "abstractions/base" => 1 },
|
|
}
|
|
};
|
|
}
|
|
|
|
# if the executable exists on this system, pull in extra dependencies
|
|
if (-f $fqdbin) {
|
|
my $hashbang = head($fqdbin);
|
|
if ($hashbang && $hashbang =~ /^#!\s*(\S+)/) {
|
|
my $interpreter = get_full_path($1);
|
|
$profile->{$fqdbin}{allow}{path}->{$interpreter}{mode} |= str_to_mode("ix");
|
|
$profile->{$fqdbin}{allow}{path}->{$interpreter}{audit} |= 0;
|
|
if ($interpreter =~ /perl/) {
|
|
$profile->{$fqdbin}{include}->{"abstractions/perl"} = 1;
|
|
} elsif ($interpreter =~ m/\/bin\/(bash|sh)/) {
|
|
$profile->{$fqdbin}{include}->{"abstractions/bash"} = 1;
|
|
}
|
|
handle_binfmt($profile->{$fqdbin}, $interpreter);
|
|
} else {
|
|
handle_binfmt($profile->{$fqdbin}, $fqdbin);
|
|
}
|
|
}
|
|
|
|
# create required infrastructure hats if it's a known change_hat app
|
|
for my $hatglob (keys %{$cfg->{required_hats}}) {
|
|
if ($fqdbin =~ /$hatglob/) {
|
|
for my $hat (sort split(/\s+/, $cfg->{required_hats}{$hatglob})) {
|
|
$profile->{$hat} = { flags => "complain" };
|
|
}
|
|
}
|
|
}
|
|
push @created, $fqdbin;
|
|
return { $fqdbin => $profile };
|
|
}
|
|
|
|
sub delete_profile ($) {
|
|
my $profile = shift;
|
|
my $profilefile = getprofilefilename( $profile );
|
|
if ( -e $profilefile ) {
|
|
unlink( $profilefile );
|
|
}
|
|
if ( defined $sd{$profile} ) {
|
|
delete $sd{$profile};
|
|
}
|
|
}
|
|
|
|
sub get_profile($) {
|
|
my $fqdbin = shift;
|
|
my $profile_data;
|
|
|
|
my $distro = $cfg->{repository}{distro};
|
|
my $repo_url = $cfg->{repository}{url};
|
|
my @profiles;
|
|
my %profile_hash;
|
|
|
|
if (repo_is_enabled()) {
|
|
my $results;
|
|
UI_BusyStart( gettext("Connecting to repository.....") );
|
|
|
|
my ($status_ok,$ret) =
|
|
fetch_profiles_by_name($repo_url, $distro, $fqdbin );
|
|
UI_BusyStop();
|
|
if ( $status_ok ) {
|
|
%profile_hash = %$ret;
|
|
} else {
|
|
my $errmsg =
|
|
sprintf(gettext("WARNING: Error fetching profiles from the repository:\n%s\n"),
|
|
$ret?$ret:gettext("UNKNOWN ERROR"));
|
|
UI_Important( $errmsg );
|
|
}
|
|
}
|
|
|
|
my $inactive_profile = get_inactive_profile($fqdbin);
|
|
if ( defined $inactive_profile && $inactive_profile ne "" ) {
|
|
# set the profile to complain mode
|
|
my $uname = gettext( "Inactive local profile for ") . $fqdbin;
|
|
$inactive_profile->{$fqdbin}{$fqdbin}{flags} = "complain";
|
|
# inactive profiles store where they came from
|
|
delete $inactive_profile->{$fqdbin}{$fqdbin}{filename};
|
|
$profile_hash{$uname} =
|
|
{
|
|
"username" => $uname,
|
|
"profile_type" => "INACTIVE_LOCAL",
|
|
"profile" => serialize_profile($inactive_profile->{$fqdbin},
|
|
$fqdbin
|
|
),
|
|
"profile_data" => $inactive_profile,
|
|
};
|
|
}
|
|
|
|
return undef if ( keys %profile_hash == 0 ); # No repo profiles, no inactive
|
|
# profile
|
|
my @options;
|
|
my @tmp_list;
|
|
my $preferred_present = 0;
|
|
my $preferred_user = $cfg->{repository}{preferred_user} || "NOVELL";
|
|
|
|
foreach my $p ( keys %profile_hash ) {
|
|
if ( $profile_hash{$p}->{username} eq $preferred_user ) {
|
|
$preferred_present = 1;
|
|
} else {
|
|
push @tmp_list, $profile_hash{$p}->{username};
|
|
}
|
|
}
|
|
|
|
if ( $preferred_present ) {
|
|
push @options, $preferred_user;
|
|
}
|
|
push @options, @tmp_list;
|
|
|
|
my $q = {};
|
|
$q->{headers} = [];
|
|
push @{ $q->{headers} }, gettext("Profile"), $fqdbin;
|
|
|
|
$q->{functions} = [ "CMD_VIEW_PROFILE", "CMD_USE_PROFILE",
|
|
"CMD_CREATE_PROFILE", "CMD_ABORT", "CMD_FINISHED" ];
|
|
|
|
$q->{default} = "CMD_VIEW_PROFILE";
|
|
|
|
$q->{options} = [@options];
|
|
$q->{selected} = 0;
|
|
|
|
my ($p, $ans, $arg);
|
|
do {
|
|
($ans, $arg) = UI_PromptUser($q);
|
|
$p = $profile_hash{$options[$arg]};
|
|
for (my $i = 0; $i < scalar(@options); $i++) {
|
|
if ($options[$i] eq $options[$arg]) {
|
|
$q->{selected} = $i;
|
|
}
|
|
}
|
|
|
|
if ($ans eq "CMD_VIEW_PROFILE") {
|
|
if ($UI_Mode eq "yast") {
|
|
SendDataToYast(
|
|
{
|
|
type => "dialog-view-profile",
|
|
user => $options[$arg],
|
|
profile => $p->{profile},
|
|
profile_type => $p->{profile_type}
|
|
}
|
|
);
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
} else {
|
|
my $pager = get_pager();
|
|
open(PAGER, "| $pager");
|
|
print PAGER gettext("Profile submitted by") .
|
|
" $options[$arg]:\n\n" . $p->{profile} . "\n\n";
|
|
close(PAGER);
|
|
}
|
|
} elsif ($ans eq "CMD_USE_PROFILE") {
|
|
if ( $p->{profile_type} eq "INACTIVE_LOCAL" ) {
|
|
$profile_data = $p->{profile_data};
|
|
push @created, $fqdbin; # This really is ugly here
|
|
# need to find a better place to mark
|
|
# this as newly created
|
|
} else {
|
|
$profile_data =
|
|
parse_repo_profile($fqdbin, $repo_url, $p);
|
|
}
|
|
}
|
|
} until ($ans =~ /^CMD_(USE_PROFILE|CREATE_PROFILE)$/);
|
|
|
|
return $profile_data;
|
|
}
|
|
|
|
sub activate_repo_profiles ($$$) {
|
|
my ($url,$profiles,$complain) = @_;
|
|
|
|
readprofiles();
|
|
eval {
|
|
for my $p ( @$profiles ) {
|
|
my $pname = $p->[0];
|
|
my $profile_data = parse_repo_profile( $pname, $url, $p->[1] );
|
|
attach_profile_data(\%sd, $profile_data);
|
|
writeprofile($pname);
|
|
if ( $complain ) {
|
|
my $filename = getprofilefilename($pname);
|
|
setprofileflags($filename, "complain");
|
|
UI_Info(sprintf(gettext('Setting %s to complain mode.'),
|
|
$pname));
|
|
}
|
|
}
|
|
};
|
|
# if there were errors....
|
|
if ($@) {
|
|
$@ =~ s/\n$//;
|
|
print STDERR sprintf(gettext("Error activating profiles: %s\n"), $@);
|
|
}
|
|
}
|
|
|
|
sub autodep_base($$) {
|
|
my ($bin, $pname) = @_;
|
|
%extras = ();
|
|
|
|
$bin = $pname if (! $bin) && ($pname =~ /^\//);
|
|
|
|
unless ($repo_cfg || not defined $cfg->{repository}{url}) {
|
|
$repo_cfg = read_config("repository.conf");
|
|
if ( (not defined $repo_cfg->{repository}) ||
|
|
($repo_cfg->{repository}{enabled} eq "later") ) {
|
|
UI_ask_to_enable_repo();
|
|
}
|
|
}
|
|
|
|
my $fqdbin;
|
|
if ($bin) {
|
|
# findexecutable() might fail if we're running on a different system
|
|
# than the logs were collected on. ugly. we'll just hope for the best.
|
|
$fqdbin = findexecutable($bin) || $bin;
|
|
|
|
# try to make sure we have a full path in case findexecutable failed
|
|
return unless $fqdbin =~ /^\//;
|
|
|
|
# ignore directories
|
|
return if -d $fqdbin;
|
|
}
|
|
|
|
$pname = $fqdbin if $fqdbin;
|
|
|
|
my $profile_data;
|
|
|
|
readinactiveprofiles(); # need to read the profiles to see if an
|
|
# inactive local profile is present
|
|
$profile_data = eval { get_profile($pname) };
|
|
# propagate any errors we hit inside the get_profile call
|
|
if ($@) { die $@; }
|
|
|
|
unless ($profile_data) {
|
|
$profile_data = create_new_profile($pname);
|
|
}
|
|
|
|
my $file = getprofilefilename($pname);
|
|
|
|
# stick the profile into our data structure.
|
|
attach_profile_data(\%sd, $profile_data);
|
|
# and store a "clean" version also so we can display the changes we've
|
|
# made during this run
|
|
attach_profile_data(\%original_sd, $profile_data);
|
|
|
|
if (-f "$profiledir/tunables/global") {
|
|
unless (exists $filelist{$file}) {
|
|
$filelist{$file} = { };
|
|
}
|
|
$filelist{$file}{include}{'tunables/global'} = 1; # sorry
|
|
}
|
|
|
|
# write out the profile...
|
|
writeprofile_ui_feedback($pname);
|
|
}
|
|
|
|
sub autodep ($) {
|
|
my $bin = shift;
|
|
return autodep_base($bin, "");
|
|
}
|
|
|
|
sub getprofilefilename ($) {
|
|
my $profile = shift;
|
|
|
|
my $filename = $profile;
|
|
if ($filename =~ /^\//) {
|
|
$filename =~ s/^\///; # strip leading /
|
|
} else {
|
|
$filename = "profile_$filename";
|
|
}
|
|
$filename =~ s/\//./g; # convert /'s to .'s
|
|
|
|
return "$profiledir/$filename";
|
|
}
|
|
|
|
sub setprofileflags ($$) {
|
|
my $filename = shift;
|
|
my $newflags = shift;
|
|
|
|
if (open(PROFILE, "$filename")) {
|
|
if (open(NEWPROFILE, ">$filename.new")) {
|
|
while (<PROFILE>) {
|
|
if (m/^\s*(("??\/.+?"??)|(profile\s+("??.+?"??)))\s+(flags=\(.+\)\s+)*\{\s*$/) {
|
|
my ($binary, $flags) = ($1, $5);
|
|
|
|
if ($newflags) {
|
|
$_ = "$binary flags=($newflags) {\n";
|
|
} else {
|
|
$_ = "$binary {\n";
|
|
}
|
|
} elsif (m/^(\s*\^\S+)\s+(flags=\(.+\)\s+)*\{\s*$/) {
|
|
my ($hat, $flags) = ($1, $2);
|
|
|
|
if ($newflags) {
|
|
$_ = "$hat flags=($newflags) {\n";
|
|
} else {
|
|
$_ = "$hat {\n";
|
|
}
|
|
}
|
|
print NEWPROFILE;
|
|
}
|
|
close(NEWPROFILE);
|
|
rename("$filename.new", "$filename");
|
|
}
|
|
close(PROFILE);
|
|
}
|
|
}
|
|
|
|
sub profile_exists($) {
|
|
my $program = shift || return 0;
|
|
|
|
# if it's already in the cache, return true
|
|
return 1 if $existing_profiles{$program};
|
|
|
|
# if the profile exists, mark it in the cache and return true
|
|
my $profile = getprofilefilename($program);
|
|
if (-e $profile) {
|
|
$existing_profiles{$program} = 1;
|
|
return 1;
|
|
}
|
|
|
|
# couldn't find a profile, so we'll return false
|
|
return 0;
|
|
}
|
|
|
|
sub sync_profiles() {
|
|
|
|
my ($user, $pass) = get_repo_user_pass();
|
|
return unless ( $user && $pass );
|
|
|
|
my @repo_profiles;
|
|
my @changed_profiles;
|
|
my @new_profiles;
|
|
my $serialize_opts = { };
|
|
my ($status_ok,$ret) =
|
|
fetch_profiles_by_user($cfg->{repository}{url},
|
|
$cfg->{repository}{distro},
|
|
$user
|
|
);
|
|
if ( !$status_ok ) {
|
|
my $errmsg =
|
|
sprintf(gettext("WARNING: Error syncronizing profiles with the repository:\n%s\n"),
|
|
$ret?$ret:gettext("UNKNOWN ERROR"));
|
|
UI_Important($errmsg);
|
|
return;
|
|
} else {
|
|
my $users_repo_profiles = $ret;
|
|
$serialize_opts->{NO_FLAGS} = 1;
|
|
#
|
|
# Find changes made to non-repo profiles
|
|
#
|
|
for my $profile (sort keys %sd) {
|
|
if (is_repo_profile($sd{$profile}{$profile})) {
|
|
push @repo_profiles, $profile;
|
|
}
|
|
if ( grep(/^$profile$/, @created) ) {
|
|
my $p_local = serialize_profile($sd{$profile},
|
|
$profile,
|
|
$serialize_opts);
|
|
if ( not defined $users_repo_profiles->{$profile} ) {
|
|
push @new_profiles, [ $profile, $p_local, "" ];
|
|
} else {
|
|
my $p_repo = $users_repo_profiles->{$profile}->{profile};
|
|
if ( $p_local ne $p_repo ) {
|
|
push @changed_profiles, [ $profile, $p_local, $p_repo ];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# Find changes made to local profiles with repo metadata
|
|
#
|
|
if (@repo_profiles) {
|
|
for my $profile (@repo_profiles) {
|
|
my $p_local = serialize_profile($sd{$profile},
|
|
$profile,
|
|
$serialize_opts);
|
|
if ( not exists $users_repo_profiles->{$profile} ) {
|
|
push @new_profiles, [ $profile, $p_local, "" ];
|
|
} else {
|
|
my $p_repo = "";
|
|
if ( $sd{$profile}{$profile}{repo}{user} eq $user ) {
|
|
$p_repo = $users_repo_profiles->{$profile}->{profile};
|
|
} else {
|
|
my ($status_ok,$ret) =
|
|
fetch_profile_by_id($cfg->{repository}{url},
|
|
$sd{$profile}{$profile}{repo}{id}
|
|
);
|
|
if ( $status_ok ) {
|
|
$p_repo = $ret->{profile};
|
|
} else {
|
|
my $errmsg =
|
|
sprintf(
|
|
gettext("WARNING: Error syncronizing profiles with the repository:\n%s\n"),
|
|
$ret?$ret:gettext("UNKNOWN ERROR"));
|
|
UI_Important($errmsg);
|
|
next;
|
|
}
|
|
}
|
|
if ( $p_repo ne $p_local ) {
|
|
push @changed_profiles, [ $profile, $p_local, $p_repo ];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( @changed_profiles ) {
|
|
submit_changed_profiles( \@changed_profiles );
|
|
}
|
|
if ( @new_profiles ) {
|
|
submit_created_profiles( \@new_profiles );
|
|
}
|
|
}
|
|
}
|
|
|
|
sub submit_created_profiles($) {
|
|
my $new_profiles = shift;
|
|
my $url = $cfg->{repository}{url};
|
|
|
|
if ($UI_Mode eq "yast") {
|
|
my $title = gettext("New profiles");
|
|
my $explanation =
|
|
gettext("Please choose the newly created profiles that you would".
|
|
" like\nto store in the repository");
|
|
yast_select_and_upload_profiles($title,
|
|
$explanation,
|
|
$new_profiles);
|
|
} else {
|
|
my $title =
|
|
gettext("Submit newly created profiles to the repository");
|
|
my $explanation =
|
|
gettext("Would you like to upload the newly created profiles?");
|
|
console_select_and_upload_profiles($title,
|
|
$explanation,
|
|
$new_profiles);
|
|
}
|
|
}
|
|
|
|
sub submit_changed_profiles($) {
|
|
my $changed_profiles = shift;
|
|
my $url = $cfg->{repository}{url};
|
|
if (@$changed_profiles) {
|
|
if ($UI_Mode eq "yast") {
|
|
my $explanation =
|
|
gettext("Select which of the changed profiles you would".
|
|
" like to upload\nto the repository");
|
|
my $title = gettext("Changed profiles");
|
|
yast_select_and_upload_profiles($title,
|
|
$explanation,
|
|
$changed_profiles);
|
|
} else {
|
|
my $title =
|
|
gettext("Submit changed profiles to the repository");
|
|
my $explanation =
|
|
gettext("The following profiles from the repository were".
|
|
" changed.\nWould you like to upload your changes?");
|
|
console_select_and_upload_profiles($title,
|
|
$explanation,
|
|
$changed_profiles);
|
|
}
|
|
}
|
|
}
|
|
|
|
sub yast_select_and_upload_profiles($$$) {
|
|
|
|
my ($title, $explanation, $profiles_ref) = @_;
|
|
my $url = $cfg->{repository}{url};
|
|
my %profile_changes;
|
|
my @profiles = @$profiles_ref;
|
|
|
|
foreach my $prof (@profiles) {
|
|
$profile_changes{ $prof->[0] } =
|
|
get_profile_diff($prof->[2], $prof->[1]);
|
|
}
|
|
|
|
my (@selected_profiles, $changelog, $changelogs, $single_changelog);
|
|
SendDataToYast(
|
|
{
|
|
type => "dialog-select-profiles",
|
|
title => $title,
|
|
explanation => $explanation,
|
|
default_select => "false",
|
|
disable_ask_upload => "true",
|
|
profiles => \%profile_changes
|
|
}
|
|
);
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
if ($yarg->{STATUS} eq "cancel") {
|
|
return;
|
|
} else {
|
|
my $selected_profiles_ref = $yarg->{PROFILES};
|
|
@selected_profiles = @$selected_profiles_ref;
|
|
$changelogs = $yarg->{CHANGELOG};
|
|
if (defined $changelogs->{SINGLE_CHANGELOG}) {
|
|
$changelog = $changelogs->{SINGLE_CHANGELOG};
|
|
$single_changelog = 1;
|
|
}
|
|
}
|
|
|
|
for my $profile (@selected_profiles) {
|
|
my ($user, $pass) = get_repo_user_pass();
|
|
my $profile_string = serialize_profile($sd{$profile}, $profile);
|
|
if (!$single_changelog) {
|
|
$changelog = $changelogs->{$profile};
|
|
}
|
|
my ($status_ok, $ret) = upload_profile( $url,
|
|
$user,
|
|
$pass,
|
|
$cfg->{repository}{distro},
|
|
$profile,
|
|
$profile_string,
|
|
$changelog
|
|
);
|
|
if ($status_ok) {
|
|
my $newprofile = $ret;
|
|
my $newid = $newprofile->{id};
|
|
set_repo_info($sd{$profile}{$profile}, $url, $user, $newid);
|
|
writeprofile_ui_feedback($profile);
|
|
} else {
|
|
my $errmsg =
|
|
sprintf(
|
|
gettext("WARNING: An error occured while uploading the profile %s\n%s\n"),
|
|
$profile, $ret?$ret:gettext("UNKNOWN ERROR"));
|
|
UI_Important( $errmsg );
|
|
}
|
|
}
|
|
UI_Info(gettext("Uploaded changes to repository."));
|
|
|
|
# Check to see if unselected profiles should be marked as local only
|
|
# this is outside of the main repo code as we want users to be able to mark
|
|
# profiles as local only even if they aren't able to connect to the repo.
|
|
if (defined $yarg->{NEVER_ASK_AGAIN}) {
|
|
my @unselected_profiles;
|
|
foreach my $prof (@profiles) {
|
|
if ( grep(/^$prof->[0]$/, @selected_profiles) == 0 ) {
|
|
push @unselected_profiles, $prof->[0];
|
|
}
|
|
}
|
|
set_profiles_local_only( @unselected_profiles );
|
|
}
|
|
}
|
|
|
|
sub console_select_and_upload_profiles($$$) {
|
|
my ($title, $explanation, $profiles_ref) = @_;
|
|
my $url = $cfg->{repository}{url};
|
|
my @profiles = @$profiles_ref;
|
|
my $q = {};
|
|
$q->{title} = $title;
|
|
$q->{headers} = [ gettext("Repository"), $url, ];
|
|
|
|
$q->{explanation} = $explanation;
|
|
|
|
$q->{functions} = [ "CMD_UPLOAD_CHANGES",
|
|
"CMD_VIEW_CHANGES",
|
|
"CMD_ASK_LATER",
|
|
"CMD_ASK_NEVER",
|
|
"CMD_ABORT", ];
|
|
|
|
$q->{default} = "CMD_VIEW_CHANGES";
|
|
|
|
$q->{options} = [ map { $_->[0] } @profiles ];
|
|
$q->{selected} = 0;
|
|
|
|
my ($ans, $arg);
|
|
do {
|
|
($ans, $arg) = UI_PromptUser($q);
|
|
|
|
if ($ans eq "CMD_VIEW_CHANGES") {
|
|
display_changes($profiles[$arg]->[2], $profiles[$arg]->[1]);
|
|
}
|
|
} until $ans =~ /^CMD_(UPLOAD_CHANGES|ASK_NEVER|ASK_LATER)/;
|
|
|
|
if ($ans eq "CMD_ASK_NEVER") {
|
|
set_profiles_local_only( map { $_->[0] } @profiles );
|
|
} elsif ($ans eq "CMD_UPLOAD_CHANGES") {
|
|
my $changelog = UI_GetString(gettext("Changelog Entry: "), "");
|
|
my ($user, $pass) = get_repo_user_pass();
|
|
if ($user && $pass) {
|
|
for my $p_data (@profiles) {
|
|
my $profile = $p_data->[0];
|
|
my $profile_string = $p_data->[1];
|
|
my ($status_ok,$ret) =
|
|
upload_profile( $url,
|
|
$user,
|
|
$pass,
|
|
$cfg->{repository}{distro},
|
|
$profile,
|
|
$profile_string,
|
|
$changelog
|
|
);
|
|
if ($status_ok) {
|
|
my $newprofile = $ret;
|
|
my $newid = $newprofile->{id};
|
|
set_repo_info($sd{$profile}{$profile}, $url, $user, $newid);
|
|
writeprofile_ui_feedback($profile);
|
|
UI_Info(
|
|
sprintf(gettext("Uploaded %s to repository."), $profile)
|
|
);
|
|
} else {
|
|
my $errmsg =
|
|
sprintf(
|
|
gettext("WARNING: An error occured while uploading the profile %s\n%s\n"),
|
|
$profile, $ret?$ret:gettext("UNKNOWN ERROR"));
|
|
UI_Important( $errmsg );
|
|
}
|
|
}
|
|
} else {
|
|
UI_Important(gettext("Repository Error\n" .
|
|
"Registration or Signin was unsuccessful. User login\n" .
|
|
"information is required to upload profiles to the\n" .
|
|
"repository. These changes have not been sent.\n"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# Mark the profiles passed in @profiles as local only
|
|
# and don't prompt to upload changes to the repository
|
|
#
|
|
sub set_profiles_local_only(@) {
|
|
my @profiles = @_;
|
|
for my $profile (@profiles) {
|
|
$sd{$profile}{$profile}{repo}{neversubmit} = 1;
|
|
writeprofile_ui_feedback($profile);
|
|
}
|
|
}
|
|
|
|
##########################################################################
|
|
# Here are the console/yast interface functions
|
|
|
|
sub UI_Info ($) {
|
|
my $text = shift;
|
|
|
|
$DEBUGGING && debug "UI_Info: $UI_Mode: $text";
|
|
|
|
if ($UI_Mode eq "text") {
|
|
print "$text\n";
|
|
} else {
|
|
ycp::y2milestone($text);
|
|
}
|
|
}
|
|
|
|
sub UI_Important ($) {
|
|
my $text = shift;
|
|
|
|
$DEBUGGING && debug "UI_Important: $UI_Mode: $text";
|
|
|
|
if ($UI_Mode eq "text") {
|
|
print "\n$text\n";
|
|
} else {
|
|
SendDataToYast({ type => "dialog-error", message => $text });
|
|
my ($path, $yarg) = GetDataFromYast();
|
|
}
|
|
}
|
|
|
|
sub UI_YesNo ($$) {
|
|
my $text = shift;
|
|
my $default = shift;
|
|
|
|
$DEBUGGING && debug "UI_YesNo: $UI_Mode: $text $default";
|
|
|
|
my $ans;
|
|
if ($UI_Mode eq "text") {
|
|
|
|
my $yes = gettext("(Y)es");
|
|
my $no = gettext("(N)o");
|
|
|
|
# figure out our localized hotkeys
|
|
my $usrmsg = "PromptUser: " . gettext("Invalid hotkey for");
|
|
$yes =~ /\((\S)\)/ or fatal_error "$usrmsg '$yes'";
|
|
my $yeskey = lc($1);
|
|
$no =~ /\((\S)\)/ or fatal_error "$usrmsg '$no'";
|
|
my $nokey = lc($1);
|
|
|
|
print "\n$text\n";
|
|
if ($default eq "y") {
|
|
print "\n[$yes] / $no\n";
|
|
} else {
|
|
print "\n$yes / [$no]\n";
|
|
}
|
|
$ans = getkey() || (($default eq "y") ? $yeskey : $nokey);
|
|
|
|
# convert back from a localized answer to english y or n
|
|
$ans = (lc($ans) eq $yeskey) ? "y" : "n";
|
|
} else {
|
|
|
|
SendDataToYast({ type => "dialog-yesno", question => $text });
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
$ans = $yarg->{answer} || $default;
|
|
|
|
}
|
|
|
|
return $ans;
|
|
}
|
|
|
|
sub UI_YesNoCancel ($$) {
|
|
my $text = shift;
|
|
my $default = shift;
|
|
|
|
$DEBUGGING && debug "UI_YesNoCancel: $UI_Mode: $text $default";
|
|
|
|
my $ans;
|
|
if ($UI_Mode eq "text") {
|
|
|
|
my $yes = gettext("(Y)es");
|
|
my $no = gettext("(N)o");
|
|
my $cancel = gettext("(C)ancel");
|
|
|
|
# figure out our localized hotkeys
|
|
my $usrmsg = "PromptUser: " . gettext("Invalid hotkey for");
|
|
$yes =~ /\((\S)\)/ or fatal_error "$usrmsg '$yes'";
|
|
my $yeskey = lc($1);
|
|
$no =~ /\((\S)\)/ or fatal_error "$usrmsg '$no'";
|
|
my $nokey = lc($1);
|
|
$cancel =~ /\((\S)\)/ or fatal_error "$usrmsg '$cancel'";
|
|
my $cancelkey = lc($1);
|
|
|
|
$ans = "XXXINVALIDXXX";
|
|
while ($ans !~ /^(y|n|c)$/) {
|
|
print "\n$text\n";
|
|
if ($default eq "y") {
|
|
print "\n[$yes] / $no / $cancel\n";
|
|
} elsif ($default eq "n") {
|
|
print "\n$yes / [$no] / $cancel\n";
|
|
} else {
|
|
print "\n$yes / $no / [$cancel]\n";
|
|
}
|
|
|
|
$ans = getkey();
|
|
|
|
if ($ans) {
|
|
# convert back from a localized answer to english y or n
|
|
$ans = lc($ans);
|
|
if ($ans eq $yeskey) {
|
|
$ans = "y";
|
|
} elsif ($ans eq $nokey) {
|
|
$ans = "n";
|
|
} elsif ($ans eq $cancelkey) {
|
|
$ans = "c";
|
|
}
|
|
} else {
|
|
$ans = $default;
|
|
}
|
|
}
|
|
} else {
|
|
|
|
SendDataToYast({ type => "dialog-yesnocancel", question => $text });
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
$ans = $yarg->{answer} || $default;
|
|
|
|
}
|
|
|
|
return $ans;
|
|
}
|
|
|
|
sub UI_GetString ($$) {
|
|
my $text = shift;
|
|
my $default = shift;
|
|
|
|
$DEBUGGING && debug "UI_GetString: $UI_Mode: $text $default";
|
|
|
|
my $string;
|
|
if ($UI_Mode eq "text") {
|
|
|
|
if ($term) {
|
|
$string = $term->readline($text, $default);
|
|
} else {
|
|
local $| = 1;
|
|
print "$text";
|
|
$string = <STDIN>;
|
|
chomp($string);
|
|
}
|
|
|
|
} else {
|
|
|
|
SendDataToYast({
|
|
type => "dialog-getstring",
|
|
label => $text,
|
|
default => $default
|
|
});
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
$string = $yarg->{string};
|
|
|
|
}
|
|
return $string;
|
|
}
|
|
|
|
sub UI_GetFile ($) {
|
|
my $f = shift;
|
|
|
|
$DEBUGGING && debug "UI_GetFile: $UI_Mode";
|
|
|
|
my $filename;
|
|
if ($UI_Mode eq "text") {
|
|
|
|
local $| = 1;
|
|
print "$f->{description}\n";
|
|
$filename = <STDIN>;
|
|
chomp($filename);
|
|
|
|
} else {
|
|
|
|
$f->{type} = "dialog-getfile";
|
|
|
|
SendDataToYast($f);
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
if ($yarg->{answer} eq "okay") {
|
|
$filename = $yarg->{filename};
|
|
}
|
|
}
|
|
|
|
return $filename;
|
|
}
|
|
|
|
sub UI_BusyStart ($) {
|
|
my $message = shift;
|
|
$DEBUGGING && debug "UI_BusyStart: $UI_Mode";
|
|
|
|
if ($UI_Mode eq "text") {
|
|
UI_Info( $message );
|
|
} else {
|
|
SendDataToYast({
|
|
type => "dialog-busy-start",
|
|
message => $message,
|
|
});
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
}
|
|
}
|
|
|
|
sub UI_BusyStop() {
|
|
$DEBUGGING && debug "UI_BusyStop: $UI_Mode";
|
|
|
|
if ($UI_Mode ne "text") {
|
|
SendDataToYast({ type => "dialog-busy-stop" });
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
}
|
|
}
|
|
|
|
|
|
my %CMDS = (
|
|
CMD_ALLOW => "(A)llow",
|
|
CMD_OTHER => "(M)ore",
|
|
CMD_AUDIT_NEW => "Audi(t)",
|
|
CMD_AUDIT_OFF => "Audi(t) off",
|
|
CMD_AUDIT_FULL => "Audit (A)ll",
|
|
CMD_OTHER => "(O)pts",
|
|
CMD_USER_ON => "(O)wner permissions on",
|
|
CMD_USER_OFF => "(O)wner permissions off",
|
|
CMD_DENY => "(D)eny",
|
|
CMD_ABORT => "Abo(r)t",
|
|
CMD_FINISHED => "(F)inish",
|
|
CMD_ix => "(I)nherit",
|
|
CMD_px => "(P)rofile",
|
|
CMD_px_safe => "(P)rofile Clean Exec",
|
|
CMD_cx => "(C)hild",
|
|
CMD_cx_safe => "(C)hild Clean Exec",
|
|
CMD_nx => "(N)ame",
|
|
CMD_nx_safe => "(N)amed Clean Exec",
|
|
CMD_ux => "(U)nconfined",
|
|
CMD_ux_safe => "(U)nconfined Clean Exec",
|
|
CMD_pix => "(P)rofile ix",
|
|
CMD_pix_safe => "(P)rofile ix Clean Exec",
|
|
CMD_cix => "(C)hild ix",
|
|
CMD_cix_safe => "(C)hild ix Cx Clean Exec",
|
|
CMD_nix => "(N)ame ix",
|
|
CMD_nix_safe => "(N)ame ix",
|
|
CMD_EXEC_IX_ON => "(X)ix",
|
|
CMD_EXEC_IX_OFF => "(X)ix",
|
|
CMD_SAVE => "(S)ave Changes",
|
|
CMD_CONTINUE => "(C)ontinue Profiling",
|
|
CMD_NEW => "(N)ew",
|
|
CMD_GLOB => "(G)lob",
|
|
CMD_GLOBEXT => "Glob w/(E)xt",
|
|
CMD_ADDHAT => "(A)dd Requested Hat",
|
|
CMD_USEDEFAULT => "(U)se Default Hat",
|
|
CMD_SCAN => "(S)can system log for AppArmor events",
|
|
CMD_HELP => "(H)elp",
|
|
CMD_VIEW_PROFILE => "(V)iew Profile",
|
|
CMD_USE_PROFILE => "(U)se Profile",
|
|
CMD_CREATE_PROFILE => "(C)reate New Profile",
|
|
CMD_UPDATE_PROFILE => "(U)pdate Profile",
|
|
CMD_IGNORE_UPDATE => "(I)gnore Update",
|
|
CMD_SAVE_CHANGES => "(S)ave Changes",
|
|
CMD_UPLOAD_CHANGES => "(U)pload Changes",
|
|
CMD_VIEW_CHANGES => "(V)iew Changes",
|
|
CMD_VIEW => "(V)iew",
|
|
CMD_ENABLE_REPO => "(E)nable Repository",
|
|
CMD_DISABLE_REPO => "(D)isable Repository",
|
|
CMD_ASK_NEVER => "(N)ever Ask Again",
|
|
CMD_ASK_LATER => "Ask Me (L)ater",
|
|
CMD_YES => "(Y)es",
|
|
CMD_NO => "(N)o",
|
|
CMD_ALL_NET => "Allow All (N)etwork",
|
|
CMD_NET_FAMILY => "Allow Network Fa(m)ily",
|
|
CMD_OVERWRITE => "(O)verwrite Profile",
|
|
CMD_KEEP => "(K)eep Profile",
|
|
CMD_CONTINUE => "(C)ontinue",
|
|
);
|
|
|
|
sub UI_PromptUser ($) {
|
|
my $q = shift;
|
|
|
|
my ($cmd, $arg);
|
|
if ($UI_Mode eq "text") {
|
|
|
|
($cmd, $arg) = Text_PromptUser($q);
|
|
|
|
} else {
|
|
|
|
$q->{type} = "wizard";
|
|
|
|
SendDataToYast($q);
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
|
|
$cmd = $yarg->{selection} || "CMD_ABORT";
|
|
$arg = $yarg->{selected};
|
|
}
|
|
|
|
if ($cmd eq "CMD_ABORT") {
|
|
confirm_and_abort();
|
|
$cmd = "XXXINVALIDXXX";
|
|
} elsif ($cmd eq "CMD_FINISHED") {
|
|
confirm_and_finish();
|
|
$cmd = "XXXINVALIDXXX";
|
|
}
|
|
|
|
if (wantarray) {
|
|
return ($cmd, $arg);
|
|
} else {
|
|
return $cmd;
|
|
}
|
|
}
|
|
|
|
|
|
sub UI_ShortMessage($$) {
|
|
my ($headline, $message) = @_;
|
|
|
|
SendDataToYast(
|
|
{
|
|
type => "short-dialog-message",
|
|
headline => $headline,
|
|
message => $message
|
|
}
|
|
);
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
}
|
|
|
|
sub UI_LongMessage($$) {
|
|
my ($headline, $message) = @_;
|
|
|
|
$headline = "MISSING" if not defined $headline;
|
|
$message = "MISSING" if not defined $message;
|
|
|
|
SendDataToYast(
|
|
{
|
|
type => "long-dialog-message",
|
|
headline => $headline,
|
|
message => $message
|
|
}
|
|
);
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
}
|
|
|
|
##########################################################################
|
|
# here are the interface functions to send data back and forth between
|
|
# the yast frontend and the perl backend
|
|
|
|
# this is super ugly, but waits for the next ycp Read command and sends data
|
|
# back to the ycp front end.
|
|
|
|
sub SendDataToYast($) {
|
|
my $data = shift;
|
|
|
|
$DEBUGGING && debug "SendDataToYast: Waiting for YCP command";
|
|
|
|
while (<STDIN>) {
|
|
$DEBUGGING && debug "SendDataToYast: YCP: $_";
|
|
my ($ycommand, $ypath, $yargument) = ycp::ParseCommand($_);
|
|
|
|
if ($ycommand && $ycommand eq "Read") {
|
|
|
|
if ($DEBUGGING) {
|
|
my $debugmsg = Data::Dumper->Dump([$data], [qw(*data)]);
|
|
debug "SendDataToYast: Sending--\n$debugmsg";
|
|
}
|
|
|
|
ycp::Return($data);
|
|
return 1;
|
|
|
|
} else {
|
|
|
|
$DEBUGGING && debug "SendDataToYast: Expected 'Read' but got-- $_";
|
|
|
|
}
|
|
}
|
|
|
|
# if we ever break out here, something's horribly wrong.
|
|
fatal_error "SendDataToYast: didn't receive YCP command before connection died";
|
|
}
|
|
|
|
# this is super ugly, but waits for the next ycp Write command and grabs
|
|
# whatever the ycp front end gives us
|
|
|
|
sub GetDataFromYast() {
|
|
|
|
$DEBUGGING && debug "GetDataFromYast: Waiting for YCP command";
|
|
|
|
while (<STDIN>) {
|
|
$DEBUGGING && debug "GetDataFromYast: YCP: $_";
|
|
my ($ycmd, $ypath, $yarg) = ycp::ParseCommand($_);
|
|
|
|
if ($DEBUGGING) {
|
|
my $debugmsg = Data::Dumper->Dump([$yarg], [qw(*data)]);
|
|
debug "GetDataFromYast: Received--\n$debugmsg";
|
|
}
|
|
|
|
if ($ycmd && $ycmd eq "Write") {
|
|
|
|
ycp::Return("true");
|
|
return ($ypath, $yarg);
|
|
|
|
} else {
|
|
$DEBUGGING && debug "GetDataFromYast: Expected 'Write' but got-- $_";
|
|
}
|
|
}
|
|
|
|
# if we ever break out here, something's horribly wrong.
|
|
fatal_error "GetDataFromYast: didn't receive YCP command before connection died";
|
|
}
|
|
|
|
sub confirm_and_abort() {
|
|
my $ans = UI_YesNo(gettext("Are you sure you want to abandon this set of profile changes and exit?"), "n");
|
|
if ($ans eq "y") {
|
|
UI_Info(gettext("Abandoning all changes."));
|
|
shutdown_yast();
|
|
foreach my $prof (@created) {
|
|
delete_profile($prof);
|
|
}
|
|
exit 0;
|
|
}
|
|
}
|
|
|
|
sub confirm_and_finish() {
|
|
die "FINISHING\n";
|
|
}
|
|
|
|
sub build_x_functions($$$) {
|
|
my ($default, $options, $exec_toggle) = @_;
|
|
my @{list};
|
|
if ($exec_toggle) {
|
|
push @list, "CMD_ix" if $options =~ /i/;
|
|
push @list, "CMD_pix" if $options =~ /p/ and $options =~ /i/;
|
|
push @list, "CMD_cix" if $options =~ /c/ and $options =~ /i/;
|
|
push @list, "CMD_nix" if $options =~ /n/ and $options =~ /i/;
|
|
push @list, "CMD_ux" if $options =~ /u/;
|
|
} else {
|
|
push @list, "CMD_ix" if $options =~ /i/;
|
|
push @list, "CMD_px" if $options =~ /p/;
|
|
push @list, "CMD_cx" if $options =~ /c/;
|
|
push @list, "CMD_nx" if $options =~ /n/;
|
|
push @list, "CMD_ux" if $options =~ /u/;
|
|
}
|
|
if ($exec_toggle) {
|
|
push @list, "CMD_EXEC_IX_OFF" if $options =~/p|c|n/;
|
|
} else {
|
|
push @list, "CMD_EXEC_IX_ON" if $options =~/p|c|n/;
|
|
}
|
|
push @list, "CMD_DENY", "CMD_ABORT", "CMD_FINISHED";
|
|
return @list;
|
|
}
|
|
|
|
##########################################################################
|
|
# this is the hideously ugly function that descends down the flow/event
|
|
# trees that we've generated by parsing the logfile
|
|
|
|
sub handlechildren($$$);
|
|
|
|
sub handlechildren($$$) {
|
|
my $profile = shift;
|
|
my $hat = shift;
|
|
my $root = shift;
|
|
|
|
my @entries = @$root;
|
|
for my $entry (@entries) {
|
|
fatal_error "$entry is not a ref" if not ref($entry);
|
|
|
|
if (ref($entry->[0])) {
|
|
handlechildren($profile, $hat, $entry);
|
|
} else {
|
|
|
|
my @entry = @$entry;
|
|
my $type = shift @entry;
|
|
|
|
if ($type eq "fork") {
|
|
my ($pid, $p, $h) = @entry;
|
|
|
|
if ( ($p !~ /null(-complain)*-profile/)
|
|
&& ($h !~ /null(-complain)*-profile/))
|
|
{
|
|
$profile = $p;
|
|
$hat = $h;
|
|
}
|
|
|
|
if ($hat) {
|
|
$profilechanges{$pid} = $profile . "//" . $hat;
|
|
} else {
|
|
$profilechanges{$pid} = $profile;
|
|
}
|
|
} elsif ($type eq "unknown_hat") {
|
|
my ($pid, $p, $h, $sdmode, $uhat) = @entry;
|
|
|
|
if ($p !~ /null(-complain)*-profile/) {
|
|
$profile = $p;
|
|
}
|
|
|
|
if ($sd{$profile}{$uhat}) {
|
|
$hat = $uhat;
|
|
next;
|
|
}
|
|
|
|
my $new_p = update_repo_profile($sd{$profile}{$profile});
|
|
if ( $new_p and
|
|
UI_SelectUpdatedRepoProfile($profile, $new_p) and
|
|
$sd{$profile}{$uhat} ) {
|
|
$hat = $uhat;
|
|
next;
|
|
}
|
|
|
|
# figure out what our default hat for this application is.
|
|
my $defaulthat;
|
|
for my $hatglob (keys %{$cfg->{defaulthat}}) {
|
|
$defaulthat = $cfg->{defaulthat}{$hatglob}
|
|
if $profile =~ /$hatglob/;
|
|
}
|
|
# keep track of previous answers for this run...
|
|
my $context = $profile;
|
|
$context .= " -> ^$uhat";
|
|
my $ans = $transitions{$context} || "XXXINVALIDXXX";
|
|
|
|
while ($ans !~ /^CMD_(ADDHAT|USEDEFAULT|DENY)$/) {
|
|
my $q = {};
|
|
$q->{headers} = [];
|
|
push @{ $q->{headers} }, gettext("Profile"), $profile;
|
|
if ($defaulthat) {
|
|
push @{ $q->{headers} }, gettext("Default Hat"), $defaulthat;
|
|
}
|
|
push @{ $q->{headers} }, gettext("Requested Hat"), $uhat;
|
|
|
|
$q->{functions} = [];
|
|
push @{ $q->{functions} }, "CMD_ADDHAT";
|
|
push @{ $q->{functions} }, "CMD_USEDEFAULT" if $defaulthat;
|
|
push @{$q->{functions}}, "CMD_DENY", "CMD_ABORT",
|
|
"CMD_FINISHED";
|
|
|
|
$q->{default} = ($sdmode eq "PERMITTING") ? "CMD_ADDHAT" : "CMD_DENY";
|
|
|
|
$seenevents++;
|
|
|
|
$ans = UI_PromptUser($q);
|
|
|
|
}
|
|
$transitions{$context} = $ans;
|
|
|
|
if ($ans eq "CMD_ADDHAT") {
|
|
$hat = $uhat;
|
|
$sd{$profile}{$hat}{flags} = $sd{$profile}{$profile}{flags};
|
|
} elsif ($ans eq "CMD_USEDEFAULT") {
|
|
$hat = $defaulthat;
|
|
} elsif ($ans eq "CMD_DENY") {
|
|
return;
|
|
}
|
|
|
|
} elsif ($type eq "capability") {
|
|
my ($pid, $p, $h, $prog, $sdmode, $capability) = @entry;
|
|
|
|
if ( ($p !~ /null(-complain)*-profile/)
|
|
&& ($h !~ /null(-complain)*-profile/))
|
|
{
|
|
$profile = $p;
|
|
$hat = $h;
|
|
}
|
|
|
|
# print "$pid $profile $hat $prog $sdmode capability $capability\n";
|
|
|
|
next unless $profile && $hat;
|
|
|
|
$prelog{$sdmode}{$profile}{$hat}{capability}{$capability} = 1;
|
|
} elsif (($type eq "path") || ($type eq "exec")) {
|
|
my ($pid, $p, $h, $prog, $sdmode, $mode, $detail, $to_name) = @entry;
|
|
|
|
$mode = 0 unless ($mode);
|
|
|
|
if ( ($p !~ /null(-complain)*-profile/)
|
|
&& ($h !~ /null(-complain)*-profile/))
|
|
{
|
|
$profile = $p;
|
|
$hat = $h;
|
|
}
|
|
|
|
next unless $profile && $hat && $detail;
|
|
my $domainchange = ($type eq "exec") ? "change" : "nochange";
|
|
|
|
# escape special characters that show up in literal paths
|
|
$detail =~ s/(\[|\]|\+|\*|\{|\})/\\$1/g;
|
|
|
|
# we need to give the Execute dialog if they're requesting x
|
|
# access for something that's not a directory - we'll force
|
|
# a "ix" Path dialog for directories
|
|
my $do_execute = 0;
|
|
my $exec_target = $detail;
|
|
|
|
if ($mode & str_to_mode("x")) {
|
|
if (-d $exec_target) {
|
|
$mode &= (~$ALL_AA_EXEC_TYPE);
|
|
$mode |= str_to_mode("ix");
|
|
} else {
|
|
$do_execute = 1;
|
|
}
|
|
}
|
|
|
|
if ($mode & $AA_MAY_LINK) {
|
|
if ($detail =~ m/^from (.+) to (.+)$/) {
|
|
my ($path, $target) = ($1, $2);
|
|
|
|
my $frommode = str_to_mode("lr");
|
|
if (defined $prelog{$sdmode}{$profile}{$hat}{path}{$path}) {
|
|
$frommode |= $prelog{$sdmode}{$profile}{$hat}{path}{$path};
|
|
}
|
|
$prelog{$sdmode}{$profile}{$hat}{path}{$path} = $frommode;
|
|
|
|
my $tomode = str_to_mode("lr");
|
|
if (defined $prelog{$sdmode}{$profile}{$hat}{path}{$target}) {
|
|
$tomode |= $prelog{$sdmode}{$profile}{$hat}{path}{$target};
|
|
}
|
|
$prelog{$sdmode}{$profile}{$hat}{path}{$target} = $tomode;
|
|
|
|
# print "$pid $profile $hat $prog $sdmode $path:$frommode -> $target:$tomode\n";
|
|
} else {
|
|
next;
|
|
}
|
|
} elsif ($mode) {
|
|
my $path = $detail;
|
|
|
|
if (defined $prelog{$sdmode}{$profile}{$hat}{path}{$path}) {
|
|
$mode |= $prelog{$sdmode}{$profile}{$hat}{path}{$path};
|
|
}
|
|
$prelog{$sdmode}{$profile}{$hat}{path}{$path} = $mode;
|
|
|
|
# print "$pid $profile $hat $prog $sdmode $mode $path\n";
|
|
}
|
|
|
|
if ($do_execute) {
|
|
next if ( profile_known_exec($sd{$profile}{$hat},
|
|
"exec", $exec_target ) );
|
|
|
|
my $p = update_repo_profile($sd{$profile}{$profile});
|
|
|
|
if ($to_name) {
|
|
next if ( $to_name and
|
|
UI_SelectUpdatedRepoProfile($profile, $p) and
|
|
profile_known_exec($sd{$profile}{$hat},
|
|
"exec", $to_name ) );
|
|
} else {
|
|
next if ( UI_SelectUpdatedRepoProfile($profile, $p) and
|
|
profile_known_exec($sd{$profile}{$hat},
|
|
"exec", $exec_target ) );
|
|
}
|
|
|
|
my $context = $profile;
|
|
$context .= "^$hat" if $profile ne $hat;
|
|
$context .= " -> $exec_target";
|
|
my $ans = $transitions{$context} || "";
|
|
|
|
my ($combinedmode, $combinedaudit, $cm, $am, @m);
|
|
$combinedmode = 0;
|
|
$combinedaudit = 0;
|
|
|
|
# does path match any regexps in original profile?
|
|
($cm, $am, @m) = rematchfrag($sd{$profile}{$hat}, 'allow', $exec_target);
|
|
$combinedmode |= $cm if $cm;
|
|
$combinedaudit |= $am if $am;
|
|
|
|
# find the named transition if is present
|
|
if ($combinedmode & str_to_mode("x")) {
|
|
my $nt_name;
|
|
foreach my $entry (@m) {
|
|
if ($sd{$profile}{$hat}{allow}{path}{$entry}{to}) {
|
|
$nt_name = $sd{$profile}{$hat}{allow}{path}{$entry}{to};
|
|
last;
|
|
}
|
|
}
|
|
if ($to_name and $nt_name and ($to_name ne $nt_name)) {
|
|
#fatal_error "transition name from "
|
|
} elsif ($nt_name) {
|
|
$to_name = $nt_name;
|
|
}
|
|
}
|
|
|
|
# does path match anything pulled in by includes in
|
|
# original profile?
|
|
($cm, $am, @m) = match_prof_incs_to_path($sd{$profile}{$hat}, 'allow', $exec_target);
|
|
$combinedmode |= $cm if $cm;
|
|
$combinedaudit |= $am if $am;
|
|
if ($combinedmode & str_to_mode("x")) {
|
|
my $nt_name;
|
|
foreach my $entry (@m) {
|
|
if ($sd{$profile}{$hat}{allow}{path}{$entry}{to}) {
|
|
$nt_name = $sd{$profile}{$hat}{allow}{path}{$entry}{to};
|
|
last;
|
|
}
|
|
}
|
|
if ($to_name and $nt_name and ($to_name ne $nt_name)) {
|
|
#fatal_error "transition name from "
|
|
} elsif ($nt_name) {
|
|
$to_name = $nt_name;
|
|
}
|
|
}
|
|
|
|
|
|
#nx does not exist in profiles. It does in log
|
|
#files however. The log parsing routines will convert
|
|
#it to its profile form.
|
|
#nx is internally represented by cx/px/cix/pix + to_name
|
|
my $exec_mode = 0;
|
|
if (contains($combinedmode, "pix")) {
|
|
if ($to_name) {
|
|
$ans = "CMD_nix";
|
|
} else {
|
|
$ans = "CMD_pix";
|
|
}
|
|
$exec_mode = str_to_mode("pixr");
|
|
} elsif (contains($combinedmode, "cix")) {
|
|
if ($to_name) {
|
|
$ans = "CMD_nix";
|
|
} else {
|
|
$ans = "CMD_cix";
|
|
}
|
|
$exec_mode = str_to_mode("cixr");
|
|
} elsif (contains($combinedmode, "Pix")) {
|
|
if ($to_name) {
|
|
$ans = "CMD_nix_safe";
|
|
} else {
|
|
$ans = "CMD_pix_safe";
|
|
}
|
|
$exec_mode = str_to_mode("Pixr");
|
|
} elsif (contains($combinedmode, "Cix")) {
|
|
if ($to_name) {
|
|
$ans = "CMD_nix_safe";
|
|
} else {
|
|
$ans = "CMD_cix_safe";
|
|
}
|
|
$exec_mode = str_to_mode("Cixr");
|
|
} elsif (contains($combinedmode, "ix")) {
|
|
$ans = "CMD_ix";
|
|
$exec_mode = str_to_mode("ixr");
|
|
} elsif (contains($combinedmode, "px")) {
|
|
if ($to_name) {
|
|
$ans = "CMD_nx";
|
|
} else {
|
|
$ans = "CMD_px";
|
|
}
|
|
$exec_mode = str_to_mode("px");
|
|
} elsif (contains($combinedmode, "cx")) {
|
|
if ($to_name) {
|
|
$ans = "CMD_nx";
|
|
} else {
|
|
$ans = "CMD_cx";
|
|
}
|
|
$exec_mode = str_to_mode("cx");
|
|
} elsif (contains($combinedmode, "ux")) {
|
|
$ans = "CMD_ux";
|
|
$exec_mode = str_to_mode("ux");
|
|
} elsif (contains($combinedmode, "Px")) {
|
|
if ($to_name) {
|
|
$ans = "CMD_nx_safe";
|
|
} else {
|
|
$ans = "CMD_px_safe";
|
|
}
|
|
$exec_mode = str_to_mode("Px");
|
|
} elsif (contains($combinedmode, "Cx")) {
|
|
if ($to_name) {
|
|
$ans = "CMD_nx_safe";
|
|
} else {
|
|
$ans = "CMD_cx_safe";
|
|
}
|
|
$exec_mode = str_to_mode("Cx");
|
|
} elsif (contains($combinedmode, "Ux")) {
|
|
$ans = "CMD_ux_safe";
|
|
$exec_mode = str_to_mode("Ux");
|
|
} else {
|
|
my $options = $cfg->{qualifiers}{$exec_target} || "ipcnu";
|
|
fatal_error "$entry has transition name but not transition mode" if $to_name;
|
|
|
|
# force "ix" as the only option when the profiled
|
|
# program executes itself
|
|
$options = "i" if $exec_target eq $profile;
|
|
|
|
# for now don't allow hats to cx
|
|
$options =~ s/c// if $hat and $hat ne $profile;
|
|
|
|
# we always need deny...
|
|
$options .= "d";
|
|
|
|
# figure out what our default option should be...
|
|
my $default;
|
|
if ($options =~ /p/
|
|
&& -e getprofilefilename($exec_target))
|
|
{
|
|
$default = "CMD_px";
|
|
} elsif ($options =~ /i/) {
|
|
$default = "CMD_ix";
|
|
} elsif ($options =~ /c/) {
|
|
$default = "CMD_cx";
|
|
} elsif ($options =~ /n/) {
|
|
$default = "CMD_nx";
|
|
} else {
|
|
$default = "CMD_DENY";
|
|
}
|
|
|
|
# ugh, this doesn't work if someone does an ix before
|
|
# calling this particular child process. at least
|
|
# it's only a hint instead of mandatory to get this
|
|
# right.
|
|
my $parent_uses_ld_xxx = check_for_LD_XXX($profile);
|
|
|
|
my $severity = $sevdb->rank($exec_target, "x");
|
|
|
|
# build up the prompt...
|
|
my $q = {};
|
|
$q->{headers} = [];
|
|
push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
|
|
if ($prog && $prog ne "HINT") {
|
|
push @{ $q->{headers} }, gettext("Program"), $prog;
|
|
}
|
|
# $to_name should NOT exist here other wise we know what
|
|
# mode we are supposed to be transitioning to
|
|
# which is handled above.
|
|
push @{ $q->{headers} }, gettext("Execute"), $exec_target;
|
|
push @{ $q->{headers} }, gettext("Severity"), $severity;
|
|
|
|
$q->{functions} = [];
|
|
|
|
my $prompt = "\n$context\n";
|
|
my $exec_toggle = 0;
|
|
|
|
push @{ $q->{functions} }, build_x_functions($default, $options, $exec_toggle);
|
|
|
|
$options = join("|", split(//, $options));
|
|
|
|
$seenevents++;
|
|
|
|
while ($ans !~ m/^CMD_(ix|px|cx|nx|pix|cix|nix|px_safe|cx_safe|nx_safe|pix_safe|cix_safe|nix_safe|ux|ux_safe|EXEC_TOGGLE|DENY)$/) {
|
|
$ans = UI_PromptUser($q);
|
|
|
|
if ($ans =~ /CMD_EXEC_IX_/) {
|
|
$exec_toggle = !$exec_toggle;
|
|
|
|
$q->{functions} = [ ];
|
|
push @{ $q->{functions} }, build_x_functions($default, $options, $exec_toggle);
|
|
$ans = "";
|
|
next;
|
|
}
|
|
if ($ans =~ /CMD_(nx|nix)/) {
|
|
my $arg = $exec_target;
|
|
|
|
my $ynans = "n";
|
|
if ($profile eq $hat) {
|
|
$ynans = UI_YesNo("Are you specifying a transition to a local profile?", "n");
|
|
}
|
|
|
|
if ($ynans eq "y") {
|
|
if ($ans eq "CMD_nx") {
|
|
$ans = "CMD_cx";
|
|
} else {
|
|
$ans = "CMD_cix";
|
|
}
|
|
} else {
|
|
if ($ans eq "CMD_nx") {
|
|
$ans = "CMD_px";
|
|
} else {
|
|
$ans = "CMD_pix";
|
|
}
|
|
}
|
|
$to_name = UI_GetString(gettext("Enter profile name to transition to: "), $arg);
|
|
}
|
|
if ($ans =~ /CMD_ix/) {
|
|
$exec_mode = str_to_mode("ix");
|
|
} elsif ($ans =~ /CMD_(px|cx|nx|pix|cix|nix)/) {
|
|
my $match = $1;
|
|
$exec_mode = str_to_mode($match);
|
|
my $px_default = "n";
|
|
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.");
|
|
if ($parent_uses_ld_xxx) {
|
|
$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.");
|
|
}
|
|
my $ynans = UI_YesNo($px_mesg, $px_default);
|
|
$ans = "CMD_$match";
|
|
if ($ynans eq "y") {
|
|
$exec_mode &= ~$AA_EXEC_UNSAFE;
|
|
}
|
|
} elsif ($ans eq "CMD_ux") {
|
|
$exec_mode = str_to_mode("ux");
|
|
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");
|
|
if ($ynans eq "y") {
|
|
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");
|
|
if ($ynans eq "y") {
|
|
$exec_mode &= ~($AA_EXEC_UNSAFE | ($AA_EXEC_UNSAFE << $AA_OTHER_SHIFT));
|
|
}
|
|
} else {
|
|
$ans = "INVALID";
|
|
}
|
|
}
|
|
}
|
|
$transitions{$context} = $ans;
|
|
|
|
if ($ans =~ /CMD_(ix|px|cx|nx|pix|cix|nix)/) {
|
|
# if we're inheriting, things'll bitch unless we have r
|
|
if ($exec_mode & str_to_mode("i")) {
|
|
$exec_mode |= str_to_mode("r");
|
|
}
|
|
|
|
} else {
|
|
if ($ans eq "CMD_DENY") {
|
|
$sd{$profile}{$hat}{deny}{path}{$exec_target}{mode} |= str_to_mode("x");
|
|
|
|
$sd{$profile}{$hat}{deny}{path}{$exec_target}{audit} |= 0;
|
|
$changed{$profile} = 1;
|
|
# skip all remaining events if they say to deny
|
|
# the exec
|
|
return if $domainchange eq "change";
|
|
}
|
|
|
|
}
|
|
|
|
unless ($ans eq "CMD_DENY") {
|
|
# ???? if its defined in the prelog we shouldn't have asked
|
|
if (defined $prelog{PERMITTING}{$profile}{$hat}{path}{$exec_target}) {
|
|
# $exec_mode = $prelog{PERMITTING}{$profile}{$hat}{path}{$exec_target};
|
|
}
|
|
|
|
$prelog{PERMITTING}{$profile}{$hat}{path}{$exec_target} |= $exec_mode;
|
|
$log{PERMITTING}{$profile} = {};
|
|
$sd{$profile}{$hat}{allow}{path}{$exec_target}{mode} |= $exec_mode;
|
|
$sd{$profile}{$hat}{allow}{path}{$exec_target}{audit} |= 0;
|
|
$sd{$profile}{$hat}{allow}{path}{$exec_target}{to} = $to_name if ($to_name);
|
|
|
|
# mark this profile as changed
|
|
$changed{$profile} = 1;
|
|
|
|
if ($exec_mode & str_to_mode("i")) {
|
|
if ($exec_target =~ /perl/) {
|
|
$sd{$profile}{$hat}{include}{"abstractions/perl"} = 1;
|
|
} elsif ($detail =~ m/\/bin\/(bash|sh)/) {
|
|
$sd{$profile}{$hat}{include}{"abstractions/bash"} = 1;
|
|
}
|
|
my $hashbang = head($exec_target);
|
|
if ($hashbang =~ /^#!\s*(\S+)/) {
|
|
my $interpreter = get_full_path($1);
|
|
$sd{$profile}{$hat}{path}->{$interpreter}{mode} |= str_to_mode("ix");
|
|
$sd{$profile}{$hat}{path}->{$interpreter}{audit} |= 0;
|
|
if ($interpreter =~ /perl/) {
|
|
$sd{$profile}{$hat}{include}{"abstractions/perl"} = 1;
|
|
} elsif ($interpreter =~ m/\/bin\/(bash|sh)/) {
|
|
$sd{$profile}{$hat}{include}{"abstractions/bash"} = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# print "$pid $profile $hat EXEC $exec_target $ans $exec_mode\n";
|
|
|
|
# update our tracking info based on what kind of change
|
|
# this is...
|
|
if ($ans eq "CMD_ix") {
|
|
if ($hat) {
|
|
$profilechanges{$pid} = $profile . "//" . $hat;
|
|
} else {
|
|
$profilechanges{$pid} = $profile;
|
|
}
|
|
} elsif ($ans =~ /^CMD_(px|nx|pix|nix)/) {
|
|
$exec_target = $to_name if ($to_name);
|
|
if ($sdmode eq "PERMITTING") {
|
|
if ($domainchange eq "change") {
|
|
$profile = $exec_target;
|
|
$hat = $exec_target;
|
|
$profilechanges{$pid} = $profile;
|
|
}
|
|
}
|
|
# if they want to use px, make sure a profile
|
|
# exists for the target.
|
|
unless (-e getprofilefilename($exec_target)) {
|
|
my $ynans = "y";
|
|
if ($exec_mode & str_to_mode("i")) {
|
|
$ynans = UI_YesNo(sprintf(gettext("A profile for %s does not exist. Create one?"), $exec_target), "n");
|
|
}
|
|
if ($ynans eq "y") {
|
|
$helpers{$exec_target} = "enforce";
|
|
if ($to_name) {
|
|
autodep_base("", $exec_target);
|
|
} else {
|
|
autodep_base($exec_target, "");
|
|
}
|
|
reload_base($exec_target);
|
|
}
|
|
}
|
|
} elsif ($ans =~ /^CMD_(cx|cix)/) {
|
|
$exec_target = $to_name if ($to_name);
|
|
if ($sdmode eq "PERMITTING") {
|
|
if ($domainchange eq "change") {
|
|
$profilechanges{$pid} = "${profile}//${exec_target}";
|
|
# $profile = $exec_target;
|
|
# $hat = $exec_target;
|
|
}
|
|
}
|
|
|
|
# if they want to use cx, make sure a profile
|
|
# exists for the target.
|
|
unless ($sd{$profile}{$exec_target}) {
|
|
my $ynans = "y";
|
|
if ($exec_mode & str_to_mode("i")) {
|
|
$ynans = UI_YesNo(sprintf(gettext("A local profile for %s does not exist. Create one?"), $exec_target), "n");
|
|
}
|
|
if ($ynans eq "y") {
|
|
$hat = $exec_target;
|
|
# keep track of profile flags
|
|
#$profile_data->{$profile}{$hat}{flags} = ;
|
|
|
|
# we have seen more than a declaration so clear it
|
|
$sd{$profile}{$hat}{'declared'} = 0;
|
|
$sd{$profile}{$hat}{profile} = 1;
|
|
|
|
# Otherwise sub-profiles end up getting
|
|
# put in enforce mode with genprof
|
|
$sd{$profile}{$hat}{flags} = $sd{$profile}{$profile}{flags} if $profile ne $hat;
|
|
|
|
$sd{$profile}{$hat}{flags} = 'complain';
|
|
$sd{$profile}{$hat}{allow}{path} = { };
|
|
$sd{$profile}{$hat}{allow}{netdomain} = { };
|
|
my $file = $sd{$profile}{$profile}{filename};
|
|
$filelist{$file}{profiles}{$profile}{$hat} = 1;
|
|
|
|
}
|
|
}
|
|
} elsif ($ans =~ /^CMD_ux/) {
|
|
$profilechanges{$pid} = "unconfined";
|
|
return if $domainchange eq "change";
|
|
}
|
|
}
|
|
} elsif ( $type eq "netdomain" ) {
|
|
my ($pid, $p, $h, $prog, $sdmode, $family, $sock_type, $protocol) =
|
|
@entry;
|
|
|
|
if ( ($p !~ /null(-complain)*-profile/)
|
|
&& ($h !~ /null(-complain)*-profile/))
|
|
{
|
|
$profile = $p;
|
|
$hat = $h;
|
|
}
|
|
|
|
next unless $profile && $hat;
|
|
$prelog{$sdmode}
|
|
{$profile}
|
|
{$hat}
|
|
{netdomain}
|
|
{$family}
|
|
{$sock_type} = 1 unless ( !$family || !$sock_type );
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sub add_to_tree ($$$@) {
|
|
my ($pid, $parent, $type, @event) = @_;
|
|
if ( $DEBUGGING ) {
|
|
my $eventmsg = Data::Dumper->Dump([@event], [qw(*event)]);
|
|
$eventmsg =~ s/\n/ /g;
|
|
debug " add_to_tree: pid [$pid] type [$type] event [ $eventmsg ]";
|
|
}
|
|
|
|
unless (exists $pid{$pid}) {
|
|
my $profile = $event[0];
|
|
my $hat = $event[1];
|
|
if ($parent && exists $pid{$parent}) {
|
|
# fork entry is missing fake one so that fork tracking will work
|
|
$hat ||= "null-complain-profile";
|
|
my $arrayref = [];
|
|
push @{ $pid{$parent} }, $arrayref;
|
|
$pid{$pid} = $arrayref;
|
|
push @{$arrayref}, [ "fork", $pid, $profile, $hat ];
|
|
} else {
|
|
my $arrayref = [];
|
|
push @log, $arrayref;
|
|
$pid{$pid} = $arrayref;
|
|
}
|
|
}
|
|
|
|
push @{ $pid{$pid} }, [ $type, $pid, @event ];
|
|
}
|
|
|
|
#
|
|
# variables used in the logparsing routines
|
|
#
|
|
our $LOG;
|
|
our $next_log_entry;
|
|
our $logmark;
|
|
our $seenmark;
|
|
my $RE_LOG_v2_0_syslog = qr/SubDomain/;
|
|
my $RE_LOG_v2_1_syslog = qr/kernel:\s+(\[[\d\.\s]+\]\s+)?(audit\([\d\.\:]+\):\s+)?type=150[1-6]/;
|
|
my $RE_LOG_v2_6_syslog = qr/kernel:\s+(\[[\d\.\s]+\]\s+)?type=\d+\s+audit\([\d\.\:]+\):\s+apparmor=/;
|
|
my $RE_LOG_v2_0_audit =
|
|
qr/type=(APPARMOR|UNKNOWN\[1500\]) msg=audit\([\d\.\:]+\):/;
|
|
my $RE_LOG_v2_1_audit =
|
|
qr/type=(UNKNOWN\[150[1-6]\]|APPARMOR_(AUDIT|ALLOWED|DENIED|HINT|STATUS|ERROR))/;
|
|
my $RE_LOG_v2_6_audit =
|
|
qr/type=AVC\s+(msg=)?audit\([\d\.\:]+\):\s+apparmor=/;
|
|
|
|
sub prefetch_next_log_entry() {
|
|
# if we already have an existing cache entry, something's broken
|
|
if ($next_log_entry) {
|
|
print STDERR "Already had next log entry: $next_log_entry";
|
|
}
|
|
|
|
# read log entries until we either hit the end or run into an
|
|
# AA event message format we recognize
|
|
do {
|
|
$next_log_entry = <$LOG>;
|
|
$DEBUGGING && debug "prefetch_next_log_entry: next_log_entry = " . ($next_log_entry ? $next_log_entry : "empty");
|
|
} until (!$next_log_entry || $next_log_entry =~ m{
|
|
$RE_LOG_v2_0_syslog |
|
|
$RE_LOG_v2_0_audit |
|
|
$RE_LOG_v2_1_audit |
|
|
$RE_LOG_v2_1_syslog |
|
|
$RE_LOG_v2_6_syslog |
|
|
$RE_LOG_v2_6_audit |
|
|
$logmark
|
|
}x);
|
|
}
|
|
|
|
sub get_next_log_entry() {
|
|
# make sure we've got a next log entry if there is one
|
|
prefetch_next_log_entry() unless $next_log_entry;
|
|
|
|
# save a copy of the next log entry...
|
|
my $log_entry = $next_log_entry;
|
|
|
|
# zero out our cache of the next log entry
|
|
$next_log_entry = undef;
|
|
|
|
return $log_entry;
|
|
}
|
|
|
|
sub peek_at_next_log_entry() {
|
|
# make sure we've got a next log entry if there is one
|
|
prefetch_next_log_entry() unless $next_log_entry;
|
|
|
|
# return a copy of the next log entry without pulling it out of the cache
|
|
return $next_log_entry;
|
|
}
|
|
|
|
sub throw_away_next_log_entry() {
|
|
$next_log_entry = undef;
|
|
}
|
|
|
|
sub parse_log_record_v_2_0 ($$) {
|
|
my ($record, $last) = @_;
|
|
$DEBUGGING && debug "parse_log_record_v_2_0: $record";
|
|
|
|
# What's this early out for? As far as I can tell, parse_log_record_v_2_0
|
|
# won't ever be called without something in $record
|
|
return $last if ( ! $record );
|
|
|
|
$_ = $record;
|
|
|
|
if (s/(PERMITTING|REJECTING)-SYSLOGFIX/$1/) {
|
|
s/%%/%/g;
|
|
}
|
|
|
|
if (m/LOGPROF-HINT unknown_hat (\S+) pid=(\d+) profile=(.+) active=(.+)/) {
|
|
my ($uhat, $pid, $profile, $hat) = ($1, $2, $3, $4);
|
|
|
|
$last = $&;
|
|
|
|
# we want to ignore entries for profiles that don't exist
|
|
# they're most likely broken entries or old entries for
|
|
# deleted profiles
|
|
return $&
|
|
if ( ($profile ne 'null-complain-profile')
|
|
&& (!profile_exists($profile)));
|
|
|
|
add_to_tree($pid, 0, "unknown_hat", $profile, $hat,
|
|
"PERMITTING", $uhat);
|
|
} elsif (m/LOGPROF-HINT (unknown_profile|missing_mandatory_profile) image=(.+) pid=(\d+) profile=(.+) active=(.+)/) {
|
|
my ($image, $pid, $profile, $hat) = ($2, $3, $4, $5);
|
|
|
|
return $& if $last =~ /PERMITTING x access to $image/;
|
|
$last = $&;
|
|
|
|
# we want to ignore entries for profiles that don't exist
|
|
# they're most likely broken entries or old entries for
|
|
# deleted profiles
|
|
return $&
|
|
if ( ($profile ne 'null-complain-profile')
|
|
&& (!profile_exists($profile)));
|
|
|
|
add_to_tree($pid, 0, "exec", $profile, $hat, "HINT", "PERMITTING", "x", $image);
|
|
|
|
} elsif (m/(PERMITTING|REJECTING) (\S+) access (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
|
|
my ($sdmode, $mode, $detail, $prog, $pid, $profile, $hat) =
|
|
($1, $2, $3, $4, $5, $6, $7);
|
|
|
|
if ($mode eq "link") {
|
|
$mode = "l";
|
|
}
|
|
if (!validate_log_mode($mode)) {
|
|
fatal_error(sprintf(gettext('Log contains unknown mode %s.'), $mode));
|
|
}
|
|
|
|
my $domainchange = "nochange";
|
|
if ($mode =~ /x/) {
|
|
|
|
# we need to try to check if we're doing a domain transition
|
|
if ($sdmode eq "PERMITTING") {
|
|
my $following = peek_at_next_log_entry();
|
|
|
|
if ($following && ($following =~ m/changing_profile/)) {
|
|
$domainchange = "change";
|
|
throw_away_next_log_entry();
|
|
}
|
|
}
|
|
} else {
|
|
|
|
# we want to ignore duplicates for things other than executes...
|
|
return $& if $seen{$&};
|
|
$seen{$&} = 1;
|
|
}
|
|
|
|
$last = $&;
|
|
|
|
# we want to ignore entries for profiles that don't exist
|
|
# they're most likely broken entries or old entries for
|
|
# deleted profiles
|
|
if (($profile ne 'null-complain-profile')
|
|
&& (!profile_exists($profile)))
|
|
{
|
|
return $&;
|
|
}
|
|
|
|
# currently no way to stick pipe mediation in a profile, ignore
|
|
# any messages like this
|
|
return $& if $detail =~ /to pipe:/;
|
|
|
|
# strip out extra extended attribute info since we don't
|
|
# currently have a way to specify it in the profile and
|
|
# instead just need to provide the access to the base filename
|
|
$detail =~ s/\s+extended attribute \S+//;
|
|
|
|
# kerberos code checks to see if the krb5.conf file is world
|
|
# writable in a stupid way so we'll ignore any w accesses to
|
|
# krb5.conf
|
|
return $& if (($detail eq "to /etc/krb5.conf") && contains($mode, "w"));
|
|
|
|
# strip off the (deleted) tag that gets added if it's a
|
|
# deleted file
|
|
$detail =~ s/\s+\(deleted\)$//;
|
|
|
|
# next if (($detail =~ /to \/lib\/ld-/) && ($mode =~ /x/));
|
|
|
|
$detail =~ s/^to\s+//;
|
|
|
|
if ($domainchange eq "change") {
|
|
add_to_tree($pid, 0, "exec", $profile, $hat, $prog,
|
|
$sdmode, str_to_mode($mode), $detail);
|
|
} else {
|
|
add_to_tree($pid, 0, "path", $profile, $hat, $prog,
|
|
$sdmode, str_to_mode($mode), $detail);
|
|
}
|
|
|
|
} elsif (m/(PERMITTING|REJECTING) (?:mk|rm)dir on (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
|
|
my ($sdmode, $path, $prog, $pid, $profile, $hat) =
|
|
($1, $2, $3, $4, $5, $6);
|
|
|
|
# we want to ignore duplicates for things other than executes...
|
|
return $& if $seen{$&}++;
|
|
|
|
$last = $&;
|
|
|
|
# we want to ignore entries for profiles that don't exist
|
|
# they're most likely broken entries or old entries for
|
|
# deleted profiles
|
|
return $&
|
|
if ( ($profile ne 'null-complain-profile')
|
|
&& (!profile_exists($profile)));
|
|
|
|
add_to_tree($pid, 0, "path", $profile, $hat, $prog, $sdmode,
|
|
"w", $path);
|
|
|
|
} elsif (m/(PERMITTING|REJECTING) xattr (\S+) on (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
|
|
my ($sdmode, $xattr_op, $path, $prog, $pid, $profile, $hat) =
|
|
($1, $2, $3, $4, $5, $6, $7);
|
|
|
|
# we want to ignore duplicates for things other than executes...
|
|
return $& if $seen{$&}++;
|
|
|
|
$last = $&;
|
|
|
|
# we want to ignore entries for profiles that don't exist
|
|
# they're most likely broken entries or old entries for
|
|
# deleted profiles
|
|
return $&
|
|
if ( ($profile ne 'null-complain-profile')
|
|
&& (!profile_exists($profile)));
|
|
|
|
my $xattrmode;
|
|
if ($xattr_op eq "get" || $xattr_op eq "list") {
|
|
$xattrmode = "r";
|
|
} elsif ($xattr_op eq "set" || $xattr_op eq "remove") {
|
|
$xattrmode = "w";
|
|
}
|
|
|
|
if ($xattrmode) {
|
|
add_to_tree($pid, 0, "path", $profile, $hat, $prog, $sdmode,
|
|
str_to_mode($xattrmode), $path);
|
|
}
|
|
|
|
} elsif (m/(PERMITTING|REJECTING) attribute \((.*?)\) change to (.+) \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
|
|
my ($sdmode, $change, $path, $prog, $pid, $profile, $hat) =
|
|
($1, $2, $3, $4, $5, $6, $7);
|
|
|
|
# we want to ignore duplicates for things other than executes...
|
|
return $& if $seen{$&};
|
|
$seen{$&} = 1;
|
|
|
|
$last = $&;
|
|
|
|
# we want to ignore entries for profiles that don't exist
|
|
# they're most likely broken entries or old entries for
|
|
# deleted profiles
|
|
return $&
|
|
if ( ($profile ne 'null-complain-profile')
|
|
&& (!profile_exists($profile)));
|
|
|
|
# kerberos code checks to see if the krb5.conf file is world
|
|
# writable in a stupid way so we'll ignore any w accesses to
|
|
# krb5.conf
|
|
return $& if $path eq "/etc/krb5.conf";
|
|
|
|
add_to_tree($pid, 0, "path", $profile, $hat, $prog, $sdmode,
|
|
str_to_mode("w"), $path);
|
|
|
|
} elsif (m/(PERMITTING|REJECTING) access to capability '(\S+)' \((.+)\((\d+)\) profile (.+) active (.+)\)/) {
|
|
my ($sdmode, $capability, $prog, $pid, $profile, $hat) =
|
|
($1, $2, $3, $4, $5, $6);
|
|
|
|
return $& if $seen{$&};
|
|
|
|
$seen{$&} = 1;
|
|
$last = $&;
|
|
|
|
# we want to ignore entries for profiles that don't exist - they're
|
|
# most likely broken entries or old entries for deleted profiles
|
|
return $&
|
|
if ( ($profile ne 'null-complain-profile')
|
|
&& (!profile_exists($profile)));
|
|
|
|
add_to_tree($pid, 0, "capability", $profile, $hat, $prog,
|
|
$sdmode, $capability);
|
|
|
|
} elsif (m/Fork parent (\d+) child (\d+) profile (.+) active (.+)/
|
|
|| m/LOGPROF-HINT fork pid=(\d+) child=(\d+) profile=(.+) active=(.+)/
|
|
|| m/LOGPROF-HINT fork pid=(\d+) child=(\d+)/)
|
|
{
|
|
my ($parent, $child, $profile, $hat) = ($1, $2, $3, $4);
|
|
|
|
$profile ||= "null-complain-profile";
|
|
$hat ||= "null-complain-profile";
|
|
|
|
$last = $&;
|
|
|
|
# we want to ignore entries for profiles that don't exist
|
|
# they're most likely broken entries or old entries for
|
|
# deleted profiles
|
|
return $&
|
|
if ( ($profile ne 'null-complain-profile')
|
|
&& (!profile_exists($profile)));
|
|
|
|
my $arrayref = [];
|
|
if (exists $pid{$parent}) {
|
|
push @{ $pid{$parent} }, $arrayref;
|
|
} else {
|
|
push @log, $arrayref;
|
|
}
|
|
$pid{$child} = $arrayref;
|
|
push @{$arrayref}, [ "fork", $child, $profile, $hat ];
|
|
} else {
|
|
$DEBUGGING && debug "UNHANDLED: $_";
|
|
}
|
|
return $last;
|
|
}
|
|
|
|
sub parse_log_record ($) {
|
|
my $record = shift;
|
|
$DEBUGGING && debug "parse_log_record: $record";
|
|
my $e = parse_event($record);
|
|
|
|
return $e;
|
|
}
|
|
|
|
|
|
sub add_event_to_tree ($) {
|
|
my $e = shift;
|
|
|
|
my $sdmode = $e->{sdmode}?$e->{sdmode}:"UNKNOWN";
|
|
if ( $e->{type} ) {
|
|
if ( $e->{type} =~ /(UNKNOWN\[1501\]|APPARMOR_AUDIT|1501)/ ) {
|
|
$sdmode = "AUDIT";
|
|
} elsif ( $e->{type} =~ /(UNKNOWN\[1502\]|APPARMOR_ALLOWED|1502)/ ) {
|
|
$sdmode = "PERMITTING";
|
|
} elsif ( $e->{type} =~ /(UNKNOWN\[1503\]|APPARMOR_DENIED|1503)/ ) {
|
|
$sdmode = "REJECTING";
|
|
} elsif ( $e->{type} =~ /(UNKNOWN\[1504\]|APPARMOR_HINT|1504)/ ) {
|
|
$sdmode = "HINT";
|
|
} elsif ( $e->{type} =~ /(UNKNOWN\[1505\]|APPARMOR_STATUS|1505)/ ) {
|
|
$sdmode = "STATUS";
|
|
} elsif ( $e->{type} =~ /(UNKNOWN\[1506\]|APPARMOR_ERROR|1506)/ ) {
|
|
$sdmode = "ERROR";
|
|
} else {
|
|
$sdmode = "UNKNOWN";
|
|
}
|
|
}
|
|
return if ( $sdmode =~ /UNKNOWN|AUDIT|STATUS|ERROR/ );
|
|
return if ($e->{operation} =~ /profile_set/);
|
|
|
|
my ($profile, $hat);
|
|
|
|
# The version of AppArmor that was accepted into the mainline kernel
|
|
# issues audit events for things like change_hat while unconfined.
|
|
# Previous versions just returned -EPERM without the audit so the
|
|
# events wouldn't have been picked up here.
|
|
return if (!$e->{profile});
|
|
|
|
# just convert new null profile style names to old before we begin processing
|
|
# profile and name can contain multiple layers of null- but all we care about
|
|
# currently is single level.
|
|
if ($e->{profile} =~ m/\/\/null-/) {
|
|
$e->{profile} = "null-complain-profile";
|
|
}
|
|
($profile, $hat) = split /\/\//, $e->{profile};
|
|
if ( $e->{operation} eq "change_hat" ) {
|
|
#screen out change_hat events that aren't part of learning, as before
|
|
#AppArmor 2.4 these events only happend as hints during learning
|
|
return if ($sdmode ne "HINT" && $sdmode ne "PERMITTING");
|
|
($profile, $hat) = split /\/\//, $e->{name};
|
|
}
|
|
$hat = $profile if ( !$hat );
|
|
# TODO - refactor add_to_tree as prog is no longer supplied
|
|
# HINT is from previous format where prog was not
|
|
# consistently passed
|
|
my $prog = "HINT";
|
|
|
|
return if ($profile ne 'null-complain-profile' && !profile_exists($profile));
|
|
|
|
if ($e->{operation} eq "exec") {
|
|
if ( defined $e->{info} && $e->{info} eq "mandatory profile missing" ) {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"exec",
|
|
$profile,
|
|
$hat,
|
|
$sdmode,
|
|
"PERMITTING",
|
|
$e->{denied_mask},
|
|
$e->{name},
|
|
$e->{name2}
|
|
);
|
|
} elsif ( defined $e->{name2} && $e->{name2} =~ m/\/\/null-/) {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"exec",
|
|
$profile,
|
|
$hat,
|
|
$prog,
|
|
$sdmode,
|
|
$e->{denied_mask},
|
|
$e->{name},
|
|
""
|
|
);
|
|
}
|
|
} elsif ($e->{operation} =~ m/file_/) {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"path",
|
|
$profile,
|
|
$hat,
|
|
$prog,
|
|
$sdmode,
|
|
$e->{denied_mask},
|
|
$e->{name},
|
|
"",
|
|
);
|
|
} elsif ($e->{operation} eq "open" ||
|
|
$e->{operation} eq "truncate" ||
|
|
$e->{operation} eq "mkdir" ||
|
|
$e->{operation} eq "rename_src" ||
|
|
$e->{operation} eq "rename_dest") {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"path",
|
|
$profile,
|
|
$hat,
|
|
$prog,
|
|
$sdmode,
|
|
$e->{denied_mask},
|
|
$e->{name},
|
|
"",
|
|
);
|
|
} elsif ($e->{operation} eq "capable") {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"capability",
|
|
$profile,
|
|
$hat,
|
|
$prog,
|
|
$sdmode,
|
|
$e->{name}
|
|
);
|
|
} elsif ($e->{operation} =~ m/xattr/ ||
|
|
$e->{operation} eq "setattr") {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"path",
|
|
$profile,
|
|
$hat,
|
|
$prog,
|
|
$sdmode,
|
|
$e->{denied_mask},
|
|
$e->{name},
|
|
""
|
|
);
|
|
} elsif ($e->{operation} =~ m/inode_/) {
|
|
my $is_domain_change = 0;
|
|
|
|
if ($e->{operation} eq "inode_permission" &&
|
|
$e->{denied_mask} & $AA_MAY_EXEC &&
|
|
$sdmode eq "PERMITTING") {
|
|
|
|
my $following = peek_at_next_log_entry();
|
|
if ($following) {
|
|
my $entry = parse_log_record($following);
|
|
if ($entry &&
|
|
$entry->{info} &&
|
|
$entry->{info} eq "set profile" ) {
|
|
|
|
$is_domain_change = 1;
|
|
throw_away_next_log_entry();
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($is_domain_change) {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"exec",
|
|
$profile,
|
|
$hat,
|
|
$prog,
|
|
$sdmode,
|
|
$e->{denied_mask},
|
|
$e->{name},
|
|
$e->{name2}
|
|
);
|
|
} else {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"path",
|
|
$profile,
|
|
$hat,
|
|
$prog,
|
|
$sdmode,
|
|
$e->{denied_mask},
|
|
$e->{name},
|
|
""
|
|
);
|
|
}
|
|
} elsif ($e->{operation} eq "sysctl") {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"path",
|
|
$profile,
|
|
$hat,
|
|
$prog,
|
|
$sdmode,
|
|
$e->{denied_mask},
|
|
$e->{name},
|
|
""
|
|
);
|
|
} elsif ($e->{operation} eq "clone") {
|
|
my ($parent, $child) = ($e->{pid}, $e->{task});
|
|
$profile ||= "null-complain-profile";
|
|
$hat ||= "null-complain-profile";
|
|
my $arrayref = [];
|
|
if (exists $pid{$parent}) {
|
|
push @{ $pid{$parent} }, $arrayref;
|
|
} else {
|
|
push @log, $arrayref;
|
|
}
|
|
$pid{$child} = $arrayref;
|
|
push @{$arrayref}, [ "fork", $child, $profile, $hat ];
|
|
} elsif (optype($e->{operation}) eq "net") {
|
|
add_to_tree( $e->{pid},
|
|
$e->{parent},
|
|
"netdomain",
|
|
$profile,
|
|
$hat,
|
|
$prog,
|
|
$sdmode,
|
|
$e->{family},
|
|
$e->{sock_type},
|
|
$e->{protocol},
|
|
);
|
|
} elsif ($e->{operation} eq "change_hat") {
|
|
add_to_tree($e->{pid}, $e->{parent}, "unknown_hat", $profile, $hat, $sdmode, $hat);
|
|
} else {
|
|
if ( $DEBUGGING ) {
|
|
my $msg = Data::Dumper->Dump([$e], [qw(*event)]);
|
|
debug "UNHANDLED: $msg";
|
|
}
|
|
}
|
|
}
|
|
|
|
sub read_log($) {
|
|
$logmark = shift;
|
|
$seenmark = $logmark ? 0 : 1;
|
|
my $last;
|
|
my $event_type;
|
|
|
|
# okay, done loading the previous profiles, get on to the good stuff...
|
|
open($LOG, $filename)
|
|
or fatal_error "Can't read AppArmor logfile $filename: $!";
|
|
while ($_ = get_next_log_entry()) {
|
|
chomp;
|
|
|
|
$DEBUGGING && debug "read_log: $_";
|
|
|
|
$seenmark = 1 if /$logmark/;
|
|
|
|
$DEBUGGING && debug "read_log: seenmark = $seenmark";
|
|
next unless $seenmark;
|
|
|
|
my $last_match = ""; # v_2_0 syslog record parsing requires
|
|
# the previous aa record in the mandatory profile
|
|
# case
|
|
# all we care about is apparmor messages
|
|
if (/$RE_LOG_v2_0_syslog/ || /$RE_LOG_v2_0_audit/) {
|
|
$last_match = parse_log_record_v_2_0( $_, $last_match );
|
|
} else {
|
|
my $event = parse_log_record($_);
|
|
add_event_to_tree($event) if ( $event );
|
|
}
|
|
}
|
|
close($LOG);
|
|
$logmark = "";
|
|
}
|
|
|
|
|
|
sub UI_SelectUpdatedRepoProfile ($$) {
|
|
|
|
my ($profile, $p) = @_;
|
|
my $distro = $cfg->{repository}{distro};
|
|
my $url = $sd{$profile}{$profile}{repo}{url};
|
|
my $user = $sd{$profile}{$profile}{repo}{user};
|
|
my $id = $sd{$profile}{$profile}{repo}{id};
|
|
my $updated = 0;
|
|
|
|
if ($p) {
|
|
my $q = { };
|
|
$q->{headers} = [
|
|
"Profile", $profile,
|
|
"User", $user,
|
|
"Old Revision", $id,
|
|
"New Revision", $p->{id},
|
|
];
|
|
$q->{explanation} =
|
|
gettext( "An updated version of this profile has been found in the profile repository. Would you like to use it?");
|
|
$q->{functions} = [
|
|
"CMD_VIEW_CHANGES", "CMD_UPDATE_PROFILE", "CMD_IGNORE_UPDATE",
|
|
"CMD_ABORT", "CMD_FINISHED"
|
|
];
|
|
|
|
my $ans;
|
|
do {
|
|
$ans = UI_PromptUser($q);
|
|
|
|
if ($ans eq "CMD_VIEW_CHANGES") {
|
|
my $oldprofile = serialize_profile($sd{$profile}, $profile);
|
|
my $newprofile = $p->{profile};
|
|
display_changes($oldprofile, $newprofile);
|
|
}
|
|
} until $ans =~ /^CMD_(UPDATE_PROFILE|IGNORE_UPDATE)/;
|
|
|
|
if ($ans eq "CMD_UPDATE_PROFILE") {
|
|
eval {
|
|
my $profile_data =
|
|
parse_profile_data($p->{profile}, getprofilefilename($profile), 0);
|
|
if ($profile_data) {
|
|
attach_profile_data(\%sd, $profile_data);
|
|
$changed{$profile} = 1;
|
|
}
|
|
|
|
set_repo_info($sd{$profile}{$profile}, $url, $user, $p->{id});
|
|
|
|
UI_Info(
|
|
sprintf(
|
|
gettext("Updated profile %s to revision %s."),
|
|
$profile, $p->{id}
|
|
)
|
|
);
|
|
};
|
|
|
|
if ($@) {
|
|
UI_Info(gettext("Error parsing repository profile."));
|
|
} else {
|
|
$updated = 1;
|
|
}
|
|
}
|
|
}
|
|
return $updated;
|
|
}
|
|
|
|
sub UI_repo_signup() {
|
|
|
|
my ($url, $res, $save_config, $newuser, $user, $pass, $email, $signup_okay);
|
|
$url = $cfg->{repository}{url};
|
|
do {
|
|
if ($UI_Mode eq "yast") {
|
|
SendDataToYast(
|
|
{
|
|
type => "dialog-repo-sign-in",
|
|
repo_url => $url
|
|
}
|
|
);
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
$email = $yarg->{email};
|
|
$user = $yarg->{user};
|
|
$pass = $yarg->{pass};
|
|
$newuser = $yarg->{newuser};
|
|
$save_config = $yarg->{save_config};
|
|
if ($yarg->{cancelled} && $yarg->{cancelled} eq "y") {
|
|
return;
|
|
}
|
|
$DEBUGGING && debug("AppArmor Repository: \n\t " .
|
|
($newuser eq "1") ?
|
|
"New User\n\temail: [" . $email . "]" :
|
|
"Signin" . "\n\t user[" . $user . "]" .
|
|
"password [" . $pass . "]\n");
|
|
} else {
|
|
$newuser = UI_YesNo(gettext("Create New User?"), "n");
|
|
$user = UI_GetString(gettext("Username: "), $user);
|
|
$pass = UI_GetString(gettext("Password: "), $pass);
|
|
$email = UI_GetString(gettext("Email Addr: "), $email)
|
|
if ($newuser eq "y");
|
|
$save_config = UI_YesNo(gettext("Save Configuration? "), "y");
|
|
}
|
|
|
|
if ($newuser eq "y") {
|
|
my ($status_ok,$res) = user_register($url, $user, $pass, $email);
|
|
if ($status_ok) {
|
|
$signup_okay = 1;
|
|
} else {
|
|
my $errmsg =
|
|
gettext("The Profile Repository server returned the following error:") .
|
|
"\n" . $res?$res:gettext("UNKOWN ERROR") . "\n" .
|
|
gettext("Please re-enter registration information or contact the administrator.");
|
|
UI_Important(gettext("Login Error\n") . $errmsg);
|
|
}
|
|
} else {
|
|
my ($status_ok,$res) = user_login($url, $user, $pass);
|
|
if ($status_ok) {
|
|
$signup_okay = 1;
|
|
} else {
|
|
my $errmsg = gettext("Login failure\n Please check username and password and try again.") . "\n" . $res;
|
|
UI_Important($errmsg);
|
|
}
|
|
}
|
|
} until $signup_okay;
|
|
|
|
$repo_cfg->{repository}{user} = $user;
|
|
$repo_cfg->{repository}{pass} = $pass;
|
|
$repo_cfg->{repository}{email} = $email;
|
|
|
|
write_config("repository.conf", $repo_cfg) if ( $save_config eq "y" );
|
|
|
|
return ($user, $pass);
|
|
}
|
|
|
|
sub UI_ask_to_enable_repo() {
|
|
|
|
my $q = { };
|
|
return if ( not defined $cfg->{repository}{url} );
|
|
$q->{headers} = [
|
|
gettext("Repository"), $cfg->{repository}{url},
|
|
];
|
|
$q->{explanation} = gettext( "Would you like to enable access to the
|
|
profile repository?" ); $q->{functions} = [ "CMD_ENABLE_REPO",
|
|
"CMD_DISABLE_REPO", "CMD_ASK_LATER", ];
|
|
|
|
my $cmd;
|
|
do {
|
|
$cmd = UI_PromptUser($q);
|
|
} until $cmd =~ /^CMD_(ENABLE_REPO|DISABLE_REPO|ASK_LATER)/;
|
|
|
|
if ($cmd eq "CMD_ENABLE_REPO") {
|
|
$repo_cfg->{repository}{enabled} = "yes";
|
|
} elsif ($cmd eq "CMD_DISABLE_REPO") {
|
|
$repo_cfg->{repository}{enabled} = "no";
|
|
} elsif ($cmd eq "CMD_ASK_LATER") {
|
|
$repo_cfg->{repository}{enabled} = "later";
|
|
}
|
|
|
|
eval { write_config("repository.conf", $repo_cfg) };
|
|
if ($@) {
|
|
fatal_error($@);
|
|
}
|
|
}
|
|
|
|
|
|
sub UI_ask_to_upload_profiles() {
|
|
|
|
my $q = { };
|
|
$q->{headers} = [
|
|
gettext("Repository"), $cfg->{repository}{url},
|
|
];
|
|
$q->{explanation} =
|
|
gettext( "Would you like to upload newly created and changed profiles to
|
|
the profile repository?" );
|
|
$q->{functions} = [
|
|
"CMD_YES", "CMD_NO", "CMD_ASK_LATER",
|
|
];
|
|
|
|
my $cmd;
|
|
do {
|
|
$cmd = UI_PromptUser($q);
|
|
} until $cmd =~ /^CMD_(YES|NO|ASK_LATER)/;
|
|
|
|
if ($cmd eq "CMD_NO") {
|
|
$repo_cfg->{repository}{upload} = "no";
|
|
} elsif ($cmd eq "CMD_YES") {
|
|
$repo_cfg->{repository}{upload} = "yes";
|
|
} elsif ($cmd eq "CMD_ASK_LATER") {
|
|
$repo_cfg->{repository}{upload} = "later";
|
|
}
|
|
|
|
eval { write_config("repository.conf", $repo_cfg) };
|
|
if ($@) {
|
|
fatal_error($@);
|
|
}
|
|
}
|
|
|
|
|
|
sub parse_repo_profile($$$) {
|
|
my ($fqdbin, $repo_url, $profile) = @_;
|
|
|
|
my $profile_data = eval {
|
|
parse_profile_data($profile->{profile}, getprofilefilename($fqdbin), 0);
|
|
};
|
|
if ($@) {
|
|
print STDERR "PARSING ERROR: $@\n";
|
|
$profile_data = undef;
|
|
}
|
|
|
|
if ($profile_data) {
|
|
set_repo_info($profile_data->{$fqdbin}{$fqdbin}, $repo_url,
|
|
$profile->{username}, $profile->{id});
|
|
}
|
|
|
|
return $profile_data;
|
|
}
|
|
|
|
|
|
sub set_repo_info($$$$) {
|
|
my ($profile_data, $repo_url, $username, $id) = @_;
|
|
|
|
# save repository metadata
|
|
$profile_data->{repo}{url} = $repo_url;
|
|
$profile_data->{repo}{user} = $username;
|
|
$profile_data->{repo}{id} = $id;
|
|
}
|
|
|
|
|
|
sub is_repo_profile($) {
|
|
my $profile_data = shift;
|
|
|
|
return $profile_data->{repo}{url} &&
|
|
$profile_data->{repo}{user} &&
|
|
$profile_data->{repo}{id};
|
|
}
|
|
|
|
|
|
sub get_repo_user_pass() {
|
|
my ($user, $pass);
|
|
|
|
if ($repo_cfg) {
|
|
$user = $repo_cfg->{repository}{user};
|
|
$pass = $repo_cfg->{repository}{pass};
|
|
}
|
|
|
|
unless ($user && $pass) {
|
|
($user, $pass) = UI_repo_signup();
|
|
}
|
|
|
|
return ($user, $pass);
|
|
}
|
|
|
|
|
|
sub get_preferred_user ($) {
|
|
my $repo_url = shift;
|
|
return $cfg->{repository}{preferred_user} || "NOVELL";
|
|
}
|
|
|
|
|
|
sub repo_is_enabled () {
|
|
my $enabled;
|
|
if ($cfg->{repository}{url} &&
|
|
$repo_cfg &&
|
|
$repo_cfg->{repository}{enabled} &&
|
|
$repo_cfg->{repository}{enabled} eq "yes") {
|
|
$enabled = 1;
|
|
}
|
|
return $enabled;
|
|
}
|
|
|
|
|
|
sub update_repo_profile($) {
|
|
my $profile = shift;
|
|
|
|
return undef if ( not is_repo_profile($profile) );
|
|
my $distro = $cfg->{repository}{distro};
|
|
my $url = $profile->{repo}{url};
|
|
my $user = $profile->{repo}{user};
|
|
my $id = $profile->{repo}{id};
|
|
|
|
UI_BusyStart( gettext("Connecting to repository.....") );
|
|
my ($status_ok,$res) = fetch_newer_profile( $url,
|
|
$distro,
|
|
$user,
|
|
$id,
|
|
$profile->{name}
|
|
);
|
|
UI_BusyStop();
|
|
if ( ! $status_ok ) {
|
|
my $errmsg =
|
|
sprintf(
|
|
gettext("WARNING: Profile update check failed\nError Detail:\n%s"),
|
|
defined $res?$res:gettext("UNKNOWN ERROR"));
|
|
UI_Important($errmsg);
|
|
$res = undef;
|
|
}
|
|
return( $res );
|
|
}
|
|
|
|
sub UI_ask_mode_toggles ($$$) {
|
|
my ($audit_toggle, $owner_toggle, $oldmode) = @_;
|
|
my $q = { };
|
|
$q->{headers} = [ ];
|
|
# "Repository", $cfg->{repository}{url},
|
|
# ];
|
|
$q->{explanation} = gettext( "Change mode modifiers");
|
|
|
|
if ($audit_toggle) {
|
|
$q->{functions} = [ "CMD_AUDIT_OFF" ];
|
|
} else {
|
|
$q->{functions} = [ "CMD_AUDIT_NEW" ];
|
|
push @{$q->{functions}}, "CMD_AUDIT_FULL" if ($oldmode);
|
|
}
|
|
|
|
if ($owner_toggle) {
|
|
push @{$q->{functions}}, "CMD_USER_OFF";
|
|
} else {
|
|
push @{$q->{functions}}, "CMD_USER_ON";
|
|
}
|
|
push @{$q->{functions}}, "CMD_CONTINUE";
|
|
|
|
my $cmd;
|
|
do {
|
|
$cmd = UI_PromptUser($q);
|
|
} until $cmd =~ /^CMD_(AUDIT_OFF|AUDIT_NEW|AUDIT_FULL|USER_ON|USER_OFF|CONTINUE)/;
|
|
|
|
if ($cmd eq "CMD_AUDIT_OFF") {
|
|
$audit_toggle = 0;
|
|
} elsif ($cmd eq "CMD_AUDIT_NEW") {
|
|
$audit_toggle = 1;
|
|
} elsif ($cmd eq "CMD_AUDIT_FULL") {
|
|
$audit_toggle = 2;
|
|
} elsif ($cmd eq "CMD_USER_ON") {
|
|
$owner_toggle = 1;
|
|
} elsif ($cmd eq "CMD_USER_OFF") {
|
|
$owner_toggle = 0;
|
|
# $owner_toggle++;
|
|
# $owner_toggle++ if (!$oldmode && $owner_toggle == 2);
|
|
# $owner_toggle = 0 if ($owner_toggle > 3);
|
|
}
|
|
return ($audit_toggle, $owner_toggle);
|
|
}
|
|
|
|
sub ask_the_questions() {
|
|
my $found; # do the magic foo-foo
|
|
for my $sdmode (sort keys %log) {
|
|
|
|
# let them know what sort of changes we're about to list...
|
|
if ($sdmode eq "PERMITTING") {
|
|
UI_Info(gettext("Complain-mode changes:"));
|
|
} elsif ($sdmode eq "REJECTING") {
|
|
UI_Info(gettext("Enforce-mode changes:"));
|
|
} else {
|
|
|
|
# if we're not permitting and not rejecting, something's broken.
|
|
# most likely the code we're using to build the hash tree of log
|
|
# entries - this should never ever happen
|
|
fatal_error(sprintf(gettext('Invalid mode found: %s'), $sdmode));
|
|
}
|
|
|
|
for my $profile (sort keys %{ $log{$sdmode} }) {
|
|
my $p = update_repo_profile($sd{$profile}{$profile});
|
|
UI_SelectUpdatedRepoProfile($profile, $p) if ( $p );
|
|
|
|
$found++;
|
|
|
|
# this sorts the list of hats, but makes sure that the containing
|
|
# profile shows up in the list first to keep the question order
|
|
# rational
|
|
my @hats =
|
|
grep { $_ ne $profile } keys %{ $log{$sdmode}{$profile} };
|
|
unshift @hats, $profile
|
|
if defined $log{$sdmode}{$profile}{$profile};
|
|
|
|
for my $hat (@hats) {
|
|
|
|
# step through all the capabilities first...
|
|
for my $capability (sort keys %{ $log{$sdmode}{$profile}{$hat}{capability} }) {
|
|
|
|
# we don't care about it if we've already added it to the
|
|
# profile
|
|
next if profile_known_capability($sd{$profile}{$hat},
|
|
$capability);
|
|
|
|
my $severity = $sevdb->rank(uc("cap_$capability"));
|
|
|
|
my $defaultoption = 1;
|
|
my @options = ();
|
|
my @newincludes;
|
|
@newincludes = matchcapincludes($sd{$profile}{$hat},
|
|
$capability);
|
|
|
|
|
|
my $q = {};
|
|
|
|
if (@newincludes) {
|
|
push @options,
|
|
map { "#include <$_>" } sort(uniq(@newincludes));
|
|
}
|
|
|
|
if ( @options ) {
|
|
push @options, "capability $capability";
|
|
$q->{options} = [@options];
|
|
$q->{selected} = $defaultoption - 1;
|
|
}
|
|
|
|
$q->{headers} = [];
|
|
push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
|
|
push @{ $q->{headers} }, gettext("Capability"), $capability;
|
|
push @{ $q->{headers} }, gettext("Severity"), $severity;
|
|
|
|
my $audit_toggle = 0;
|
|
$q->{functions} = [
|
|
"CMD_ALLOW", "CMD_DENY", "CMD_AUDIT_NEW", "CMD_ABORT", "CMD_FINISHED"
|
|
];
|
|
|
|
# complain-mode events default to allow - enforce defaults
|
|
# to deny
|
|
$q->{default} = ($sdmode eq "PERMITTING") ? "CMD_ALLOW" : "CMD_DENY";
|
|
|
|
$seenevents++;
|
|
my $done = 0;
|
|
while ( not $done ) {
|
|
# what did the grand exalted master tell us to do?
|
|
my ($ans, $selected) = UI_PromptUser($q);
|
|
|
|
if ($ans =~ /^CMD_AUDIT/) {
|
|
$audit_toggle = !$audit_toggle;
|
|
my $audit = "";
|
|
if ($audit_toggle) {
|
|
$q->{functions} = [
|
|
"CMD_ALLOW", "CMD_DENY", "CMD_AUDIT_OFF", "CMD_ABORT", "CMD_FINISHED"
|
|
];
|
|
$audit = "audit ";
|
|
} else {
|
|
$q->{functions} = [
|
|
"CMD_ALLOW", "CMD_DENY", "CMD_AUDIT_NEW", "CMD_ABORT", "CMD_FINISHED"
|
|
];
|
|
}
|
|
$q->{headers} = [];
|
|
push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
|
|
push @{ $q->{headers} }, gettext("Capability"), $audit . $capability;
|
|
push @{ $q->{headers} }, gettext("Severity"), $severity;
|
|
|
|
} if ($ans eq "CMD_ALLOW") {
|
|
|
|
# they picked (a)llow, so...
|
|
|
|
my $selection = $options[$selected];
|
|
$done = 1;
|
|
if ($selection &&
|
|
$selection =~ m/^#include <(.+)>$/) {
|
|
my $deleted = 0;
|
|
my $inc = $1;
|
|
$deleted = delete_duplicates($sd{$profile}{$hat},
|
|
$inc
|
|
);
|
|
$sd{$profile}{$hat}{include}{$inc} = 1;
|
|
|
|
$changed{$profile} = 1;
|
|
UI_Info(sprintf(
|
|
gettext('Adding #include <%s> to profile.'),
|
|
$inc));
|
|
UI_Info(sprintf(
|
|
gettext('Deleted %s previous matching profile entries.'),
|
|
$deleted)) if $deleted;
|
|
}
|
|
# stick the capability into the profile
|
|
$sd{$profile}{$hat}{allow}{capability}{$capability}{set} = 1;
|
|
$sd{$profile}{$hat}{allow}{capability}{$capability}{audit} = $audit_toggle;
|
|
|
|
# mark this profile as changed
|
|
$changed{$profile} = 1;
|
|
$done = 1;
|
|
# give a little feedback to the user
|
|
UI_Info(sprintf(gettext('Adding capability %s to profile.'), $capability));
|
|
} elsif ($ans eq "CMD_DENY") {
|
|
$sd{$profile}{$hat}{deny}{capability}{$capability}{set} = 1;
|
|
# mark this profile as changed
|
|
$changed{$profile} = 1;
|
|
UI_Info(sprintf(gettext('Denying capability %s to profile.'), $capability));
|
|
$done = 1;
|
|
} else {
|
|
redo;
|
|
}
|
|
}
|
|
}
|
|
|
|
# and then step through all of the path entries...
|
|
for my $path (sort keys %{ $log{$sdmode}{$profile}{$hat}{path} }) {
|
|
|
|
my $mode = $log{$sdmode}{$profile}{$hat}{path}{$path};
|
|
|
|
# do original profile lookup once.
|
|
|
|
my $allow_mode = 0;
|
|
my $allow_audit = 0;
|
|
my $deny_mode = 0;
|
|
my $deny_audit = 0;
|
|
|
|
my ($fmode, $famode, $imode, $iamode, @fm, @im, $cm, $am, $cam, @m);
|
|
($fmode, $famode, @fm) = rematchfrag($sd{$profile}{$hat}, 'allow', $path);
|
|
$allow_mode |= $fmode if $fmode;
|
|
$allow_audit |= $famode if $famode;
|
|
($imode, $iamode, @im) = match_prof_incs_to_path($sd{$profile}{$hat}, 'allow', $path);
|
|
$allow_mode |= $imode if $imode;
|
|
$allow_audit |= $iamode if $iamode;
|
|
|
|
($cm, $cam, @m) = rematchfrag($sd{$profile}{$hat}, 'deny', $path);
|
|
$deny_mode |= $cm if $cm;
|
|
$deny_audit |= $cam if $cam;
|
|
($cm, $cam, @m) = match_prof_incs_to_path($sd{$profile}{$hat}, 'deny', $path);
|
|
$deny_mode |= $cm if $cm;
|
|
$deny_audit |= $cam if $cam;
|
|
|
|
if ($deny_mode & $AA_MAY_EXEC) {
|
|
$deny_mode |= $ALL_AA_EXEC_TYPE;
|
|
}
|
|
|
|
# mask off the modes that have been denied
|
|
$mode &= ~$deny_mode;
|
|
$allow_mode &= ~$deny_mode;
|
|
|
|
# if we had an access(X_OK) request or some other kind of
|
|
# event that generates a "PERMITTING x" syslog entry,
|
|
# first check if it was already dealt with by a i/p/x
|
|
# question due to a exec(). if not, ask about adding ix
|
|
# permission.
|
|
if ($mode & $AA_MAY_EXEC) {
|
|
|
|
# get rid of the access() markers.
|
|
$mode &= (~$ALL_AA_EXEC_TYPE);
|
|
|
|
unless ($allow_mode & $allow_mode & $AA_MAY_EXEC) {
|
|
$mode |= str_to_mode("ix");
|
|
}
|
|
}
|
|
|
|
# if we had an mmap(PROT_EXEC) request, first check if we
|
|
# already have added an ix rule to the profile
|
|
if ($mode & $AA_EXEC_MMAP) {
|
|
# ix implies m. don't ask if they want to add an "m"
|
|
# rule when we already have a matching ix rule.
|
|
if ($allow_mode && contains($allow_mode, "ix")) {
|
|
$mode &= (~$AA_EXEC_MMAP);
|
|
}
|
|
}
|
|
|
|
next unless $mode;
|
|
|
|
|
|
my @matches;
|
|
|
|
if ($fmode) {
|
|
push @matches, @fm;
|
|
}
|
|
if ($imode) {
|
|
push @matches, @im;
|
|
}
|
|
|
|
unless ($allow_mode && mode_contains($allow_mode, $mode)) {
|
|
|
|
my $defaultoption = 1;
|
|
my @options = ();
|
|
|
|
# check the path against the available set of include
|
|
# files
|
|
my @newincludes;
|
|
my $includevalid;
|
|
for my $incname (keys %include) {
|
|
$includevalid = 0;
|
|
|
|
# don't suggest it if we're already including it,
|
|
# that's dumb
|
|
next if $sd{$profile}{$hat}{$incname};
|
|
|
|
# only match includes that can be suggested to
|
|
# the user
|
|
if ($cfg->{settings}{custom_includes}) {
|
|
for my $incm (split(/\s+/,
|
|
$cfg->{settings}{custom_includes})
|
|
) {
|
|
$includevalid = 1 if $incname =~ /$incm/;
|
|
}
|
|
}
|
|
$includevalid = 1 if $incname =~ /abstractions/;
|
|
next if ($includevalid == 0);
|
|
|
|
($cm, $am, @m) = match_include_to_path($incname, 'allow', $path);
|
|
if ($cm && mode_contains($cm, $mode)) {
|
|
#make sure it doesn't deny $mode
|
|
my $dm = match_include_to_path($incname, 'deny', $path);
|
|
unless (($mode & $dm) || (grep { $_ eq "/**" } @m)) {
|
|
push @newincludes, $incname;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
# did any match? add them to the option list...
|
|
if (@newincludes) {
|
|
push @options,
|
|
map { "#include <$_>" }
|
|
sort(uniq(@newincludes));
|
|
}
|
|
|
|
# include the literal path in the option list...
|
|
push @options, $path;
|
|
|
|
# match the current path against the globbing list in
|
|
# logprof.conf
|
|
my @globs = globcommon($path);
|
|
if (@globs) {
|
|
push @matches, @globs;
|
|
}
|
|
|
|
# suggest any matching globs the user manually entered
|
|
for my $userglob (@userglobs) {
|
|
push @matches, $userglob
|
|
if matchliteral($userglob, $path);
|
|
}
|
|
|
|
# we'll take the cheesy way and order the suggested
|
|
# globbing list by length, which is usually right,
|
|
# but not always always
|
|
push @options,
|
|
sort { length($b) <=> length($a) }
|
|
grep { $_ ne $path }
|
|
uniq(@matches);
|
|
$defaultoption = $#options + 1;
|
|
|
|
my $severity = $sevdb->rank($path, mode_to_str($mode));
|
|
|
|
my $audit_toggle = 0;
|
|
my $owner_toggle = $cfg->{settings}{default_owner_prompt};
|
|
my $done = 0;
|
|
while (not $done) {
|
|
|
|
my $q = {};
|
|
$q->{headers} = [];
|
|
push @{ $q->{headers} }, gettext("Profile"), combine_name($profile, $hat);
|
|
push @{ $q->{headers} }, gettext("Path"), $path;
|
|
|
|
# merge in any previous modes from this run
|
|
if ($allow_mode) {
|
|
my $str;
|
|
#print "mode: " . print_mode($mode) . " allow: " . print_mode($allow_mode) . "\n";
|
|
$mode |= $allow_mode;
|
|
my $tail;
|
|
my $prompt_mode;
|
|
if ($owner_toggle == 0) {
|
|
$prompt_mode = flatten_mode($mode);
|
|
$tail = " " . gettext("(owner permissions off)");
|
|
} elsif ($owner_toggle == 1) {
|
|
$prompt_mode = $mode;
|
|
$tail = "";
|
|
} elsif ($owner_toggle == 2) {
|
|
$prompt_mode = $allow_mode | owner_flatten_mode($mode & ~$allow_mode);
|
|
$tail = " " . gettext("(force new perms to owner)");
|
|
} else {
|
|
$prompt_mode = owner_flatten_mode($mode);
|
|
$tail = " " . gettext("(force all rule perms to owner)");
|
|
}
|
|
|
|
if ($audit_toggle == 1) {
|
|
$str = mode_to_str_user($allow_mode);
|
|
$str .= ", " if ($allow_mode);
|
|
$str .= "audit " . mode_to_str_user($prompt_mode & ~$allow_mode) . $tail;
|
|
} elsif ($audit_toggle == 2) {
|
|
$str = "audit " . mode_to_str_user($prompt_mode) . $tail;
|
|
} else {
|
|
$str = mode_to_str_user($prompt_mode) . $tail;
|
|
}
|
|
push @{ $q->{headers} }, gettext("Old Mode"), mode_to_str_user($allow_mode);
|
|
push @{ $q->{headers} }, gettext("New Mode"), $str;
|
|
} else {
|
|
my $str = "";
|
|
if ($audit_toggle) {
|
|
$str = "audit ";
|
|
}
|
|
my $tail;
|
|
my $prompt_mode;
|
|
if ($owner_toggle == 0) {
|
|
$prompt_mode = flatten_mode($mode);
|
|
$tail = " " . gettext("(owner permissions off)");
|
|
} elsif ($owner_toggle == 1) {
|
|
$prompt_mode = $mode;
|
|
$tail = "";
|
|
} else {
|
|
$prompt_mode = owner_flatten_mode($mode);
|
|
$tail = " " . gettext("(force perms to owner)");
|
|
}
|
|
$str .= mode_to_str_user($prompt_mode) . $tail;
|
|
push @{ $q->{headers} }, gettext("Mode"), $str;
|
|
}
|
|
push @{ $q->{headers} }, gettext("Severity"), $severity;
|
|
|
|
$q->{options} = [@options];
|
|
$q->{selected} = $defaultoption - 1;
|
|
|
|
$q->{functions} = [
|
|
"CMD_ALLOW", "CMD_DENY", "CMD_GLOB", "CMD_GLOBEXT", "CMD_NEW",
|
|
"CMD_ABORT", "CMD_FINISHED", "CMD_OTHER"
|
|
];
|
|
|
|
$q->{default} =
|
|
($sdmode eq "PERMITTING")
|
|
? "CMD_ALLOW"
|
|
: "CMD_DENY";
|
|
|
|
$seenevents++;
|
|
# if they just hit return, use the default answer
|
|
my ($ans, $selected) = UI_PromptUser($q);
|
|
|
|
if ($ans eq "CMD_OTHER") {
|
|
|
|
($audit_toggle, $owner_toggle) = UI_ask_mode_toggles($audit_toggle, $owner_toggle, $allow_mode);
|
|
} elsif ($ans eq "CMD_USER_TOGGLE") {
|
|
$owner_toggle++;
|
|
$owner_toggle++ if (!$allow_mode && $owner_toggle == 2);
|
|
$owner_toggle = 0 if ($owner_toggle > 3);
|
|
} elsif ($ans eq "CMD_ALLOW") {
|
|
$path = $options[$selected];
|
|
$done = 1;
|
|
if ($path =~ m/^#include <(.+)>$/) {
|
|
my $inc = $1;
|
|
my $deleted = 0;
|
|
|
|
$deleted = delete_duplicates($sd{$profile}{$hat},
|
|
$inc );
|
|
|
|
# record the new entry
|
|
$sd{$profile}{$hat}{include}{$inc} = 1;
|
|
|
|
$changed{$profile} = 1;
|
|
UI_Info(sprintf(gettext('Adding #include <%s> to profile.'), $inc));
|
|
UI_Info(sprintf(gettext('Deleted %s previous matching profile entries.'), $deleted)) if $deleted;
|
|
} else {
|
|
if ($sd{$profile}{$hat}{allow}{path}{$path}{mode}) {
|
|
$mode |= $sd{$profile}{$hat}{allow}{path}{$path}{mode};
|
|
}
|
|
|
|
my $deleted = 0;
|
|
for my $entry (keys %{ $sd{$profile}{$hat}{allow}{path} }) {
|
|
|
|
next if $path eq $entry;
|
|
|
|
if (matchregexp($path, $entry)) {
|
|
|
|
# regexp matches, add it's mode to
|
|
# the list to check against
|
|
if (mode_contains($mode,
|
|
$sd{$profile}{$hat}{allow}{path}{$entry}{mode})) {
|
|
delete $sd{$profile}{$hat}{allow}{path}{$entry};
|
|
$deleted++;
|
|
}
|
|
}
|
|
}
|
|
|
|
# record the new entry
|
|
if ($owner_toggle == 0) {
|
|
$mode = flatten_mode($mode);
|
|
} elsif ($owner_toggle == 1) {
|
|
$mode = $mode;
|
|
} elsif ($owner_toggle == 2) {
|
|
$mode = $allow_mode | owner_flatten_mode($mode & ~$allow_mode);
|
|
} elsif ($owner_toggle == 3) {
|
|
$mode = owner_flatten_mode($mode);
|
|
}
|
|
$sd{$profile}{$hat}{allow}{path}{$path}{mode} |= $mode;
|
|
my $tmpmode = ($audit_toggle == 1) ? $mode & ~$allow_mode : 0;
|
|
$tmpmode = ($audit_toggle == 2) ? $mode : $tmpmode;
|
|
$sd{$profile}{$hat}{allow}{path}{$path}{audit} |= $tmpmode;
|
|
|
|
$changed{$profile} = 1;
|
|
UI_Info(sprintf(gettext('Adding %s %s to profile.'), $path, mode_to_str_user($mode)));
|
|
UI_Info(sprintf(gettext('Deleted %s previous matching profile entries.'), $deleted)) if $deleted;
|
|
}
|
|
} elsif ($ans eq "CMD_DENY") {
|
|
# record the new entry
|
|
$sd{$profile}{$hat}{deny}{path}{$path}{mode} |= $mode & ~$allow_mode;
|
|
$sd{$profile}{$hat}{deny}{path}{$path}{audit} |= 0;
|
|
|
|
$changed{$profile} = 1;
|
|
|
|
# go on to the next entry without saving this
|
|
# one
|
|
$done = 1;
|
|
} elsif ($ans eq "CMD_NEW") {
|
|
my $arg = $options[$selected];
|
|
if ($arg !~ /^#include/) {
|
|
$ans = UI_GetString(gettext("Enter new path: "), $arg);
|
|
if ($ans) {
|
|
unless (matchliteral($ans, $path)) {
|
|
my $ynprompt = gettext("The specified path does not match this log entry:") . "\n\n";
|
|
$ynprompt .= " " . gettext("Log Entry") . ": $path\n";
|
|
$ynprompt .= " " . gettext("Entered Path") . ": $ans\n\n";
|
|
$ynprompt .= gettext("Do you really want to use this path?") . "\n";
|
|
|
|
# we default to no if they just hit return...
|
|
my $key = UI_YesNo($ynprompt, "n");
|
|
|
|
next if $key eq "n";
|
|
}
|
|
|
|
# save this one for later
|
|
push @userglobs, $ans;
|
|
|
|
push @options, $ans;
|
|
$defaultoption = $#options + 1;
|
|
}
|
|
}
|
|
} elsif ($ans eq "CMD_GLOB") {
|
|
|
|
# do globbing if they don't have an include
|
|
# selected
|
|
my $newpath = $options[$selected];
|
|
chomp $newpath ;
|
|
unless ($newpath =~ /^#include/) {
|
|
# is this entry directory specific
|
|
if ( $newpath =~ m/\/$/ ) {
|
|
# do we collapse to /* or /**?
|
|
if ($newpath =~ m/\/\*{1,2}\/$/) {
|
|
$newpath =~
|
|
s/\/[^\/]+\/\*{1,2}\/$/\/\*\*\//;
|
|
} else {
|
|
$newpath =~ s/\/[^\/]+\/$/\/\*\//;
|
|
}
|
|
} else {
|
|
# do we collapse to /* or /**?
|
|
if ($newpath =~ m/\/\*{1,2}$/) {
|
|
$newpath =~ s/\/[^\/]+\/\*{1,2}$/\/\*\*/;
|
|
} else {
|
|
$newpath =~ s/\/[^\/]+$/\/\*/;
|
|
}
|
|
}
|
|
if ($newpath ne $selected) {
|
|
push @options, $newpath;
|
|
$defaultoption = $#options + 1;
|
|
}
|
|
}
|
|
} elsif ($ans eq "CMD_GLOBEXT") {
|
|
|
|
# do globbing if they don't have an include
|
|
# selected
|
|
my $newpath = $options[$selected];
|
|
unless ($newpath =~ /^#include/) {
|
|
# do we collapse to /*.ext or /**.ext?
|
|
if ($newpath =~ m/\/\*{1,2}\.[^\/]+$/) {
|
|
$newpath =~ s/\/[^\/]+\/\*{1,2}(\.[^\/]+)$/\/\*\*$1/;
|
|
} else {
|
|
$newpath =~ s/\/[^\/]+(\.[^\/]+)$/\/\*$1/;
|
|
}
|
|
if ($newpath ne $selected) {
|
|
push @options, $newpath;
|
|
$defaultoption = $#options + 1;
|
|
}
|
|
}
|
|
} elsif ($ans =~ /\d/) {
|
|
$defaultoption = $ans;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# and then step through all of the netdomain entries...
|
|
for my $family (sort keys %{$log{$sdmode}
|
|
{$profile}
|
|
{$hat}
|
|
{netdomain}}) {
|
|
|
|
# TODO - severity handling for net toggles
|
|
#my $severity = $sevdb->rank();
|
|
for my $sock_type (sort keys %{$log{$sdmode}
|
|
{$profile}
|
|
{$hat}
|
|
{netdomain}
|
|
{$family}}) {
|
|
|
|
# we don't care about it if we've already added it to the
|
|
# profile
|
|
next if ( profile_known_network($sd{$profile}{$hat},
|
|
$family,
|
|
$sock_type));
|
|
my $defaultoption = 1;
|
|
my @options = ();
|
|
my @newincludes;
|
|
@newincludes = matchnetincludes($sd{$profile}{$hat},
|
|
$family,
|
|
$sock_type);
|
|
|
|
my $q = {};
|
|
|
|
if (@newincludes) {
|
|
push @options,
|
|
map { "#include <$_>" } sort(uniq(@newincludes));
|
|
}
|
|
|
|
if ( @options ) {
|
|
push @options, "network $family $sock_type";
|
|
$q->{options} = [@options];
|
|
$q->{selected} = $defaultoption - 1;
|
|
}
|
|
|
|
$q->{headers} = [];
|
|
push @{ $q->{headers} },
|
|
gettext("Profile"),
|
|
combine_name($profile, $hat);
|
|
push @{ $q->{headers} },
|
|
gettext("Network Family"),
|
|
$family;
|
|
push @{ $q->{headers} },
|
|
gettext("Socket Type"),
|
|
$sock_type;
|
|
|
|
my $audit_toggle = 0;
|
|
|
|
$q->{functions} = [
|
|
"CMD_ALLOW",
|
|
"CMD_DENY",
|
|
"CMD_AUDIT_NEW",
|
|
"CMD_ABORT",
|
|
"CMD_FINISHED"
|
|
];
|
|
|
|
# complain-mode events default to allow - enforce defaults
|
|
# to deny
|
|
$q->{default} = ($sdmode eq "PERMITTING") ? "CMD_ALLOW" :
|
|
"CMD_DENY";
|
|
|
|
$seenevents++;
|
|
|
|
# what did the grand exalted master tell us to do?
|
|
my $done = 0;
|
|
while ( not $done ) {
|
|
my ($ans, $selected) = UI_PromptUser($q);
|
|
if ($ans =~ /^CMD_AUDIT/) {
|
|
$audit_toggle = !$audit_toggle;
|
|
my $audit = $audit_toggle ? "audit " : "";
|
|
if ($audit_toggle) {
|
|
$q->{functions} = [
|
|
"CMD_ALLOW",
|
|
"CMD_DENY",
|
|
"CMD_AUDIT_OFF",
|
|
"CMD_ABORT",
|
|
"CMD_FINISHED"
|
|
];
|
|
} else {
|
|
$q->{functions} = [
|
|
"CMD_ALLOW",
|
|
"CMD_DENY",
|
|
"CMD_AUDIT_NEW",
|
|
"CMD_ABORT",
|
|
"CMD_FINISHED"
|
|
];
|
|
}
|
|
$q->{headers} = [];
|
|
push @{ $q->{headers} },
|
|
gettext("Profile"),
|
|
combine_name($profile, $hat);
|
|
push @{ $q->{headers} },
|
|
gettext("Network Family"),
|
|
$audit . $family;
|
|
push @{ $q->{headers} },
|
|
gettext("Socket Type"),
|
|
$sock_type;
|
|
} elsif ($ans eq "CMD_ALLOW") {
|
|
my $selection = $options[$selected];
|
|
$done = 1;
|
|
if ($selection &&
|
|
$selection =~ m/^#include <(.+)>$/) {
|
|
my $inc = $1;
|
|
my $deleted = 0;
|
|
$deleted = delete_duplicates($sd{$profile}{$hat},
|
|
$inc
|
|
);
|
|
# record the new entry
|
|
$sd{$profile}{$hat}{include}{$inc} = 1;
|
|
|
|
$changed{$profile} = 1;
|
|
UI_Info(
|
|
sprintf(
|
|
gettext('Adding #include <%s> to profile.'),
|
|
$inc));
|
|
UI_Info(
|
|
sprintf(
|
|
gettext('Deleted %s previous matching profile entries.'),
|
|
$deleted)) if $deleted;
|
|
} else {
|
|
|
|
# stick the whole rule into the profile
|
|
$sd{$profile}
|
|
{$hat}
|
|
{allow}
|
|
{netdomain}
|
|
{audit}
|
|
{$family}
|
|
{$sock_type} = $audit_toggle;
|
|
|
|
$sd{$profile}
|
|
{$hat}
|
|
{allow}
|
|
{netdomain}
|
|
{rule}
|
|
{$family}
|
|
{$sock_type} = 1;
|
|
|
|
# mark this profile as changed
|
|
$changed{$profile} = 1;
|
|
|
|
# give a little feedback to the user
|
|
UI_Info(sprintf(
|
|
gettext('Adding network access %s %s to profile.'),
|
|
$family,
|
|
$sock_type
|
|
)
|
|
);
|
|
}
|
|
} elsif ($ans eq "CMD_DENY") {
|
|
$done = 1;
|
|
# record the new entry
|
|
$sd{$profile}
|
|
{$hat}
|
|
{deny}
|
|
{netdomain}
|
|
{rule}
|
|
{$family}
|
|
{$sock_type} = 1;
|
|
|
|
$changed{$profile} = 1;
|
|
UI_Info(sprintf(
|
|
gettext('Denying network access %s %s to profile.'),
|
|
$family,
|
|
$sock_type
|
|
)
|
|
);
|
|
} else {
|
|
redo;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sub delete_net_duplicates($$) {
|
|
my ($netrules, $incnetrules) = @_;
|
|
my $deleted = 0;
|
|
if ( $incnetrules && $netrules ) {
|
|
my $incnetglob = defined $incnetrules->{all};
|
|
|
|
# See which if any profile rules are matched by the include and can be
|
|
# deleted
|
|
for my $fam ( keys %$netrules ) {
|
|
if ( $incnetglob || (ref($incnetrules->{rule}{$fam}) ne "HASH" &&
|
|
$incnetrules->{rule}{$fam} == 1)) { # include allows
|
|
# all net or
|
|
# all fam
|
|
if ( ref($netrules->{rule}{$fam}) eq "HASH" ) {
|
|
$deleted += ( keys %{$netrules->{rule}{$fam}} );
|
|
} else {
|
|
$deleted++;
|
|
}
|
|
delete $netrules->{rule}{$fam};
|
|
} elsif ( ref($netrules->{rule}{$fam}) ne "HASH" &&
|
|
$netrules->{rule}{$fam} == 1 ){
|
|
next; # profile has all family
|
|
} else {
|
|
for my $socket_type ( keys %{$netrules->{rule}{$fam}} ) {
|
|
if ( defined $incnetrules->{$fam}{$socket_type} ) {
|
|
delete $netrules->{$fam}{$socket_type};
|
|
$deleted++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $deleted;
|
|
}
|
|
|
|
sub delete_cap_duplicates ($$) {
|
|
my ($profilecaps, $inccaps) = @_;
|
|
my $deleted = 0;
|
|
if ( $profilecaps && $inccaps ) {
|
|
for my $capname ( keys %$profilecaps ) {
|
|
if ( defined $inccaps->{$capname}{set} && $inccaps->{$capname}{set} == 1 ) {
|
|
delete $profilecaps->{$capname};
|
|
$deleted++;
|
|
}
|
|
}
|
|
}
|
|
return $deleted;
|
|
}
|
|
|
|
sub delete_path_duplicates ($$$) {
|
|
my ($profile, $incname, $allow) = @_;
|
|
my $deleted = 0;
|
|
|
|
for my $entry (keys %{ $profile->{$allow}{path} }) {
|
|
next if $entry eq "#include <$incname>";
|
|
my ($cm, $am, @m) = match_include_to_path($incname, $allow, $entry);
|
|
if ($cm
|
|
&& mode_contains($cm, $profile->{$allow}{path}{$entry}{mode})
|
|
&& mode_contains($am, $profile->{$allow}{path}{$entry}{audit}))
|
|
{
|
|
delete $profile->{$allow}{path}{$entry};
|
|
$deleted++;
|
|
}
|
|
}
|
|
return $deleted;
|
|
}
|
|
|
|
sub delete_duplicates (\%$) {
|
|
my ( $profile, $incname ) = @_;
|
|
my $deleted = 0;
|
|
|
|
# don't cross delete allow rules covered by denied rules as the coverage
|
|
# may not be complete. ie. want to deny a subset of allow, allow a subset
|
|
# of deny with different perms.
|
|
|
|
## network rules
|
|
$deleted += delete_net_duplicates($profile->{allow}{netdomain}, $include{$incname}{$incname}{allow}{netdomain});
|
|
$deleted += delete_net_duplicates($profile->{deny}{netdomain}, $include{$incname}{$incname}{deny}{netdomain});
|
|
|
|
## capabilities
|
|
$deleted += delete_cap_duplicates($profile->{allow}{capability},
|
|
$include{$incname}{$incname}{allow}{capability});
|
|
$deleted += delete_cap_duplicates($profile->{deny}{capability},
|
|
$include{$incname}{$incname}{deny}{capability});
|
|
|
|
## paths
|
|
$deleted += delete_path_duplicates($profile, $incname, 'allow');
|
|
$deleted += delete_path_duplicates($profile, $incname, 'deny');
|
|
|
|
return $deleted;
|
|
}
|
|
|
|
sub matchnetinclude ($$$) {
|
|
my ($incname, $family, $type) = @_;
|
|
|
|
my @matches;
|
|
|
|
# scan the include fragments for this profile looking for matches
|
|
my @includelist = ($incname);
|
|
my @checked;
|
|
while (my $name = shift @includelist) {
|
|
push @checked, $name;
|
|
return 1
|
|
if netrules_access_check($include{$name}{$name}{allow}{netdomain}, $family, $type);
|
|
# if this fragment includes others, check them too
|
|
if (keys %{ $include{$name}{$name}{include} } &&
|
|
(grep($name, @checked) == 0) ) {
|
|
push @includelist, keys %{ $include{$name}{$name}{include} };
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub matchcapincludes (\%$) {
|
|
my ($profile, $cap) = @_;
|
|
|
|
# check the path against the available set of include
|
|
# files
|
|
my @newincludes;
|
|
my $includevalid;
|
|
for my $incname (keys %include) {
|
|
$includevalid = 0;
|
|
|
|
# don't suggest it if we're already including it,
|
|
# that's dumb
|
|
next if $profile->{include}{$incname};
|
|
|
|
# only match includes that can be suggested to
|
|
# the user
|
|
if ($cfg->{settings}{custom_includes}) {
|
|
for my $incm (split(/\s+/,
|
|
$cfg->{settings}{custom_includes})) {
|
|
$includevalid = 1 if $incname =~ /$incm/;
|
|
}
|
|
}
|
|
$includevalid = 1 if $incname =~ /abstractions/;
|
|
next if ($includevalid == 0);
|
|
|
|
push @newincludes, $incname
|
|
if ( defined $include{$incname}{$incname}{allow}{capability}{$cap}{set} &&
|
|
$include{$incname}{$incname}{allow}{capability}{$cap}{set} == 1 );
|
|
}
|
|
return @newincludes;
|
|
}
|
|
|
|
sub matchnetincludes (\%$$) {
|
|
my ($profile, $family, $type) = @_;
|
|
|
|
# check the path against the available set of include
|
|
# files
|
|
my @newincludes;
|
|
my $includevalid;
|
|
for my $incname (keys %include) {
|
|
$includevalid = 0;
|
|
|
|
# don't suggest it if we're already including it,
|
|
# that's dumb
|
|
next if $profile->{include}{$incname};
|
|
|
|
# only match includes that can be suggested to
|
|
# the user
|
|
if ($cfg->{settings}{custom_includes}) {
|
|
for my $incm (split(/\s+/, $cfg->{settings}{custom_includes})) {
|
|
$includevalid = 1 if $incname =~ /$incm/;
|
|
}
|
|
}
|
|
$includevalid = 1 if $incname =~ /abstractions/;
|
|
next if ($includevalid == 0);
|
|
|
|
push @newincludes, $incname
|
|
if matchnetinclude($incname, $family, $type);
|
|
}
|
|
return @newincludes;
|
|
}
|
|
|
|
|
|
sub do_logprof_pass($) {
|
|
my $logmark = shift || "";
|
|
|
|
# zero out the state variables for this pass...
|
|
%t = ( );
|
|
%transitions = ( );
|
|
%seen = ( );
|
|
%sd = ( );
|
|
%profilechanges = ( );
|
|
%prelog = ( );
|
|
@log = ( );
|
|
%log = ( );
|
|
%changed = ( );
|
|
%skip = ( );
|
|
%filelist = ( );
|
|
|
|
UI_Info(sprintf(gettext('Reading log entries from %s.'), $filename));
|
|
UI_Info(sprintf(gettext('Updating AppArmor profiles in %s.'), $profiledir));
|
|
|
|
readprofiles();
|
|
unless ($sevdb) {
|
|
$sevdb = new Immunix::Severity("$confdir/severity.db", gettext("unknown
|
|
"));
|
|
}
|
|
|
|
# we need to be able to break all the way out of deep into subroutine calls
|
|
# if they select "Finish" so we can take them back out to the genprof prompt
|
|
eval {
|
|
unless ($repo_cfg || not defined $cfg->{repository}{url}) {
|
|
$repo_cfg = read_config("repository.conf");
|
|
unless ($repo_cfg->{repository}{enabled} &&
|
|
($repo_cfg->{repository}{enabled} eq "yes" ||
|
|
$repo_cfg->{repository}{enabled} eq "no")) {
|
|
UI_ask_to_enable_repo();
|
|
}
|
|
}
|
|
|
|
read_log($logmark);
|
|
|
|
for my $root (@log) {
|
|
handlechildren(undef, undef, $root);
|
|
}
|
|
|
|
for my $pid (sort { $a <=> $b } keys %profilechanges) {
|
|
setprocess($pid, $profilechanges{$pid});
|
|
}
|
|
|
|
collapselog();
|
|
|
|
ask_the_questions();
|
|
|
|
if ($UI_Mode eq "yast") {
|
|
if (not $running_under_genprof) {
|
|
if ($seenevents) {
|
|
my $w = { type => "wizard" };
|
|
$w->{explanation} = gettext("The profile analyzer has completed processing the log files.\n\nAll updated profiles will be reloaded");
|
|
$w->{functions} = [ "CMD_ABORT", "CMD_FINISHED" ];
|
|
SendDataToYast($w);
|
|
my $foo = GetDataFromYast();
|
|
} else {
|
|
my $w = { type => "wizard" };
|
|
$w->{explanation} = gettext("No unhandled AppArmor events were found in the system log.");
|
|
$w->{functions} = [ "CMD_ABORT", "CMD_FINISHED" ];
|
|
SendDataToYast($w);
|
|
my $foo = GetDataFromYast();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
my $finishing = 0;
|
|
if ($@) {
|
|
if ($@ =~ /FINISHING/) {
|
|
$finishing = 1;
|
|
} else {
|
|
die $@;
|
|
}
|
|
}
|
|
|
|
save_profiles();
|
|
|
|
if (repo_is_enabled()) {
|
|
if ( (not defined $repo_cfg->{repository}{upload}) ||
|
|
($repo_cfg->{repository}{upload} eq "later") ) {
|
|
UI_ask_to_upload_profiles();
|
|
}
|
|
if ($repo_cfg->{repository}{upload} eq "yes") {
|
|
sync_profiles();
|
|
}
|
|
@created = ();
|
|
}
|
|
|
|
# if they hit "Finish" we need to tell the caller that so we can exit
|
|
# all the way instead of just going back to the genprof prompt
|
|
return $finishing ? "FINISHED" : "NORMAL";
|
|
}
|
|
|
|
sub save_profiles() {
|
|
# make sure the profile changes we've made are saved to disk...
|
|
my @changed = sort keys %changed;
|
|
#
|
|
# first make sure that profiles in %changed are active (or actual profiles
|
|
# in %sd) - this is to handle the sloppiness of setting profiles as changed
|
|
# when they are parsed in the case of legacy hat code that we want to write
|
|
# out in an updated format
|
|
foreach my $profile_name ( keys %changed ) {
|
|
if ( ! is_active_profile( $profile_name ) ) {
|
|
delete $changed{ $profile_name };
|
|
}
|
|
}
|
|
@changed = sort keys %changed;
|
|
|
|
if (@changed) {
|
|
if ($UI_Mode eq "yast") {
|
|
my (@selected_profiles, $title, $explanation, %profile_changes);
|
|
foreach my $prof (@changed) {
|
|
my $oldprofile = serialize_profile($original_sd{$prof}, $prof);
|
|
my $newprofile = serialize_profile($sd{$prof}, $prof);
|
|
|
|
$profile_changes{$prof} = get_profile_diff($oldprofile,
|
|
$newprofile);
|
|
}
|
|
$explanation = gettext("Select which profile changes you would like to save to the\nlocal profile set");
|
|
$title = gettext("Local profile changes");
|
|
SendDataToYast(
|
|
{
|
|
type => "dialog-select-profiles",
|
|
title => $title,
|
|
explanation => $explanation,
|
|
default_select => "true",
|
|
get_changelog => "false",
|
|
profiles => \%profile_changes
|
|
}
|
|
);
|
|
my ($ypath, $yarg) = GetDataFromYast();
|
|
if ($yarg->{STATUS} eq "cancel") {
|
|
return;
|
|
} else {
|
|
my $selected_profiles_ref = $yarg->{PROFILES};
|
|
for my $profile (@$selected_profiles_ref) {
|
|
writeprofile_ui_feedback($profile);
|
|
reload_base($profile);
|
|
}
|
|
}
|
|
} else {
|
|
my $q = {};
|
|
$q->{title} = "Changed Local Profiles";
|
|
$q->{headers} = [];
|
|
|
|
$q->{explanation} =
|
|
gettext( "The following local profiles were changed. Would you like to save them?");
|
|
|
|
$q->{functions} = [ "CMD_SAVE_CHANGES",
|
|
"CMD_VIEW_CHANGES",
|
|
"CMD_ABORT", ];
|
|
|
|
$q->{default} = "CMD_VIEW_CHANGES";
|
|
|
|
$q->{options} = [@changed];
|
|
$q->{selected} = 0;
|
|
|
|
my ($p, $ans, $arg);
|
|
do {
|
|
($ans, $arg) = UI_PromptUser($q);
|
|
|
|
if ($ans eq "CMD_VIEW_CHANGES") {
|
|
my $which = $changed[$arg];
|
|
my $oldprofile =
|
|
serialize_profile($original_sd{$which}, $which);
|
|
my $newprofile = serialize_profile($sd{$which}, $which);
|
|
display_changes($oldprofile, $newprofile);
|
|
}
|
|
|
|
} until $ans =~ /^CMD_SAVE_CHANGES/;
|
|
|
|
for my $profile (sort keys %changed) {
|
|
writeprofile_ui_feedback($profile);
|
|
reload_base($profile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
sub get_pager() {
|
|
|
|
if ( $ENV{PAGER} and (-x "/usr/bin/$ENV{PAGER}" ||
|
|
-x "/usr/sbin/$ENV{PAGER}" )
|
|
) {
|
|
return $ENV{PAGER};
|
|
} else {
|
|
return "less"
|
|
}
|
|
}
|
|
|
|
|
|
sub display_text($$) {
|
|
my ($header, $body) = @_;
|
|
my $pager = get_pager();
|
|
if (open(PAGER, "| $pager")) {
|
|
print PAGER "$header\n\n$body";
|
|
close(PAGER);
|
|
}
|
|
}
|
|
|
|
sub get_profile_diff($$) {
|
|
my ($oldprofile, $newprofile) = @_;
|
|
my $oldtmp = new File::Temp(UNLINK => 0);
|
|
print $oldtmp $oldprofile;
|
|
close($oldtmp);
|
|
|
|
my $newtmp = new File::Temp(UNLINK => 0);
|
|
print $newtmp $newprofile;
|
|
close($newtmp);
|
|
|
|
my $difftmp = new File::Temp(UNLINK => 0);
|
|
my @diff;
|
|
system("diff -u $oldtmp $newtmp > $difftmp");
|
|
while (<$difftmp>) {
|
|
push(@diff, $_) unless (($_ =~ /^(---|\+\+\+)/) ||
|
|
($_ =~ /^\@\@.*\@\@$/));
|
|
}
|
|
unlink($difftmp);
|
|
unlink($oldtmp);
|
|
unlink($newtmp);
|
|
return join("", @diff);
|
|
}
|
|
|
|
sub display_changes($$) {
|
|
my ($oldprofile, $newprofile) = @_;
|
|
|
|
my $oldtmp = new File::Temp( UNLINK => 0 );
|
|
print $oldtmp $oldprofile;
|
|
close($oldtmp);
|
|
|
|
my $newtmp = new File::Temp( UNLINK => 0 );
|
|
print $newtmp $newprofile;
|
|
close($newtmp);
|
|
|
|
my $difftmp = new File::Temp(UNLINK => 0);
|
|
my @diff;
|
|
system("diff -u $oldtmp $newtmp > $difftmp");
|
|
if ($UI_Mode eq "yast") {
|
|
while (<$difftmp>) {
|
|
push(@diff, $_) unless (($_ =~ /^(---|\+\+\+)/) ||
|
|
($_ =~ /^\@\@.*\@\@$/));
|
|
}
|
|
UI_LongMessage(gettext("Profile Changes"), join("", @diff));
|
|
} else {
|
|
system("less $difftmp");
|
|
}
|
|
|
|
unlink($difftmp);
|
|
unlink($oldtmp);
|
|
unlink($newtmp);
|
|
}
|
|
|
|
sub setprocess ($$) {
|
|
my ($pid, $profile) = @_;
|
|
|
|
# don't do anything if the process exited already...
|
|
return unless -e "/proc/$pid/attr/current";
|
|
|
|
return unless open(CURR, "/proc/$pid/attr/current");
|
|
my $current = <CURR>;
|
|
return unless $current;
|
|
chomp $current;
|
|
close(CURR);
|
|
|
|
# only change null profiles
|
|
return unless $current =~ /null(-complain)*-profile/;
|
|
|
|
return unless open(STAT, "/proc/$pid/stat");
|
|
my $stat = <STAT>;
|
|
chomp $stat;
|
|
close(STAT);
|
|
|
|
return unless $stat =~ /^\d+ \((\S+)\) /;
|
|
my $currprog = $1;
|
|
|
|
open(CURR, ">/proc/$pid/attr/current") or return;
|
|
print CURR "setprofile $profile";
|
|
close(CURR);
|
|
}
|
|
|
|
sub collapselog () {
|
|
for my $sdmode (keys %prelog) {
|
|
for my $profile (keys %{ $prelog{$sdmode} }) {
|
|
for my $hat (keys %{ $prelog{$sdmode}{$profile} }) {
|
|
for my $path (keys %{ $prelog{$sdmode}{$profile}{$hat}{path} }) {
|
|
|
|
my $mode = $prelog{$sdmode}{$profile}{$hat}{path}{$path};
|
|
|
|
# we want to ignore anything from the log that's already
|
|
# in the profile
|
|
my $combinedmode = 0;
|
|
|
|
# is it in the original profile?
|
|
if ($sd{$profile}{$hat}{allow}{path}{$path}) {
|
|
$combinedmode |= $sd{$profile}{$hat}{allow}{path}{$path}{mode};
|
|
}
|
|
|
|
# does path match any regexps in original profile?
|
|
$combinedmode |= rematchfrag($sd{$profile}{$hat}, 'allow', $path);
|
|
|
|
# does path match anything pulled in by includes in
|
|
# original profile?
|
|
$combinedmode |= match_prof_incs_to_path($sd{$profile}{$hat}, 'allow', $path);
|
|
|
|
# if we found any matching entries, do the modes match?
|
|
unless ($combinedmode && mode_contains($combinedmode, $mode)) {
|
|
|
|
# merge in any previous modes from this run
|
|
if ($log{$sdmode}{$profile}{$hat}{$path}) {
|
|
$mode |= $log{$sdmode}{$profile}{$hat}{path}{$path};
|
|
}
|
|
|
|
# record the new entry
|
|
$log{$sdmode}{$profile}{$hat}{path}{$path} = $mode;
|
|
}
|
|
}
|
|
|
|
for my $capability (keys %{ $prelog{$sdmode}{$profile}{$hat}{capability} }) {
|
|
|
|
# if we don't already have this capability in the profile,
|
|
# add it
|
|
unless ($sd{$profile}{$hat}{allow}{capability}{$capability}{set}) {
|
|
$log{$sdmode}{$profile}{$hat}{capability}{$capability} = 1;
|
|
}
|
|
}
|
|
|
|
# Network toggle handling
|
|
my $ndref = $prelog{$sdmode}{$profile}{$hat}{netdomain};
|
|
for my $family ( keys %{$ndref} ) {
|
|
for my $sock_type ( keys %{$ndref->{$family}} ) {
|
|
unless ( profile_known_network($sd{$profile}{$hat},
|
|
$family, $sock_type)) {
|
|
$log{$sdmode}
|
|
{$profile}
|
|
{$hat}
|
|
{netdomain}
|
|
{$family}
|
|
{$sock_type}=1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sub profilemode ($) {
|
|
my $mode = shift;
|
|
|
|
my $modifier = ($mode =~ m/[iupUP]/)[0];
|
|
if ($modifier) {
|
|
$mode =~ s/[iupUPx]//g;
|
|
$mode .= $modifier . "x";
|
|
}
|
|
|
|
return $mode;
|
|
}
|
|
|
|
# kinky.
|
|
sub commonprefix (@) { (join("\0", @_) =~ m/^([^\0]*)[^\0]*(\0\1[^\0]*)*$/)[0] }
|
|
sub commonsuffix (@) { reverse(((reverse join("\0", @_)) =~ m/^([^\0]*)[^\0]*(\0\1[^\0]*)*$/)[0]); }
|
|
|
|
sub uniq (@) {
|
|
my %seen;
|
|
my @result = sort grep { !$seen{$_}++ } @_;
|
|
return @result;
|
|
}
|
|
|
|
our $MODE_MAP_RE = "r|w|l|m|k|a|x|i|u|p|c|n|I|U|P|C|N";
|
|
our $LOG_MODE_RE = "r|w|l|m|k|a|x|ix|ux|px|cx|nx|pix|cix|Ix|Ux|Px|PUx|Cx|Nx|Pix|Cix";
|
|
our $PROFILE_MODE_RE = "r|w|l|m|k|a|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix";
|
|
our $PROFILE_MODE_NT_RE = "r|w|l|m|k|a|x|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix";
|
|
our $PROFILE_MODE_DENY_RE = "r|w|l|m|k|a|x";
|
|
|
|
sub split_log_mode($) {
|
|
my $mode = shift;
|
|
my $user = "";
|
|
my $other = "";
|
|
|
|
if ($mode =~ /(.*?)::(.*)/) {
|
|
$user = $1 if ($1);
|
|
$other = $2 if ($2);
|
|
} else {
|
|
$user = $mode;
|
|
$other = $mode;
|
|
}
|
|
return ($user, $other);
|
|
}
|
|
|
|
sub map_log_mode ($) {
|
|
my $mode = shift;
|
|
return $mode;
|
|
# $mode =~ s/(.*l.*)::.*/$1/ge;
|
|
# $mode =~ s/.*::(.*l.*)/$1/ge;
|
|
# $mode =~ s/:://;
|
|
# return $mode;
|
|
# return $1;
|
|
}
|
|
|
|
sub hide_log_mode($) {
|
|
my $mode = shift;
|
|
|
|
$mode =~ s/:://;
|
|
return $mode;
|
|
}
|
|
|
|
sub validate_log_mode ($) {
|
|
my $mode = shift;
|
|
|
|
return ($mode =~ /^($LOG_MODE_RE)+$/) ? 1 : 0;
|
|
}
|
|
|
|
sub validate_profile_mode ($$$) {
|
|
my ($mode, $allow, $nt_name) = @_;
|
|
|
|
if ($allow eq 'deny') {
|
|
return ($mode =~ /^($PROFILE_MODE_DENY_RE)+$/) ? 1 : 0;
|
|
} elsif ($nt_name) {
|
|
return ($mode =~ /^($PROFILE_MODE_NT_RE)+$/) ? 1 : 0;
|
|
}
|
|
|
|
return ($mode =~ /^($PROFILE_MODE_RE)+$/) ? 1 : 0;
|
|
}
|
|
|
|
# modes internally are stored as a bit Mask
|
|
sub sub_str_to_mode($) {
|
|
my $str = shift;
|
|
my $mode = 0;
|
|
|
|
return 0 if (not $str);
|
|
|
|
while ($str =~ s/(${MODE_MAP_RE})//) {
|
|
my $tmp = $1;
|
|
#print "found mode $1\n";
|
|
|
|
if ($tmp && $MODE_HASH{$tmp}) {
|
|
$mode |= $MODE_HASH{$tmp};
|
|
} else {
|
|
#print "found mode $tmp\n";
|
|
}
|
|
}
|
|
|
|
#my $tmp = mode_to_str($mode);
|
|
#print "parsed_mode $mode\n";
|
|
return $mode;
|
|
}
|
|
|
|
sub print_mode ($) {
|
|
my $mode = shift;
|
|
|
|
my ($user, $other) = split_mode($mode);
|
|
my $str = sub_mode_to_str($user) . "::" . sub_mode_to_str($other);
|
|
|
|
return $str;
|
|
}
|
|
|
|
sub str_to_mode ($) {
|
|
my $str = shift;
|
|
|
|
return 0 if (not $str);
|
|
|
|
my ($user, $other) = split_log_mode($str);
|
|
|
|
#print "str: $str user: $user, other $other\n";
|
|
# we only allow user or all
|
|
$user = $other if (!$user);
|
|
|
|
my $mode = sub_str_to_mode($user);
|
|
$mode |= (sub_str_to_mode($other) << $AA_OTHER_SHIFT);
|
|
|
|
#print "user: $user " .sub_str_to_mode($user) . " other: $other " . (sub_str_to_mode($other) << $AA_OTHER_SHIFT) . " mode = $mode\n";
|
|
|
|
return $mode;
|
|
}
|
|
|
|
sub log_str_to_mode($$$) {
|
|
my ($profile, $str, $nt_name) = @_;
|
|
|
|
my $mode = str_to_mode($str);
|
|
|
|
# this will cover both nx and nix
|
|
if (contains($mode, "Nx")) {
|
|
# need to transform to px, cx
|
|
|
|
if ($nt_name =~ /(.+?)\/\/(.+?)/) {
|
|
my ($lprofile, $lhat) = @_;
|
|
my $tmode = 0;
|
|
if ($profile eq $profile) {
|
|
if ($mode & ($AA_MAY_EXEC)) {
|
|
$tmode = str_to_mode("Cx::");
|
|
}
|
|
if ($mode & ($AA_MAY_EXEC << $AA_OTHER_SHIFT)) {
|
|
$tmode |= str_to_mode("Cx");
|
|
}
|
|
$nt_name = $lhat;
|
|
} else {
|
|
if ($mode & ($AA_MAY_EXEC)) {
|
|
$tmode = str_to_mode("Px::");
|
|
}
|
|
if ($mode & ($AA_MAY_EXEC << $AA_OTHER_SHIFT)) {
|
|
$tmode |= str_to_mode("Px");
|
|
}
|
|
$nt_name = $lhat;
|
|
}
|
|
$mode = ($mode & ~(str_to_mode("Nx")));
|
|
$mode |= $tmode;
|
|
}
|
|
}
|
|
return ($mode, $nt_name);
|
|
}
|
|
|
|
sub split_mode ($) {
|
|
my $mode = shift;
|
|
|
|
my $user = $mode & $AA_USER_MASK;
|
|
my $other = ($mode >> $AA_OTHER_SHIFT) & $AA_USER_MASK;
|
|
|
|
return ($user, $other);
|
|
}
|
|
|
|
sub is_user_mode ($) {
|
|
my $mode = shift;
|
|
|
|
my ($user, $other) = split_mode($mode);
|
|
|
|
if ($user && !$other) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub sub_mode_to_str($) {
|
|
my $mode = shift;
|
|
my $str = "";
|
|
|
|
# "w" implies "a"
|
|
$mode &= (~$AA_MAY_APPEND) if ($mode & $AA_MAY_WRITE);
|
|
$str .= "m" if ($mode & $AA_EXEC_MMAP);
|
|
$str .= "r" if ($mode & $AA_MAY_READ);
|
|
$str .= "w" if ($mode & $AA_MAY_WRITE);
|
|
$str .= "a" if ($mode & $AA_MAY_APPEND);
|
|
$str .= "l" if ($mode & $AA_MAY_LINK);
|
|
$str .= "k" if ($mode & $AA_MAY_LOCK);
|
|
if ($mode & $AA_EXEC_UNCONFINED) {
|
|
if ($mode & $AA_EXEC_UNSAFE) {
|
|
$str .= "u";
|
|
} else {
|
|
$str .= "U";
|
|
}
|
|
}
|
|
if ($mode & ($AA_EXEC_PROFILE | $AA_EXEC_NT)) {
|
|
if ($mode & $AA_EXEC_UNSAFE) {
|
|
$str .= "p";
|
|
} else {
|
|
$str .= "P";
|
|
}
|
|
}
|
|
if ($mode & $AA_EXEC_CHILD) {
|
|
if ($mode & $AA_EXEC_UNSAFE) {
|
|
$str .= "c";
|
|
} else {
|
|
$str .= "C";
|
|
}
|
|
}
|
|
$str .= "i" if ($mode & $AA_EXEC_INHERIT);
|
|
$str .= "x" if ($mode & $AA_MAY_EXEC);
|
|
|
|
return $str;
|
|
}
|
|
|
|
sub flatten_mode ($) {
|
|
my $mode = shift;
|
|
|
|
return 0 if (!$mode);
|
|
|
|
$mode = ($mode & $AA_USER_MASK) | (($mode >> $AA_OTHER_SHIFT) & $AA_USER_MASK);
|
|
$mode |= ($mode << $AA_OTHER_SHIFT);
|
|
}
|
|
|
|
sub mode_to_str ($) {
|
|
my $mode = shift;
|
|
$mode = flatten_mode($mode);
|
|
return sub_mode_to_str($mode);
|
|
}
|
|
|
|
sub owner_flatten_mode($) {
|
|
my $mode = shift;
|
|
$mode = flatten_mode($mode) & $AA_USER_MASK;
|
|
return $mode;
|
|
}
|
|
|
|
sub mode_to_str_user ($) {
|
|
my $mode = shift;
|
|
|
|
my ($user, $other) = split_mode($mode);
|
|
|
|
my $str = "";
|
|
$user = 0 if (!$user);
|
|
$other = 0 if (!$other);
|
|
|
|
if ($user & ~$other) {
|
|
# more user perms than other
|
|
$str = sub_mode_to_str($other). " + " if ($other);
|
|
$str .= "owner " . sub_mode_to_str($user & ~$other);
|
|
} elsif (is_user_mode($mode)) {
|
|
$str = "owner " . sub_mode_to_str($user);
|
|
} else {
|
|
$str = sub_mode_to_str(flatten_mode($mode));
|
|
}
|
|
return $str;
|
|
}
|
|
|
|
sub mode_contains ($$) {
|
|
my ($mode, $subset) = @_;
|
|
|
|
# "w" implies "a"
|
|
if ($mode & $AA_MAY_WRITE) {
|
|
$mode |= $AA_MAY_APPEND;
|
|
}
|
|
if ($mode & ($AA_MAY_WRITE << $AA_OTHER_SHIFT)) {
|
|
$mode |= ($AA_MAY_APPEND << $AA_OTHER_SHIFT);
|
|
}
|
|
|
|
# "?ix" implies "m"
|
|
if ($mode & $AA_EXEC_INHERIT) {
|
|
$mode |= $AA_EXEC_MMAP;
|
|
}
|
|
if ($mode & ($AA_EXEC_INHERIT << $AA_OTHER_SHIFT)) {
|
|
$mode |= ($AA_EXEC_MMAP << $AA_OTHER_SHIFT);
|
|
}
|
|
|
|
return (($mode & $subset) == $subset);
|
|
}
|
|
|
|
sub contains ($$) {
|
|
my ($mode, $str) = @_;
|
|
|
|
return mode_contains($mode, str_to_mode($str));
|
|
}
|
|
|
|
# isSkippableFile - return true if filename matches something that
|
|
# should be skipped (rpm backup files, dotfiles, emacs backup files
|
|
# Annoyingly, this needs to be kept in sync with the skipped files
|
|
# in the apparmor initscript.
|
|
sub isSkippableFile($) {
|
|
my $path = shift;
|
|
|
|
return ($path =~ /(^|\/)\.[^\/]*$/
|
|
|| $path =~ /\.rpm(save|new)$/
|
|
|| $path =~ /\.dpkg-(old|new)$/
|
|
|| $path =~ /\.swp$/
|
|
|| $path =~ /\~$/);
|
|
}
|
|
|
|
# isSkippableDir - return true if directory matches something that
|
|
# should be skipped (cache directory, symlink directories, etc.)
|
|
sub isSkippableDir($) {
|
|
my $path = shift;
|
|
|
|
return ($path eq "disable"
|
|
|| $path eq "cache"
|
|
|| $path eq "force-complain");
|
|
}
|
|
|
|
sub checkIncludeSyntax($) {
|
|
my $errors = shift;
|
|
|
|
if (opendir(SDDIR, $profiledir)) {
|
|
my @incdirs = grep { (!/^\./) && (-d "$profiledir/$_") } readdir(SDDIR);
|
|
close(SDDIR);
|
|
while (my $id = shift @incdirs) {
|
|
next if isSkippableDir($id);
|
|
if (opendir(SDDIR, "$profiledir/$id")) {
|
|
for my $path (grep { !/^\./ } readdir(SDDIR)) {
|
|
chomp($path);
|
|
next if isSkippableFile($path);
|
|
if (-f "$profiledir/$id/$path") {
|
|
my $file = "$id/$path";
|
|
$file =~ s/$profiledir\///;
|
|
eval { loadinclude($file); };
|
|
if ( defined $@ && $@ ne "" ) {
|
|
push @$errors, $@;
|
|
}
|
|
} elsif (-d "$id/$path") {
|
|
push @incdirs, "$id/$path";
|
|
}
|
|
}
|
|
closedir(SDDIR);
|
|
}
|
|
}
|
|
}
|
|
return $errors;
|
|
}
|
|
|
|
sub checkProfileSyntax ($) {
|
|
my $errors = shift;
|
|
|
|
# Check the syntax of profiles
|
|
|
|
opendir(SDDIR, $profiledir)
|
|
or fatal_error "Can't read AppArmor profiles in $profiledir.";
|
|
for my $file (grep { -f "$profiledir/$_" } readdir(SDDIR)) {
|
|
next if isSkippableFile($file);
|
|
my $err = readprofile("$profiledir/$file", \&printMessageErrorHandler, 1);
|
|
if (defined $err and $err ne "") {
|
|
push @$errors, $err;
|
|
}
|
|
}
|
|
closedir(SDDIR);
|
|
return $errors;
|
|
}
|
|
|
|
sub printMessageErrorHandler ($) {
|
|
my $message = shift;
|
|
return $message;
|
|
}
|
|
|
|
sub readprofiles () {
|
|
opendir(SDDIR, $profiledir)
|
|
or fatal_error "Can't read AppArmor profiles in $profiledir.";
|
|
for my $file (grep { -f "$profiledir/$_" } readdir(SDDIR)) {
|
|
next if isSkippableFile($file);
|
|
readprofile("$profiledir/$file", \&fatal_error, 1);
|
|
}
|
|
closedir(SDDIR);
|
|
}
|
|
|
|
sub readinactiveprofiles () {
|
|
return if ( ! -e $extraprofiledir );
|
|
opendir(ESDDIR, $extraprofiledir) or
|
|
fatal_error "Can't read AppArmor profiles in $extraprofiledir.";
|
|
for my $file (grep { -f "$extraprofiledir/$_" } readdir(ESDDIR)) {
|
|
next if $file =~ /\.rpm(save|new)|README$/;
|
|
readprofile("$extraprofiledir/$file", \&fatal_error, 0);
|
|
}
|
|
closedir(ESDDIR);
|
|
}
|
|
|
|
sub readprofile ($$$) {
|
|
my $file = shift;
|
|
my $error_handler = shift;
|
|
my $active_profile = shift;
|
|
if (open(SDPROF, "$file")) {
|
|
local $/;
|
|
my $data = <SDPROF>;
|
|
close(SDPROF);
|
|
|
|
eval {
|
|
my $profile_data = parse_profile_data($data, $file, 0);
|
|
if ($profile_data && $active_profile) {
|
|
attach_profile_data(\%sd, $profile_data);
|
|
attach_profile_data(\%original_sd, $profile_data);
|
|
} elsif ( $profile_data ) {
|
|
attach_profile_data(\%extras, $profile_data);
|
|
}
|
|
};
|
|
|
|
# if there were errors loading the profile, call the error handler
|
|
if ($@) {
|
|
$@ =~ s/\n$//;
|
|
return &$error_handler($@);
|
|
}
|
|
} else {
|
|
$DEBUGGING && debug "readprofile: can't read $file - skipping";
|
|
}
|
|
}
|
|
|
|
sub attach_profile_data($$) {
|
|
my ($profiles, $profile_data) = @_;
|
|
|
|
# make deep copies of the profile data so that if we change one set of
|
|
# profile data, we're not changing others because of sharing references
|
|
for my $p ( keys %$profile_data) {
|
|
$profiles->{$p} = dclone($profile_data->{$p});
|
|
}
|
|
}
|
|
|
|
sub parse_profile_data($$$) {
|
|
my ($data, $file, $do_include) = @_;
|
|
|
|
|
|
my ($profile_data, $profile, $hat, $in_contained_hat, $repo_data,
|
|
@parsed_profiles);
|
|
my $initial_comment = "";
|
|
|
|
if ($do_include) {
|
|
$profile = $file;
|
|
$hat = $file;
|
|
}
|
|
|
|
for (split(/\n/, $data)) {
|
|
chomp;
|
|
|
|
# we don't care about blank lines
|
|
next if /^\s*$/;
|
|
|
|
# start of a profile...
|
|
if (m/^\s*(("??\/.+?"??)|(profile\s+("??.+?"??)))\s+((flags=)?\((.+)\)\s+)*\{\s*(#.*)?$/) {
|
|
# if we run into the start of a profile while we're already in a
|
|
# profile, something's wrong...
|
|
if ($profile) {
|
|
unless (($profile eq $hat) and $4) {
|
|
die "$profile profile in $file contains syntax errors.\n";
|
|
}
|
|
}
|
|
|
|
# we hit the start of a profile, keep track of it...
|
|
if ($profile && ($profile eq $hat) && $4) {
|
|
# local profile
|
|
$hat = $4;
|
|
$in_contained_hat = 1;
|
|
$profile_data->{$profile}{$hat}{profile} = 1;
|
|
} else {
|
|
$profile = $2 || $4;
|
|
# hat is same as profile name if we're not in a hat
|
|
($profile, $hat) = split /\/\//, $profile;
|
|
$in_contained_hat = 0;
|
|
if ($hat) {
|
|
$profile_data->{$profile}{$hat}{external} = 1;
|
|
}
|
|
|
|
$hat ||= $profile;
|
|
}
|
|
|
|
my $flags = $7;
|
|
|
|
# deal with whitespace in profile and hat names.
|
|
$profile = strip_quotes($profile);
|
|
$hat = strip_quotes($hat) if $hat;
|
|
|
|
# save off the name and filename
|
|
$profile_data->{$profile}{$hat}{name} = $profile;
|
|
$profile_data->{$profile}{$hat}{filename} = $file;
|
|
$filelist{$file}{profiles}{$profile}{$hat} = 1;
|
|
|
|
# keep track of profile flags
|
|
$profile_data->{$profile}{$hat}{flags} = $flags;
|
|
|
|
$profile_data->{$profile}{$hat}{allow}{netdomain} = { };
|
|
$profile_data->{$profile}{$hat}{allow}{path} = { };
|
|
|
|
# store off initial comment if they have one
|
|
$profile_data->{$profile}{$hat}{initial_comment} = $initial_comment
|
|
if $initial_comment;
|
|
$initial_comment = "";
|
|
|
|
if ($repo_data) {
|
|
$profile_data->{$profile}{$profile}{repo}{url} = $repo_data->{url};
|
|
$profile_data->{$profile}{$profile}{repo}{user} = $repo_data->{user};
|
|
$profile_data->{$profile}{$profile}{repo}{id} = $repo_data->{id};
|
|
$repo_data = undef;
|
|
}
|
|
|
|
} elsif (m/^\s*\}\s*(#.*)?$/) { # end of a profile...
|
|
|
|
# if we hit the end of a profile when we're not in one, something's
|
|
# wrong...
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
|
|
if ($in_contained_hat) {
|
|
$hat = $profile;
|
|
$in_contained_hat = 0;
|
|
} else {
|
|
push @parsed_profiles, $profile;
|
|
# mark that we're outside of a profile now...
|
|
$profile = undef;
|
|
}
|
|
|
|
$initial_comment = "";
|
|
|
|
} elsif (m/^\s*(audit\s+)?(deny\s+)?capability\s+(\S+)\s*,\s*(#.*)?$/) { # capability entry
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
|
|
my $audit = $1 ? 1 : 0;
|
|
my $allow = $2 ? 'deny' : 'allow';
|
|
$allow = 'deny' if ($2);
|
|
my $capability = $3;
|
|
$profile_data->{$profile}{$hat}{$allow}{capability}{$capability}{set} = 1;
|
|
$profile_data->{$profile}{$hat}{$allow}{capability}{$capability}{audit} = $audit;
|
|
} elsif (m/^\s*set capability\s+(\S+)\s*,\s*(#.*)?$/) { # capability entry
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
|
|
my $capability = $1;
|
|
$profile_data->{$profile}{$hat}{set_capability}{$capability} = 1;
|
|
|
|
} elsif (m/^\s*(audit\s+)?(deny\s+)?link\s+(((subset)|(<=))\s+)?([\"\@\/].*?"??)\s+->\s*([\"\@\/].*?"??)\s*,\s*(#.*)?$/) { # for now just keep link
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
my $audit = $1 ? 1 : 0;
|
|
my $allow = $2 ? 'deny' : 'allow';
|
|
|
|
my $subset = $4;
|
|
my $link = strip_quotes($7);
|
|
my $value = strip_quotes($8);
|
|
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{to} = $value;
|
|
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{mode} |= $AA_MAY_LINK;
|
|
if ($subset) {
|
|
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{mode} |= $AA_LINK_SUBSET;
|
|
}
|
|
if ($audit) {
|
|
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{audit} |= $AA_LINK_SUBSET;
|
|
} else {
|
|
$profile_data->{$profile}{$hat}{$allow}{link}{$link}{audit} |= 0;
|
|
}
|
|
|
|
} elsif (m/^\s*change_profile\s+->\s*("??.+?"??),(#.*)?$/) { # for now just keep change_profile
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
my $cp = strip_quotes($1);
|
|
|
|
$profile_data->{$profile}{$hat}{change_profile}{$cp} = 1;
|
|
} elsif (m/^\s*alias\s+("??.+?"??)\s+->\s*("??.+?"??)\s*,(#.*)?$/) { # never do anything with aliases just keep them
|
|
my $from = strip_quotes($1);
|
|
my $to = strip_quotes($2);
|
|
|
|
if ($profile) {
|
|
$profile_data->{$profile}{$hat}{alias}{$from} = $to;
|
|
} else {
|
|
unless (exists $filelist{$file}) {
|
|
$filelist{$file} = { };
|
|
}
|
|
$filelist{$file}{alias}{$from} = $to;
|
|
}
|
|
|
|
} elsif (m/^\s*set\s+rlimit\s+(.+)\s+<=\s*(.+)\s*,(#.*)?$/) { # never do anything with rlimits just keep them
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
my $from = $1;
|
|
my $to = $2;
|
|
|
|
$profile_data->{$profile}{$hat}{rlimit}{$from} = $to;
|
|
|
|
} elsif (/^\s*(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*=\s*(true|false)\s*,?\s*(#.*)?$/i) { # boolean definition
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
my $bool_var = $1;
|
|
my $value = $2;
|
|
|
|
$profile_data->{$profile}{$hat}{lvar}{$bool_var} = $value;
|
|
} elsif (/^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\+?=\s*(.+?)\s*,?\s*(#.*)?$/) { # variable additions both += and = doesn't mater
|
|
my $list_var = strip_quotes($1);
|
|
my $value = strip_quotes($2);
|
|
|
|
if ($profile) {
|
|
unless (exists $profile_data->{$profile}{$hat}{lvar}) {
|
|
# create lval hash by sticking an empty list into list_var
|
|
my @empty = ();
|
|
$profile_data->{$profile}{$hat}{lvar}{$list_var} = \@empty;
|
|
}
|
|
|
|
store_list_var($profile_data->{$profile}{$hat}{lvar}, $list_var, $value);
|
|
} else {
|
|
unless (exists $filelist{$file}{lvar}) {
|
|
# create lval hash by sticking an empty list into list_var
|
|
my @empty = ();
|
|
$filelist{$file}{lvar}{$list_var} = \@empty;
|
|
}
|
|
|
|
store_list_var($filelist{$file}{lvar}, $list_var, $value);
|
|
}
|
|
} elsif (m/^\s*if\s+(not\s+)?(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*\{\s*(#.*)?$/) { # conditional -- boolean
|
|
} elsif (m/^\s*if\s+(not\s+)?defined\s+(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\{\s*(#.*)?$/) { # conditional -- variable defined
|
|
} elsif (m/^\s*if\s+(not\s+)?defined\s+(\$\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\{\s*(#.*)?$/) { # conditional -- boolean defined
|
|
} elsif (m/^\s*(audit\s+)?(deny\s+)?(owner\s+)?([\"\@\/].*?)\s+(\S+)(\s+->\s*(.*?))?\s*,\s*(#.*)?$/) { # path entry
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
|
|
my $audit = $1 ? 1 : 0;
|
|
my $allow = $2 ? 'deny' : 'allow';
|
|
my $user = $3 ? 1 : 0;
|
|
my ($path, $mode, $nt_name) = ($4, $5, $7);
|
|
|
|
# strip off any trailing spaces.
|
|
$path =~ s/\s+$//;
|
|
$nt_name =~ s/\s+$// if $nt_name;
|
|
|
|
$path = strip_quotes($path);
|
|
$nt_name = strip_quotes($nt_name) if $nt_name;
|
|
|
|
# make sure they don't have broken regexps in the profile
|
|
my $p_re = convert_regexp($path);
|
|
eval { "foo" =~ m/^$p_re$/; };
|
|
if ($@) {
|
|
die sprintf(gettext('Profile %s contains invalid regexp %s.'),
|
|
$file, $path) . "\n";
|
|
}
|
|
|
|
if (!validate_profile_mode($mode, $allow, $nt_name)) {
|
|
fatal_error(sprintf(gettext('Profile %s contains invalid mode %s.'), $file, $mode));
|
|
}
|
|
|
|
my $tmpmode;
|
|
if ($user) {
|
|
$tmpmode = str_to_mode("${mode}::");
|
|
} else {
|
|
$tmpmode = str_to_mode($mode);
|
|
}
|
|
|
|
$profile_data->{$profile}{$hat}{$allow}{path}{$path}{mode} |= $tmpmode;
|
|
$profile_data->{$profile}{$hat}{$allow}{path}{$path}{to} = $nt_name if $nt_name;
|
|
if ($audit) {
|
|
$profile_data->{$profile}{$hat}{$allow}{path}{$path}{audit} |= $tmpmode;
|
|
} else {
|
|
$profile_data->{$profile}{$hat}{$allow}{path}{$path}{audit} |= 0;
|
|
}
|
|
} elsif (m/^\s*#include <(.+)>\s*$/) { # include stuff
|
|
my $include = $1;
|
|
|
|
if ($profile) {
|
|
$profile_data->{$profile}{$hat}{include}{$include} = 1;
|
|
} else {
|
|
unless (exists $filelist{$file}) {
|
|
$filelist{$file} = { };
|
|
}
|
|
$filelist{$file}{include}{$include} = 1;
|
|
}
|
|
|
|
# include is a dir
|
|
if (-d "$profiledir/$include") {
|
|
if (opendir(SDINCDIR, "$profiledir/$include")) {
|
|
for my $path (readdir(SDINCDIR)) {
|
|
chomp($path);
|
|
next if isSkippableFile($path);
|
|
if (-f "$profiledir/$include/$path") {
|
|
my $file = "$include/$path";
|
|
$file =~ s/$profiledir\///;
|
|
my $ret = eval { loadinclude($file); };
|
|
if ($@) { die $@; }
|
|
return $ret if ( $ret != 0 );
|
|
}
|
|
}
|
|
}
|
|
closedir(SDINCDIR);
|
|
} else {
|
|
# try to load the include...
|
|
my $ret = eval { loadinclude($include); };
|
|
# propagate errors up the chain
|
|
if ($@) { die $@; }
|
|
return $ret if ( $ret != 0 );
|
|
}
|
|
} elsif (/^\s*(audit\s+)?(deny\s+)?network(.*)/) {
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
my $audit = $1 ? 1 : 0;
|
|
my $allow = $2 ? 'deny' : 'allow';
|
|
my $network = $3;
|
|
|
|
unless ($profile_data->{$profile}{$hat}{$allow}{netdomain}{rule}) {
|
|
$profile_data->{$profile}{$hat}{$allow}{netdomain}{rule} = { };
|
|
}
|
|
|
|
if ($network =~ /\s+(\S+)\s+(\S+)\s*,\s*(#.*)?$/ ) {
|
|
my $fam = $1;
|
|
my $type = $2;
|
|
$profile_data->{$profile}{$hat}{$allow}{netdomain}{rule}{$fam}{$type} = 1;
|
|
$profile_data->{$profile}{$hat}{$allow}{netdomain}{audit}{$fam}{$type} = $audit;
|
|
} elsif ( $network =~ /\s+(\S+)\s*,\s*(#.*)?$/ ) {
|
|
my $fam = $1;
|
|
$profile_data->{$profile}{$hat}{$allow}{netdomain}{rule}{$fam} = 1;
|
|
$profile_data->{$profile}{$hat}{$allow}{netdomain}{audit}{$fam} = $audit;
|
|
} else {
|
|
$profile_data->{$profile}{$hat}{$allow}{netdomain}{rule}{all} = 1;
|
|
$profile_data->{$profile}{$hat}{$allow}{netdomain}{audit}{all} = 1;
|
|
}
|
|
} elsif (/^\s*(tcp_connect|tcp_accept|udp_send|udp_receive)/) {
|
|
# just ignore and drop old style network
|
|
# die sprintf(gettext('%s contains old style network rules.'), $file) . "\n";
|
|
|
|
} elsif (m/^\s*\^(\"??.+?\"??)\s*,\s*(#.*)?$/) {
|
|
if (not $profile) {
|
|
die "$file contains syntax errors.";
|
|
}
|
|
# change_hat declaration - needed to change_hat to an external
|
|
# hat
|
|
$hat = $1;
|
|
$hat = $1 if $hat =~ /^"(.+)"$/;
|
|
|
|
#store we have a declaration if the hat hasn't been seen
|
|
$profile_data->{$profile}{$hat}{'declared'} = 1
|
|
unless exists($profile_data->{$profile}{$hat}{declared});
|
|
|
|
} elsif (m/^\s*\^(\"??.+?\"??)\s+((flags=)?\((.+)\)\s+)*\{\s*(#.*)?$/) {
|
|
# start of embedded hat syntax hat definition
|
|
# read in and mark as changed so that will be written out in the new
|
|
# format
|
|
|
|
# if we hit the start of a contained hat when we're not in a profile
|
|
# something is wrong...
|
|
if (not $profile) {
|
|
die sprintf(gettext('%s contains syntax errors.'), $file) . "\n";
|
|
}
|
|
|
|
$in_contained_hat = 1;
|
|
|
|
# we hit the start of a hat inside the current profile
|
|
$hat = $1;
|
|
my $flags = $4;
|
|
|
|
# strip quotes.
|
|
$hat = $1 if $hat =~ /^"(.+)"$/;
|
|
|
|
# keep track of profile flags
|
|
$profile_data->{$profile}{$hat}{flags} = $flags;
|
|
# we have seen more than a declaration so clear it
|
|
$profile_data->{$profile}{$hat}{'declared'} = 0;
|
|
$profile_data->{$profile}{$hat}{allow}{path} = { };
|
|
$profile_data->{$profile}{$hat}{allow}{netdomain} = { };
|
|
|
|
# store off initial comment if they have one
|
|
$profile_data->{$profile}{$hat}{initial_comment} = $initial_comment
|
|
if $initial_comment;
|
|
$initial_comment = "";
|
|
#don't mark profile as changed just because it has an embedded
|
|
#hat.
|
|
#$changed{$profile} = 1;
|
|
|
|
$filelist{$file}{profiles}{$profile}{$hat} = 1;
|
|
|
|
} elsif (/^\s*\#/) {
|
|
# we only currently handle initial comments
|
|
if (not $profile) {
|
|
# ignore vim syntax highlighting lines
|
|
next if /^\s*\# vim:syntax/;
|
|
# ignore Last Modified: lines
|
|
next if /^\s*\# Last Modified:/;
|
|
if (/^\s*\# REPOSITORY: (\S+) (\S+) (\S+)$/) {
|
|
$repo_data = { url => $1, user => $2, id => $3 };
|
|
} elsif (/^\s*\# REPOSITORY: NEVERSUBMIT$/) {
|
|
$repo_data = { neversubmit => 1 };
|
|
} else {
|
|
$initial_comment .= "$_\n";
|
|
}
|
|
}
|
|
} else {
|
|
# we hit something we don't understand in a profile...
|
|
die sprintf(gettext('%s contains syntax errors. Line [%s]'), $file, $_) . "\n";
|
|
}
|
|
}
|
|
|
|
#
|
|
# Cleanup : add required hats if not present in the
|
|
# parsed profiles
|
|
#
|
|
if (not $do_include) {
|
|
for my $hatglob (keys %{$cfg->{required_hats}}) {
|
|
for my $parsed_profile ( sort @parsed_profiles ) {
|
|
if ($parsed_profile =~ /$hatglob/) {
|
|
for my $hat (split(/\s+/, $cfg->{required_hats}{$hatglob})) {
|
|
unless ($profile_data->{$parsed_profile}{$hat}) {
|
|
$profile_data->{$parsed_profile}{$hat} = { };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} # if we're still in a profile when we hit the end of the file, it's bad
|
|
if ($profile and not $do_include) {
|
|
die "Reached the end of $file while we were still inside the $profile profile.\n";
|
|
}
|
|
|
|
return $profile_data;
|
|
}
|
|
|
|
sub eliminate_duplicates(@) {
|
|
my @data =@_;
|
|
|
|
my %set = map { $_ => 1 } @_;
|
|
@data = keys %set;
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub separate_vars($) {
|
|
my $vs = shift;
|
|
my @data;
|
|
|
|
# while ($vs =~ /\s*(((\"([^\"]|\\\"))+?\")|\S*)\s*(.*)$/) {
|
|
while ($vs =~ /\s*((\".+?\")|([^\"]\S+))\s*(.*)$/) {
|
|
my $tmp = $1;
|
|
push @data, strip_quotes($tmp);
|
|
$vs = $4;
|
|
}
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub is_active_profile ($) {
|
|
my $pname = shift;
|
|
if ( $sd{$pname} ) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
sub store_list_var (\%$$) {
|
|
my ($vars, $list_var, $value) = @_;
|
|
|
|
my @vlist = (separate_vars($value));
|
|
|
|
# if (exists $profile_data->{$profile}{$hat}{lvar}{$list_var}) {
|
|
# @vlist = (@vlist, @{$profile_data->{$profile}{$hat}{lvar}{$list_var}});
|
|
# }
|
|
#
|
|
# @vlist = eliminate_duplicates(@vlist);
|
|
# $profile_data->{$profile}{$hat}{lvar}{$list_var} = \@vlist;
|
|
|
|
if (exists $vars->{$list_var}) {
|
|
@vlist = (@vlist, @{$vars->{$list_var}});
|
|
}
|
|
|
|
@vlist = eliminate_duplicates(@vlist);
|
|
$vars->{$list_var} = \@vlist;
|
|
|
|
|
|
}
|
|
|
|
sub strip_quotes ($) {
|
|
my $data = shift;
|
|
$data = $1 if $data =~ /^\"(.*)\"$/;
|
|
return $data;
|
|
}
|
|
|
|
sub quote_if_needed ($) {
|
|
my $data = shift;
|
|
$data = "\"$data\"" if $data =~ /\s/;
|
|
|
|
return $data;
|
|
}
|
|
|
|
sub escape ($) {
|
|
my $dangerous = shift;
|
|
|
|
$dangerous = strip_quotes($dangerous);
|
|
|
|
$dangerous =~ s/((?<!\\))"/$1\\"/g;
|
|
if ($dangerous =~ m/(\s|^$|")/) {
|
|
$dangerous = "\"$dangerous\"";
|
|
}
|
|
|
|
return $dangerous;
|
|
}
|
|
|
|
sub writeheader ($$$$$) {
|
|
my ($profile_data, $depth, $name, $embedded_hat, $write_flags) = @_;
|
|
|
|
my $pre = ' ' x $depth;
|
|
my @data;
|
|
# deal with whitespace in profile names...
|
|
$name = quote_if_needed($name);
|
|
|
|
$name = "profile $name" if ((!$embedded_hat && $name =~ /^[^\/]|^"[^\/]/)
|
|
|| ($embedded_hat && $name =~/^[^^]/));
|
|
|
|
#push @data, "#include <tunables/global>" unless ( $is_hat );
|
|
if ($write_flags and $profile_data->{flags}) {
|
|
push @data, "${pre}$name flags=($profile_data->{flags}) {";
|
|
} else {
|
|
push @data, "${pre}$name {";
|
|
}
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub qin_trans ($) {
|
|
my $value = shift;
|
|
return quote_if_needed($value);
|
|
}
|
|
|
|
sub write_single ($$$$$$) {
|
|
my ($profile_data, $depth, $allow, $name, $prefix, $tail) = @_;
|
|
my $ref;
|
|
my @data;
|
|
|
|
if ($allow) {
|
|
$ref = $profile_data->{$allow};
|
|
if ($allow eq 'deny') {
|
|
$allow .= " ";
|
|
} else {
|
|
$allow = "";
|
|
}
|
|
} else {
|
|
$ref = $profile_data;
|
|
$allow = "";
|
|
}
|
|
|
|
my $pre = " " x $depth;
|
|
|
|
|
|
# dump out the data
|
|
if (exists $ref->{$name}) {
|
|
for my $key (sort keys %{$ref->{$name}}) {
|
|
my $qkey = quote_if_needed($key);
|
|
push @data, "${pre}${allow}${prefix}${qkey}${tail}";
|
|
}
|
|
push @data, "" if keys %{$ref->{$name}};
|
|
}
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub write_pair ($$$$$$$$) {
|
|
my ($profile_data, $depth, $allow, $name, $prefix, $sep, $tail, $fn) = @_;
|
|
my $ref;
|
|
my @data;
|
|
|
|
if ($allow) {
|
|
$ref = $profile_data->{$allow};
|
|
if ($allow eq 'deny') {
|
|
$allow .= " ";
|
|
} else {
|
|
$allow = "";
|
|
}
|
|
} else {
|
|
$ref = $profile_data;
|
|
$allow = "";
|
|
}
|
|
|
|
my $pre = " " x $depth;
|
|
|
|
# dump out the data
|
|
if (exists $ref->{$name}) {
|
|
for my $key (sort keys %{$ref->{$name}}) {
|
|
my $value = &{$fn}($ref->{$name}{$key});
|
|
push @data, "${pre}${allow}${prefix}${key}${sep}${value}${tail}";
|
|
}
|
|
push @data, "" if keys %{$ref->{$name}};
|
|
}
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub writeincludes ($$) {
|
|
my ($prof_data, $depth) = @_;
|
|
|
|
return write_single($prof_data, $depth,'', 'include', "#include <", ">");
|
|
}
|
|
|
|
sub writechange_profile ($$) {
|
|
my ($prof_data, $depth) = @_;
|
|
|
|
return write_single($prof_data, $depth, '', 'change_profile', "change_profile -> ", ",");
|
|
}
|
|
|
|
sub writealiases ($$) {
|
|
my ($prof_data, $depth) = @_;
|
|
|
|
return write_pair($prof_data, $depth, '', 'alias', "alias ", " -> ", ",", \&qin_trans);
|
|
}
|
|
|
|
sub writerlimits ($$) {
|
|
my ($prof_data, $depth) = @_;
|
|
|
|
return write_pair($prof_data, $depth, '', 'rlimit', "set rlimit ", " <= ", ",", \&qin_trans);
|
|
}
|
|
|
|
# take a list references and process it
|
|
sub var_transform($) {
|
|
my $ref = shift;
|
|
my @in = @{$ref};
|
|
my @data;
|
|
|
|
foreach my $value (@in) {
|
|
push @data, quote_if_needed($value);
|
|
}
|
|
|
|
return join " ", @data;
|
|
}
|
|
|
|
sub writelistvars ($$) {
|
|
my ($prof_data, $depth) = @_;
|
|
|
|
return write_pair($prof_data, $depth, '', 'lvar', "", " = ", "", \&var_transform);
|
|
}
|
|
|
|
sub writecap_rules ($$$) {
|
|
my ($profile_data, $depth, $allow) = @_;
|
|
|
|
my $allowstr = $allow eq 'deny' ? 'deny ' : '';
|
|
my $pre = " " x $depth;
|
|
|
|
my @data;
|
|
if (exists $profile_data->{$allow}{capability}) {
|
|
for my $cap (sort keys %{$profile_data->{$allow}{capability}}) {
|
|
my $audit = ($profile_data->{$allow}{capability}{$cap}{audit}) ? 'audit ' : '';
|
|
if ($profile_data->{$allow}{capability}{$cap}{set}) {
|
|
push @data, "${pre}${audit}${allowstr}capability ${cap},";
|
|
}
|
|
}
|
|
push @data, "";
|
|
}
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub writecapabilities ($$) {
|
|
my ($prof_data, $depth) = @_;
|
|
my @data;
|
|
push @data, write_single($prof_data, $depth, '', 'set_capability', "set capability ", ",");
|
|
push @data, writecap_rules($prof_data, $depth, 'deny');
|
|
push @data, writecap_rules($prof_data, $depth, 'allow');
|
|
return @data;
|
|
}
|
|
|
|
sub writenet_rules ($$$) {
|
|
my ($profile_data, $depth, $allow) = @_;
|
|
|
|
my $allowstr = $allow eq 'deny' ? 'deny ' : '';
|
|
|
|
my $pre = " " x $depth;
|
|
my $audit = "";
|
|
|
|
my @data;
|
|
# dump out the netdomain entries...
|
|
if (exists $profile_data->{$allow}{netdomain}) {
|
|
if ( $profile_data->{$allow}{netdomain}{rule} &&
|
|
$profile_data->{$allow}{netdomain}{rule} eq 'all') {
|
|
$audit = "audit " if $profile_data->{$allow}{netdomain}{audit}{all};
|
|
push @data, "${pre}${audit}network,";
|
|
} else {
|
|
for my $fam (sort keys %{$profile_data->{$allow}{netdomain}{rule}}) {
|
|
if ( $profile_data->{$allow}{netdomain}{rule}{$fam} == 1 ) {
|
|
$audit = "audit " if $profile_data->{$allow}{netdomain}{audit}{$fam};
|
|
push @data, "${pre}${audit}${allowstr}network $fam,";
|
|
} else {
|
|
for my $type
|
|
(sort keys %{$profile_data->{$allow}{netdomain}{rule}{$fam}}) {
|
|
$audit = "audit " if $profile_data->{$allow}{netdomain}{audit}{$fam}{$type};
|
|
push @data, "${pre}${audit}${allowstr}network $fam $type,";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
push @data, "" if %{$profile_data->{$allow}{netdomain}};
|
|
}
|
|
return @data;
|
|
|
|
}
|
|
|
|
sub writenetdomain ($$) {
|
|
my ($prof_data, $depth) = @_;
|
|
my @data;
|
|
|
|
push @data, writenet_rules($prof_data, $depth, 'deny');
|
|
push @data, writenet_rules($prof_data, $depth, 'allow');
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub writelink_rules ($$$) {
|
|
my ($profile_data, $depth, $allow) = @_;
|
|
|
|
my $allowstr = $allow eq 'deny' ? 'deny ' : '';
|
|
my $pre = " " x $depth;
|
|
|
|
my @data;
|
|
if (exists $profile_data->{$allow}{link}) {
|
|
for my $path (sort keys %{$profile_data->{$allow}{link}}) {
|
|
my $to = $profile_data->{$allow}{link}{$path}{to};
|
|
my $subset = ($profile_data->{$allow}{link}{$path}{mode} & $AA_LINK_SUBSET) ? 'subset ' : '';
|
|
my $audit = ($profile_data->{$allow}{link}{$path}{audit}) ? 'audit ' : '';
|
|
# deal with whitespace in path names
|
|
$path = quote_if_needed($path);
|
|
$to = quote_if_needed($to);
|
|
push @data, "${pre}${audit}${allowstr}link ${subset}${path} -> ${to},";
|
|
}
|
|
push @data, "";
|
|
}
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub writelinks ($$) {
|
|
my ($profile_data, $depth) = @_;
|
|
my @data;
|
|
|
|
push @data, writelink_rules($profile_data, $depth, 'deny');
|
|
push @data, writelink_rules($profile_data, $depth, 'allow');
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub writepath_rules ($$$) {
|
|
my ($profile_data, $depth, $allow) = @_;
|
|
|
|
my $allowstr = $allow eq 'deny' ? 'deny ' : '';
|
|
my $pre = " " x $depth;
|
|
|
|
my @data;
|
|
if (exists $profile_data->{$allow}{path}) {
|
|
for my $path (sort keys %{$profile_data->{$allow}{path}}) {
|
|
my $mode = $profile_data->{$allow}{path}{$path}{mode};
|
|
my $audit = $profile_data->{$allow}{path}{$path}{audit};
|
|
my $tail = "";
|
|
$tail = " -> " . $profile_data->{$allow}{path}{$path}{to} if ($profile_data->{$allow}{path}{$path}{to});
|
|
my ($user, $other) = split_mode($mode);
|
|
my ($user_audit, $other_audit) = split_mode($audit);
|
|
# determine whether the rule contains any owner only components
|
|
|
|
while ($user || $other) {
|
|
my $ownerstr = "";
|
|
my ($tmpmode, $tmpaudit) = 0;
|
|
if ($user & ~$other) {
|
|
# user contains bits not set in other
|
|
$ownerstr = "owner ";
|
|
$tmpmode = $user & ~$other;
|
|
$tmpaudit = $user_audit;
|
|
$user &= ~$tmpmode;
|
|
# } elsif ($other & ~$user) {
|
|
# $ownerstr = "other ";
|
|
# $tmpmode = $other & ~$user;
|
|
# $tmpaudit = $other_audit;
|
|
# $other &= ~$tmpmode;
|
|
} else {
|
|
if ($user_audit & ~$other_audit & $user) {
|
|
$ownerstr = "owner ";
|
|
$tmpaudit = $user_audit & ~$other_audit & $user;
|
|
$tmpmode = $user & $tmpaudit;
|
|
$user &= ~$tmpmode;
|
|
# } elsif ($other_audit & ~$user_audit & $other) {
|
|
# $ownerstr = "other ";
|
|
# $tmpaudit = $other_audit & ~$user_audit & $other;
|
|
# $tmpmode = $other & $tmpaudit;
|
|
# $other &= ~$tmpmode;
|
|
} else {
|
|
# user == other && user_audit == other_audit
|
|
$ownerstr = "";
|
|
#include exclusive other for now
|
|
# $tmpmode = $user;
|
|
# $tmpaudit = $user_audit;
|
|
$tmpmode = $user | $other;
|
|
$tmpaudit = $user_audit | $other_audit;
|
|
$user &= ~$tmpmode;
|
|
$other &= ~$tmpmode;
|
|
}
|
|
}
|
|
|
|
if ($tmpmode & $tmpaudit) {
|
|
my $modestr = mode_to_str($tmpmode & $tmpaudit);
|
|
if ($path =~ /\s/) {
|
|
push @data, "${pre}audit ${allowstr}${ownerstr}\"$path\" ${modestr}${tail},";
|
|
} else {
|
|
push @data, "${pre}audit ${allowstr}${ownerstr}$path ${modestr}${tail},";
|
|
}
|
|
$tmpmode &= ~$tmpaudit;
|
|
}
|
|
if ($tmpmode) {
|
|
my $modestr = mode_to_str($tmpmode);
|
|
if ($path =~ /\s/) {
|
|
push @data, "${pre}${allowstr}${ownerstr}\"$path\" ${modestr}${tail},";
|
|
} else {
|
|
push @data, "${pre}${allowstr}${ownerstr}$path ${modestr}${tail},";
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
push @data, "";
|
|
}
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub writepaths ($$) {
|
|
my ($prof_data, $depth) = @_;
|
|
|
|
my @data;
|
|
push @data, writepath_rules($prof_data, $depth, 'deny');
|
|
push @data, writepath_rules($prof_data, $depth, 'allow');
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub write_rules ($$) {
|
|
my ($prof_data, $depth) = @_;
|
|
|
|
my @data;
|
|
push @data, writealiases($prof_data, $depth);
|
|
push @data, writelistvars($prof_data, $depth);
|
|
push @data, writeincludes($prof_data, $depth);
|
|
push @data, writerlimits($prof_data, $depth);
|
|
push @data, writecapabilities($prof_data, $depth);
|
|
push @data, writenetdomain($prof_data, $depth);
|
|
push @data, writelinks($prof_data, $depth);
|
|
push @data, writepaths($prof_data, $depth);
|
|
push @data, writechange_profile($prof_data, $depth);
|
|
|
|
return @data;
|
|
}
|
|
|
|
sub writepiece ($$$$$);
|
|
sub writepiece ($$$$$) {
|
|
my ($profile_data, $depth, $name, $nhat, $write_flags) = @_;
|
|
|
|
my $pre = ' ' x $depth;
|
|
my @data;
|
|
my $wname;
|
|
my $inhat = 0;
|
|
if ($name eq $nhat) {
|
|
$wname = $name;
|
|
} else {
|
|
$wname = "$name//$nhat";
|
|
$name = $nhat;
|
|
$inhat = 1;
|
|
}
|
|
push @data, writeheader($profile_data->{$name}, $depth, $wname, 0, $write_flags);
|
|
push @data, write_rules($profile_data->{$name}, $depth + 1);
|
|
|
|
my $pre2 = ' ' x ($depth + 1);
|
|
# write external hat declarations
|
|
for my $hat (grep { $_ ne $name } sort keys %{$profile_data}) {
|
|
if ($profile_data->{$hat}{declared}) {
|
|
push @data, "${pre2}^$hat,";
|
|
}
|
|
}
|
|
|
|
if (!$inhat) {
|
|
# write embedded hats
|
|
for my $hat (grep { $_ ne $name } sort keys %{$profile_data}) {
|
|
if ((not $profile_data->{$hat}{external}) and
|
|
(not $profile_data->{$hat}{declared})) {
|
|
push @data, "";
|
|
if ($profile_data->{$hat}{profile}) {
|
|
push @data, map { "$_" } writeheader($profile_data->{$hat},
|
|
$depth + 1, $hat,
|
|
1, $write_flags);
|
|
} else {
|
|
push @data, map { "$_" } writeheader($profile_data->{$hat},
|
|
$depth + 1, "^$hat",
|
|
1, $write_flags);
|
|
}
|
|
push @data, map { "$_" } write_rules($profile_data->{$hat},
|
|
$depth + 2);
|
|
push @data, "${pre2}}";
|
|
}
|
|
}
|
|
push @data, "${pre}}";
|
|
|
|
#write external hats
|
|
for my $hat (grep { $_ ne $name } sort keys %{$profile_data}) {
|
|
if (($name eq $nhat) and $profile_data->{$hat}{external}) {
|
|
push @data, "";
|
|
push @data, map { " $_" } writepiece($profile_data, $depth - 1,
|
|
$name, $hat, $write_flags);
|
|
push @data, " }";
|
|
}
|
|
}
|
|
}
|
|
return @data;
|
|
}
|
|
|
|
sub serialize_profile($$$) {
|
|
my ($profile_data, $name, $options) = @_;
|
|
|
|
my $string = "";
|
|
my $include_metadata = 0; # By default don't write out metadata
|
|
my $include_flags = 1;
|
|
if ( $options and ref($options) eq "HASH" ) {
|
|
$include_metadata = 1 if ( defined $options->{METADATA} );
|
|
$include_flags = 0 if ( defined $options->{NO_FLAGS} );
|
|
}
|
|
|
|
if ($include_metadata) {
|
|
# keep track of when the file was last updated
|
|
$string .= "# Last Modified: " . localtime(time) . "\n";
|
|
|
|
# print out repository metadata
|
|
if ($profile_data->{$name}{repo} &&
|
|
$profile_data->{$name}{repo}{url} &&
|
|
$profile_data->{$name}{repo}{user} &&
|
|
$profile_data->{$name}{repo}{id}) {
|
|
my $repo = $profile_data->{$name}{repo};
|
|
$string .= "# REPOSITORY: $repo->{url} $repo->{user} $repo->{id}\n";
|
|
} elsif ($profile_data->{$name}{repo}{neversubmit}) {
|
|
$string .= "# REPOSITORY: NEVERSUBMIT\n";
|
|
}
|
|
}
|
|
|
|
# print out initial comment
|
|
if ($profile_data->{$name}{initial_comment}) {
|
|
my $comment = $profile_data->{$name}{initial_comment};
|
|
$comment =~ s/\\n/\n/g;
|
|
$string .= "$comment\n";
|
|
}
|
|
|
|
#bleah this is stupid the data structure needs to be reworked
|
|
my $filename = getprofilefilename($name);
|
|
my @data;
|
|
if ($filelist{$filename}) {
|
|
push @data, writealiases($filelist{$filename}, 0);
|
|
push @data, writelistvars($filelist{$filename}, 0);
|
|
push @data, writeincludes($filelist{$filename}, 0);
|
|
}
|
|
|
|
|
|
# XXX - FIXME
|
|
#
|
|
# # dump variables defined in this file
|
|
# if ($variables{$filename}) {
|
|
# for my $var (sort keys %{$variables{$filename}}) {
|
|
# if ($var =~ m/^@/) {
|
|
# my @values = sort @{$variables{$filename}{$var}};
|
|
# @values = map { escape($_) } @values;
|
|
# my $values = join (" ", @values);
|
|
# print SDPROF "$var = ";
|
|
# print SDPROF $values;
|
|
# } elsif ($var =~ m/^\$/) {
|
|
# print SDPROF "$var = ";
|
|
# print SDPROF ${$variables{$filename}{$var}};
|
|
# } elsif ($var =~ m/^\#/) {
|
|
# my $inc = $var;
|
|
# $inc =~ s/^\#//;
|
|
# print SDPROF "#include <$inc>";
|
|
# }
|
|
# print SDPROF "\n";
|
|
# }
|
|
# }
|
|
|
|
push @data, writepiece($profile_data, 0, $name, $name, $include_flags);
|
|
$string .= join("\n", @data);
|
|
|
|
return "$string\n";
|
|
}
|
|
|
|
sub writeprofile_ui_feedback ($) {
|
|
my $profile = shift;
|
|
UI_Info(sprintf(gettext('Writing updated profile for %s.'), $profile));
|
|
writeprofile($profile);
|
|
}
|
|
|
|
sub writeprofile ($) {
|
|
my ($profile) = shift;
|
|
|
|
my $filename = $sd{$profile}{$profile}{filename} || getprofilefilename($profile);
|
|
|
|
open(SDPROF, ">$filename") or
|
|
fatal_error "Can't write new AppArmor profile $filename: $!";
|
|
my $serialize_opts = { };
|
|
$serialize_opts->{METADATA} = 1;
|
|
|
|
#make sure to write out all the profiles in the file
|
|
my $profile_string = serialize_profile($sd{$profile}, $profile, $serialize_opts);
|
|
print SDPROF $profile_string;
|
|
close(SDPROF);
|
|
|
|
# mark the profile as up-to-date
|
|
delete $changed{$profile};
|
|
$original_sd{$profile} = dclone($sd{$profile});
|
|
}
|
|
|
|
sub getprofileflags($) {
|
|
my $filename = shift;
|
|
|
|
my $flags = "enforce";
|
|
|
|
if (open(PROFILE, "$filename")) {
|
|
while (<PROFILE>) {
|
|
if (m/^\s*\/\S+\s+flags=\((.+)\)\s+{\s*$/) {
|
|
$flags = $1;
|
|
close(PROFILE);
|
|
return $flags;
|
|
}
|
|
}
|
|
close(PROFILE);
|
|
}
|
|
|
|
return $flags;
|
|
}
|
|
|
|
|
|
sub matchliteral($$) {
|
|
my ($sd_regexp, $literal) = @_;
|
|
|
|
my $p_regexp = convert_regexp($sd_regexp);
|
|
|
|
# check the log entry against our converted regexp...
|
|
my $matches = eval { $literal =~ /^$p_regexp$/; };
|
|
|
|
# doesn't match if we've got a broken regexp
|
|
return undef if $@;
|
|
|
|
return $matches;
|
|
}
|
|
|
|
# test if profile has exec rule for $exec_target
|
|
sub profile_known_exec (\%$$) {
|
|
my ($profile, $type, $exec_target) = @_;
|
|
if ( $type eq "exec" ) {
|
|
my ($cm, $am, @m);
|
|
|
|
# test denies first
|
|
($cm, $am, @m) = rematchfrag($profile, 'deny', $exec_target);
|
|
if ($cm & $AA_MAY_EXEC) {
|
|
return -1;
|
|
}
|
|
($cm, $am, @m) = match_prof_incs_to_path($profile, 'deny', $exec_target);
|
|
if ($cm & $AA_MAY_EXEC) {
|
|
return -1;
|
|
}
|
|
|
|
# now test the generally longer allow lists
|
|
($cm, $am, @m) = rematchfrag($profile, 'allow', $exec_target);
|
|
if ($cm & $AA_MAY_EXEC) {
|
|
return 1;
|
|
}
|
|
|
|
($cm, $am, @m) = match_prof_incs_to_path($profile, 'allow', $exec_target);
|
|
if ($cm & $AA_MAY_EXEC) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub profile_known_capability (\%$) {
|
|
my ($profile, $capname) = @_;
|
|
|
|
return -1 if $profile->{deny}{capability}{$capname}{set};
|
|
return 1 if $profile->{allow}{capability}{$capname}{set};
|
|
for my $incname ( keys %{$profile->{include}} ) {
|
|
return -1 if $include{$incname}{$incname}{deny}{capability}{$capname}{set};
|
|
return 1 if $include{$incname}{$incname}{allow}{capability}{$capname}{set};
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub profile_known_network (\%$$) {
|
|
my ($profile, $family, $sock_type) = @_;
|
|
|
|
return -1 if netrules_access_check( $profile->{deny}{netdomain},
|
|
$family, $sock_type);
|
|
return 1 if netrules_access_check( $profile->{allow}{netdomain},
|
|
$family, $sock_type);
|
|
|
|
for my $incname ( keys %{$profile->{include}} ) {
|
|
return -1 if netrules_access_check($include{$incname}{$incname}{deny}{netdomain},
|
|
$family, $sock_type);
|
|
return 1 if netrules_access_check($include{$incname}{$incname}{allow}{netdomain},
|
|
$family, $sock_type);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub netrules_access_check ($$$) {
|
|
my ($netrules, $family, $sock_type) = @_;
|
|
return 0 if ( not defined $netrules );
|
|
my %netrules = %$netrules;
|
|
my $all_net = defined $netrules{rule}{all};
|
|
my $all_net_family = defined $netrules{rule}{$family} && $netrules{rule}{$family} == 1;
|
|
my $net_family_sock = defined $netrules{rule}{$family} &&
|
|
ref($netrules{rule}{$family}) eq "HASH" &&
|
|
defined $netrules{rule}{$family}{$sock_type};
|
|
|
|
if ( $all_net || $all_net_family || $net_family_sock ) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
sub reload_base($) {
|
|
my $bin = shift;
|
|
|
|
# don't try to reload profile if AppArmor is not running
|
|
return unless check_for_subdomain();
|
|
|
|
my $filename = getprofilefilename($bin);
|
|
|
|
system("/bin/cat '$filename' | $parser -I$profiledir -r >/dev/null 2>&1");
|
|
}
|
|
|
|
sub reload ($) {
|
|
my $bin = shift;
|
|
|
|
# don't reload the profile if the corresponding executable doesn't exist
|
|
my $fqdbin = findexecutable($bin) or return;
|
|
|
|
return reload_base($fqdbin);
|
|
}
|
|
|
|
sub read_include_from_file($) {
|
|
my $which = shift;
|
|
|
|
my $data;
|
|
if (open(INCLUDE, "$profiledir/$which")) {
|
|
local $/;
|
|
$data = <INCLUDE>;
|
|
close(INCLUDE);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
sub get_include_data($) {
|
|
my $which = shift;
|
|
|
|
my $data = read_include_from_file($which);
|
|
unless($data) {
|
|
fatal_error "Can't find include file $which: $!";
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
sub loadinclude($) {
|
|
my $which = shift;
|
|
|
|
# don't bother loading it again if we already have
|
|
return 0 if $include{$which}{$which};
|
|
|
|
my @loadincludes = ($which);
|
|
while (my $incfile = shift @loadincludes) {
|
|
|
|
my $data = get_include_data($incfile);
|
|
my $incdata = parse_profile_data($data, $incfile, 1);
|
|
if ($incdata) {
|
|
attach_profile_data(\%include, $incdata);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub rematchfrag ($$$) {
|
|
my ($frag, $allow, $path) = @_;
|
|
|
|
my $combinedmode = 0;
|
|
my $combinedaudit = 0;
|
|
my @matches;
|
|
|
|
for my $entry (keys %{ $frag->{$allow}{path} }) {
|
|
|
|
my $regexp = convert_regexp($entry);
|
|
|
|
# check the log entry against our converted regexp...
|
|
if ($path =~ /^$regexp$/) {
|
|
|
|
# regexp matches, add it's mode to the list to check against
|
|
$combinedmode |= $frag->{$allow}{path}{$entry}{mode};
|
|
$combinedaudit |= $frag->{$allow}{path}{$entry}{audit};
|
|
push @matches, $entry;
|
|
}
|
|
}
|
|
|
|
return wantarray ? ($combinedmode, $combinedaudit, @matches) : $combinedmode;
|
|
}
|
|
|
|
sub match_include_to_path ($$$) {
|
|
my ($incname, $allow, $path) = @_;
|
|
|
|
my $combinedmode = 0;
|
|
my $combinedaudit = 0;
|
|
my @matches;
|
|
|
|
my @includelist = ( $incname );
|
|
while (my $incfile = shift @includelist) {
|
|
my $ret = eval { loadinclude($incfile); };
|
|
if ($@) { fatal_error $@; }
|
|
my ($cm, $am, @m) = rematchfrag($include{$incfile}{$incfile}, $allow, $path);
|
|
if ($cm) {
|
|
$combinedmode |= $cm;
|
|
$combinedaudit |= $am;
|
|
push @matches, @m;
|
|
}
|
|
|
|
# check if a literal version is in the current include fragment
|
|
if ($include{$incfile}{$incfile}{$allow}{path}{$path}) {
|
|
$combinedmode |= $include{$incfile}{$incfile}{$allow}{path}{$path}{mode};
|
|
$combinedaudit |= $include{$incfile}{$incfile}{$allow}{path}{$path}{audit};
|
|
}
|
|
|
|
# if this fragment includes others, check them too
|
|
if (keys %{ $include{$incfile}{$incfile}{include} }) {
|
|
push @includelist, keys %{ $include{$incfile}{$incfile}{include} };
|
|
}
|
|
}
|
|
|
|
return wantarray ? ($combinedmode, $combinedaudit, @matches) : $combinedmode;
|
|
}
|
|
|
|
sub match_prof_incs_to_path ($$$) {
|
|
my ($frag, $allow, $path) = @_;
|
|
|
|
my $combinedmode = 0;
|
|
my $combinedaudit = 0;
|
|
my @matches;
|
|
|
|
# scan the include fragments for this profile looking for matches
|
|
my @includelist = keys %{ $frag->{include} };
|
|
while (my $include = shift @includelist) {
|
|
my ($cm, $am, @m) = match_include_to_path($include, $allow, $path);
|
|
if ($cm) {
|
|
$combinedmode |= $cm;
|
|
$combinedaudit |= $am;
|
|
push @matches, @m;
|
|
}
|
|
}
|
|
|
|
return wantarray ? ($combinedmode, $combinedaudit, @matches) : $combinedmode;
|
|
}
|
|
|
|
#find includes that match the path to suggest
|
|
sub suggest_incs_for_path($$$) {
|
|
my ($incname, $path, $allow) = @_;
|
|
|
|
|
|
my $combinedmode = 0;
|
|
my $combinedaudit = 0;
|
|
my @matches;
|
|
|
|
# scan the include fragments looking for matches
|
|
my @includelist = ($incname);
|
|
while (my $include = shift @includelist) {
|
|
my ($cm, $am, @m) = rematchfrag($include{$include}{$include}, 'allow', $path);
|
|
if ($cm) {
|
|
$combinedmode |= $cm;
|
|
$combinedaudit |= $am;
|
|
push @matches, @m;
|
|
}
|
|
|
|
# check if a literal version is in the current include fragment
|
|
if ($include{$include}{$include}{allow}{path}{$path}) {
|
|
$combinedmode |= $include{$include}{$include}{allow}{path}{$path}{mode};
|
|
$combinedaudit |= $include{$include}{$include}{allow}{path}{$path}{audit};
|
|
}
|
|
|
|
# if this fragment includes others, check them too
|
|
if (keys %{ $include{$include}{$include}{include} }) {
|
|
push @includelist, keys %{ $include{$include}{$include}{include} };
|
|
}
|
|
}
|
|
|
|
if ($combinedmode) {
|
|
return wantarray ? ($combinedmode, $combinedaudit, @matches) : $combinedmode;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
sub check_qualifiers($) {
|
|
my $program = shift;
|
|
|
|
if ($cfg->{qualifiers}{$program}) {
|
|
unless($cfg->{qualifiers}{$program} =~ /p/) {
|
|
fatal_error(sprintf(gettext("\%s is currently marked as a program that should not have it's own profile. Usually, programs are marked this way if creating a profile for them is likely to break the rest of the system. If you know what you're doing and are certain you want to create a profile for this program, edit the corresponding entry in the [qualifiers] section in /etc/apparmor/logprof.conf."), $program));
|
|
}
|
|
}
|
|
}
|
|
|
|
sub loadincludes() {
|
|
if (opendir(SDDIR, $profiledir)) {
|
|
my @incdirs = grep { (!/^\./) && (-d "$profiledir/$_") } readdir(SDDIR);
|
|
close(SDDIR);
|
|
|
|
while (my $id = shift @incdirs) {
|
|
next if isSkippableDir($id);
|
|
if (opendir(SDDIR, "$profiledir/$id")) {
|
|
for my $path (readdir(SDDIR)) {
|
|
chomp($path);
|
|
next if isSkippableFile($path);
|
|
if (-f "$profiledir/$id/$path") {
|
|
my $file = "$id/$path";
|
|
$file =~ s/$profiledir\///;
|
|
my $ret = eval { loadinclude($file); };
|
|
if ($@) { fatal_error $@; }
|
|
} elsif (-d "$id/$path") {
|
|
push @incdirs, "$id/$path";
|
|
}
|
|
}
|
|
closedir(SDDIR);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sub globcommon ($) {
|
|
my $path = shift;
|
|
|
|
my @globs;
|
|
|
|
# glob library versions in both foo-5.6.so and baz.so.9.2 form
|
|
if ($path =~ m/[\d\.]+\.so$/ || $path =~ m/\.so\.[\d\.]+$/) {
|
|
my $libpath = $path;
|
|
$libpath =~ s/[\d\.]+\.so$/*.so/;
|
|
$libpath =~ s/\.so\.[\d\.]+$/.so.*/;
|
|
push @globs, $libpath if $libpath ne $path;
|
|
}
|
|
|
|
for my $glob (keys %{$cfg->{globs}}) {
|
|
if ($path =~ /$glob/) {
|
|
my $globbedpath = $path;
|
|
$globbedpath =~ s/$glob/$cfg->{globs}{$glob}/g;
|
|
push @globs, $globbedpath if $globbedpath ne $path;
|
|
}
|
|
}
|
|
|
|
if (wantarray) {
|
|
return sort { length($b) <=> length($a) } uniq(@globs);
|
|
} else {
|
|
my @list = sort { length($b) <=> length($a) } uniq(@globs);
|
|
return $list[$#list];
|
|
}
|
|
}
|
|
|
|
# this is an ugly, nasty function that attempts to see if one regexp
|
|
# is a subset of another regexp
|
|
sub matchregexp ($$) {
|
|
my ($new, $old) = @_;
|
|
|
|
# bail out if old pattern has {foo,bar,baz} stuff in it
|
|
return undef if $old =~ /\{.*(\,.*)*\}/;
|
|
|
|
# are there any regexps at all in the old pattern?
|
|
if ($old =~ /\[.+\]/ or $old =~ /\*/ or $old =~ /\?/) {
|
|
|
|
# convert {foo,baz} to (foo|baz)
|
|
$new =~ y/\{\}\,/\(\)\|/ if $new =~ /\{.*\,.*\}/;
|
|
|
|
# \001 == SD_GLOB_RECURSIVE
|
|
# \002 == SD_GLOB_SIBLING
|
|
|
|
$new =~ s/\*\*/\001/g;
|
|
$new =~ s/\*/\002/g;
|
|
|
|
$old =~ s/\*\*/\001/g;
|
|
$old =~ s/\*/\002/g;
|
|
|
|
# strip common prefix
|
|
my $prefix = commonprefix($new, $old);
|
|
if ($prefix) {
|
|
|
|
# make sure we don't accidentally gobble up a trailing * or **
|
|
$prefix =~ s/(\001|\002)$//;
|
|
$new =~ s/^$prefix//;
|
|
$old =~ s/^$prefix//;
|
|
}
|
|
|
|
# strip common suffix
|
|
my $suffix = commonsuffix($new, $old);
|
|
if ($suffix) {
|
|
|
|
# make sure we don't accidentally gobble up a leading * or **
|
|
$suffix =~ s/^(\001|\002)//;
|
|
$new =~ s/$suffix$//;
|
|
$old =~ s/$suffix$//;
|
|
}
|
|
|
|
# if we boiled the differences down to a ** in the new entry, it matches
|
|
# whatever's in the old entry
|
|
return 1 if $new eq "\001";
|
|
|
|
# if we've paired things down to a * in new, old matches if there are no
|
|
# slashes left in the path
|
|
return 1 if ($new eq "\002" && $old =~ /^[^\/]+$/);
|
|
|
|
# we'll bail out if we have more globs in the old version
|
|
return undef if $old =~ /\001|\002/;
|
|
|
|
# see if we can match * globs in new against literal elements in old
|
|
$new =~ s/\002/[^\/]*/g;
|
|
|
|
return 1 if $old =~ /^$new$/;
|
|
|
|
} else {
|
|
|
|
my $new_regexp = convert_regexp($new);
|
|
|
|
# check the log entry against our converted regexp...
|
|
return 1 if $old =~ /^$new_regexp$/;
|
|
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub combine_name($$) { return ($_[0] eq $_[1]) ? $_[0] : "$_[0]^$_[1]"; }
|
|
sub split_name ($) { my ($p, $h) = split(/\^/, $_[0]); $h ||= $p; ($p, $h); }
|
|
|
|
##########################
|
|
#
|
|
# prompt_user($headers, $functions, $default, $options, $selected);
|
|
#
|
|
# $headers:
|
|
# a required arrayref made up of "key, value" pairs in the order you'd
|
|
# like them displayed to user
|
|
#
|
|
# $functions:
|
|
# a required arrayref of the different options to display at the bottom
|
|
# of the prompt like "(A)llow", "(D)eny", and "Ba(c)on". the character
|
|
# contained by ( and ) will be used as the key to select the specified
|
|
# option.
|
|
#
|
|
# $default:
|
|
# a required character which is the default "key" to enter when they
|
|
# just hit enter
|
|
#
|
|
# $options:
|
|
# an optional arrayref of the choices like the glob suggestions to be
|
|
# presented to the user
|
|
#
|
|
# $selected:
|
|
# specifies which option is currently selected
|
|
#
|
|
# when prompt_user() is called without an $options list, it returns a
|
|
# single value which is the key for the specified "function".
|
|
#
|
|
# when prompt_user() is called with an $options list, it returns an array
|
|
# of two elements, the key for the specified function as well as which
|
|
# option was currently selected
|
|
#######################################################################
|
|
|
|
sub Text_PromptUser ($) {
|
|
my $question = shift;
|
|
|
|
my $title = $question->{title};
|
|
my $explanation = $question->{explanation};
|
|
|
|
my @headers = (@{ $question->{headers} });
|
|
my @functions = (@{ $question->{functions} });
|
|
|
|
my $default = $question->{default};
|
|
my $options = $question->{options};
|
|
my $selected = $question->{selected} || 0;
|
|
|
|
my $helptext = $question->{helptext};
|
|
|
|
push @functions, "CMD_HELP" if $helptext;
|
|
|
|
my %keys;
|
|
my @menu_items;
|
|
for my $cmd (@functions) {
|
|
|
|
# make sure we know about this particular command
|
|
my $cmdmsg = "PromptUser: " . gettext("Unknown command") . " $cmd";
|
|
fatal_error $cmdmsg unless $CMDS{$cmd};
|
|
|
|
# grab the localized text to use for the menu for this command
|
|
my $menutext = gettext($CMDS{$cmd});
|
|
|
|
# figure out what the hotkey for this menu item is
|
|
my $menumsg = "PromptUser: " .
|
|
gettext("Invalid hotkey in") .
|
|
" '$menutext'";
|
|
$menutext =~ /\((\S)\)/ or fatal_error $menumsg;
|
|
|
|
# we want case insensitive comparisons so we'll force things to
|
|
# lowercase
|
|
my $key = lc($1);
|
|
|
|
# check if we're already using this hotkey for this prompt
|
|
my $hotkeymsg = "PromptUser: " .
|
|
gettext("Duplicate hotkey for") .
|
|
" $cmd: $menutext";
|
|
fatal_error $hotkeymsg if $keys{$key};
|
|
|
|
# keep track of which command they're picking if they hit this hotkey
|
|
$keys{$key} = $cmd;
|
|
|
|
if ($default && $default eq $cmd) {
|
|
$menutext = "[$menutext]";
|
|
}
|
|
|
|
push @menu_items, $menutext;
|
|
}
|
|
|
|
# figure out the key for the default option
|
|
my $default_key;
|
|
if ($default && $CMDS{$default}) {
|
|
my $defaulttext = gettext($CMDS{$default});
|
|
|
|
# figure out what the hotkey for this menu item is
|
|
my $defmsg = "PromptUser: " .
|
|
gettext("Invalid hotkey in default item") .
|
|
" '$defaulttext'";
|
|
$defaulttext =~ /\((\S)\)/ or fatal_error $defmsg;
|
|
|
|
# we want case insensitive comparisons so we'll force things to
|
|
# lowercase
|
|
$default_key = lc($1);
|
|
|
|
my $defkeymsg = "PromptUser: " .
|
|
gettext("Invalid default") .
|
|
" $default";
|
|
fatal_error $defkeymsg unless $keys{$default_key};
|
|
}
|
|
|
|
my $widest = 0;
|
|
my @poo = @headers;
|
|
while (my $header = shift @poo) {
|
|
my $value = shift @poo;
|
|
$widest = length($header) if length($header) > $widest;
|
|
}
|
|
$widest++;
|
|
|
|
my $format = '%-' . $widest . "s \%s\n";
|
|
|
|
my $function_regexp = '^(';
|
|
$function_regexp .= join("|", keys %keys);
|
|
$function_regexp .= '|\d' if $options;
|
|
$function_regexp .= ')$';
|
|
|
|
my $ans = "XXXINVALIDXXX";
|
|
while ($ans !~ /$function_regexp/i) {
|
|
# build up the prompt...
|
|
my $prompt = "\n";
|
|
|
|
$prompt .= "= $title =\n\n" if $title;
|
|
|
|
if (@headers) {
|
|
my @poo = @headers;
|
|
while (my $header = shift @poo) {
|
|
my $value = shift @poo;
|
|
$prompt .= sprintf($format, "$header:", $value);
|
|
}
|
|
$prompt .= "\n";
|
|
}
|
|
|
|
if ($explanation) {
|
|
$prompt .= "$explanation\n\n";
|
|
}
|
|
|
|
if ($options) {
|
|
for (my $i = 0; $options->[$i]; $i++) {
|
|
my $f = ($selected == $i) ? ' [%d - %s]' : ' %d - %s ';
|
|
$prompt .= sprintf("$f\n", $i + 1, $options->[$i]);
|
|
}
|
|
$prompt .= "\n";
|
|
}
|
|
$prompt .= join(" / ", @menu_items);
|
|
print "$prompt\n";
|
|
|
|
# get their input...
|
|
$ans = lc(getkey());
|
|
|
|
if ($ans) {
|
|
# handle escape sequences so you can up/down in the list
|
|
if ($ans eq "up") {
|
|
|
|
if ($options && ($selected > 0)) {
|
|
$selected--;
|
|
}
|
|
$ans = "XXXINVALIDXXX";
|
|
|
|
} elsif ($ans eq "down") {
|
|
|
|
if ($options && ($selected < (scalar(@$options) - 1))) {
|
|
$selected++;
|
|
}
|
|
$ans = "XXXINVALIDXXX";
|
|
|
|
} elsif ($keys{$ans} && $keys{$ans} eq "CMD_HELP") {
|
|
|
|
print "\n$helptext\n";
|
|
$ans = "XXXINVALIDXXX";
|
|
|
|
} elsif (ord($ans) == 10) {
|
|
|
|
# pick the default if they hit return...
|
|
$ans = $default_key;
|
|
|
|
} elsif ($options && ($ans =~ /^\d$/)) {
|
|
|
|
# handle option poo
|
|
if ($ans > 0 && $ans <= scalar(@$options)) {
|
|
$selected = $ans - 1;
|
|
}
|
|
$ans = "XXXINVALIDXXX";
|
|
}
|
|
}
|
|
|
|
if ($keys{$ans} && $keys{$ans} eq "CMD_HELP") {
|
|
print "\n$helptext\n";
|
|
$ans = "again";
|
|
}
|
|
}
|
|
|
|
# pull our command back from our hotkey map
|
|
$ans = $keys{$ans} if $keys{$ans};
|
|
return ($ans, $selected);
|
|
|
|
}
|
|
|
|
# Parse event record into key-value pairs
|
|
sub parse_event($) {
|
|
my %ev = ();
|
|
my $msg = shift;
|
|
chomp($msg);
|
|
my $event = LibAppArmor::parse_record($msg);
|
|
my ($rmask, $dmask);
|
|
|
|
$DEBUGGING && debug("parse_event: $msg");
|
|
|
|
$ev{'resource'} = LibAppArmor::aa_log_record::swig_info_get($event);
|
|
$ev{'active_hat'} = LibAppArmor::aa_log_record::swig_active_hat_get($event);
|
|
$ev{'sdmode'} = LibAppArmor::aa_log_record::swig_event_get($event);
|
|
$ev{'time'} = LibAppArmor::aa_log_record::swig_epoch_get($event);
|
|
$ev{'operation'} = LibAppArmor::aa_log_record::swig_operation_get($event);
|
|
$ev{'profile'} = LibAppArmor::aa_log_record::swig_profile_get($event);
|
|
$ev{'name'} = LibAppArmor::aa_log_record::swig_name_get($event);
|
|
$ev{'name2'} = LibAppArmor::aa_log_record::swig_name2_get($event);
|
|
$ev{'attr'} = LibAppArmor::aa_log_record::swig_attribute_get($event);
|
|
$ev{'parent'} = LibAppArmor::aa_log_record::swig_parent_get($event);
|
|
$ev{'pid'} = LibAppArmor::aa_log_record::swig_pid_get($event);
|
|
$ev{'task'} = LibAppArmor::aa_log_record::swig_task_get($event);
|
|
$ev{'info'} = LibAppArmor::aa_log_record::swig_info_get($event);
|
|
$dmask = LibAppArmor::aa_log_record::swig_denied_mask_get($event);
|
|
$rmask = LibAppArmor::aa_log_record::swig_requested_mask_get($event);
|
|
$ev{'magic_token'} =
|
|
LibAppArmor::aa_log_record::swig_magic_token_get($event);
|
|
|
|
# NetDomain
|
|
if ( $ev{'operation'} && optype($ev{'operation'}) eq "net" ) {
|
|
$ev{'family'} =
|
|
LibAppArmor::aa_log_record::swig_net_family_get($event);
|
|
$ev{'protocol'} =
|
|
LibAppArmor::aa_log_record::swig_net_protocol_get($event);
|
|
$ev{'sock_type'} =
|
|
LibAppArmor::aa_log_record::swig_net_sock_type_get($event);
|
|
}
|
|
|
|
LibAppArmor::free_record($event);
|
|
|
|
#map new c and d to w as logprof doesn't support them yet
|
|
if ($rmask) {
|
|
$rmask =~ s/c/w/g;
|
|
$rmask =~ s/d/w/g;
|
|
}
|
|
if ($dmask) {
|
|
$dmask =~ s/c/w/g;
|
|
$dmask =~ s/d/w/g;
|
|
}
|
|
|
|
if ($rmask && !validate_log_mode(hide_log_mode($rmask))) {
|
|
fatal_error(sprintf(gettext('Log contains unknown mode %s.'),
|
|
$rmask));
|
|
}
|
|
|
|
if ($dmask && !validate_log_mode(hide_log_mode($dmask))) {
|
|
fatal_error(sprintf(gettext('Log contains unknown mode %s.'),
|
|
$dmask));
|
|
}
|
|
#print "str_to_mode deny $dmask = " . str_to_mode($dmask) . "\n" if ($dmask);
|
|
#print "str_to_mode req $rmask = " . str_to_mode($rmask) . "\n" if ($rmask);
|
|
|
|
my ($mask, $name);
|
|
($mask, $name) = log_str_to_mode($ev{profile}, $dmask, $ev{name2});
|
|
$ev{'denied_mask'} = $mask;
|
|
$ev{name2} = $name;
|
|
|
|
($mask, $name) = log_str_to_mode($ev{profile}, $rmask, $ev{name2});
|
|
$ev{'request_mask'} = $mask;
|
|
$ev{name2} = $name;
|
|
|
|
if ( ! $ev{'time'} ) { $ev{'time'} = time; }
|
|
|
|
# remove null responses
|
|
for (keys(%ev)) {
|
|
if ( ! $ev{$_} || $ev{$_} !~ /[\/\w]+/) { delete($ev{$_}); }
|
|
}
|
|
|
|
if ( $ev{'sdmode'} ) {
|
|
#0 = invalid, 1 = error, 2 = AUDIT, 3 = ALLOW/PERMIT,
|
|
#4 = DENIED/REJECTED, 5 = HINT, 6 = STATUS/config change
|
|
if ( $ev{'sdmode'} == 0 ) { $ev{'sdmode'} = "UNKNOWN"; }
|
|
elsif ( $ev{'sdmode'} == 1 ) { $ev{'sdmode'} = "ERROR"; }
|
|
elsif ( $ev{'sdmode'} == 2 ) { $ev{'sdmode'} = "AUDITING"; }
|
|
elsif ( $ev{'sdmode'} == 3 ) { $ev{'sdmode'} = "PERMITTING"; }
|
|
elsif ( $ev{'sdmode'} == 4 ) { $ev{'sdmode'} = "REJECTING"; }
|
|
elsif ( $ev{'sdmode'} == 5 ) { $ev{'sdmode'} = "HINT"; }
|
|
elsif ( $ev{'sdmode'} == 6 ) { $ev{'sdmode'} = "STATUS"; }
|
|
else { delete($ev{'sdmode'}); }
|
|
}
|
|
if ( $ev{sdmode} ) {
|
|
$DEBUGGING && debug( Data::Dumper->Dump([%ev], [qw(*event)]));
|
|
return \%ev;
|
|
} else {
|
|
return( undef );
|
|
}
|
|
}
|
|
|
|
###############################################################################
|
|
# required initialization
|
|
|
|
$cfg = read_config("logprof.conf");
|
|
if ((not defined $cfg->{settings}{default_owner_prompt})) {
|
|
$cfg->{settings}{default_owner_prompt} = 0;
|
|
}
|
|
|
|
$profiledir = find_first_dir($cfg->{settings}{profiledir}) || "/etc/apparmor.d";
|
|
unless (-d $profiledir) { fatal_error "Can't find AppArmor profiles."; }
|
|
|
|
$extraprofiledir = find_first_dir($cfg->{settings}{inactive_profiledir}) ||
|
|
"/etc/apparmor/profiles/extras/";
|
|
|
|
$parser = find_first_file($cfg->{settings}{parser}) || "/sbin/apparmor_parser";
|
|
unless (-x $parser) { fatal_error "Can't find apparmor_parser."; }
|
|
|
|
$filename = find_first_file($cfg->{settings}{logfiles}) || "/var/log/messages";
|
|
unless (-f $filename) { fatal_error "Can't find system log."; }
|
|
|
|
$ldd = find_first_file($cfg->{settings}{ldd}) || "/usr/bin/ldd";
|
|
unless (-x $ldd) { fatal_error "Can't find ldd."; }
|
|
|
|
$logger = find_first_file($cfg->{settings}{logger}) || "/bin/logger";
|
|
unless (-x $logger) { fatal_error "Can't find logger."; }
|
|
|
|
1;
|
|
|