2006-04-12 19:31:08 +00:00
# $Id$
2006-04-11 21:52:54 +00:00
#
# ----------------------------------------------------------------------
# Copyright (c) 2006 Novell, Inc. All Rights Reserved.
#
# 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::SubDomain ;
use warnings ;
use strict ;
use Carp ;
use Cwd qw( cwd realpath ) ;
use File::Basename ;
use Data::Dumper ;
use Locale::gettext ;
use POSIX ;
use Immunix::Severity ;
require Exporter ;
our @ ISA = qw( Exporter ) ;
our @ EXPORT = qw( %sd $filename $profiledir $parser %qualifiers %include %helpers $UI_Mode which getprofilefilename getprofileflags setprofileflags complain enforce autodep reload UI_GetString UI_GetFile UI_YesNo UI_Important UI_Info getkey do_logprof_pass readconfig loadincludes check_for_subdomain UI_PromptUser $running_under_genprof GetDataFromYast SendDataToYast setup_yast shutdown_yast readprofile readprofiles writeprofile get_full_path fatal_error ) ;
no warnings 'all' ;
our $ confdir = "/etc/apparmor" ;
our $ running_under_genprof = 0 ;
our $ finishing = 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 , "" ) ;
textdomain ( "apparmor-utils" ) ;
# where do we get our log messages from?
our $ filename ;
if ( - f "/var/log/audit/audit.log" ) {
$ filename = "/var/log/audit/audit.log" ;
} elsif ( - f "/etc/slackware-version" ) {
$ filename = "/var/log/syslog" ;
} else {
$ filename = "/var/log/messages" ;
}
our $ profiledir = "/etc/apparmor.d" ;
# we keep track of the included profile fragments with %include
my % include ;
my % existing_profiles ;
our $ ldd = "/usr/bin/ldd" ;
our $ parser = "/sbin/subdomain_parser" ;
$ parser = "/sbin/apparmor_parser" if - f "/sbin/apparmor_parser" ;
our $ seenevents = 0 ;
# behaviour tweaking
our % qualifiers ;
our % required_hats ;
our % defaulthat ;
our % globmap ;
# 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
my % seen ;
my % profilechanges ;
my % prelog ;
my % log ;
my % changed ;
my % skip ;
our % helpers ; # we want to preserve this one between passes
my % variables ; # variables in config files
### THESE VARIABLES ARE USED WITHIN LOGPROF
sub debug ($) {
my $ message = shift ;
print DEBUG "$message\n" if $ DEBUGGING ;
}
BEGIN {
use POSIX qw( :termios_h ) ;
my ( $ term , $ oterm , $ echo , $ noecho , $ fd_stdin ) ;
$ fd_stdin = fileno ( STDIN ) ;
$ term = POSIX::Termios - > new ( ) ;
$ term - > getattr ( $ fd_stdin ) ;
$ oterm = $ term - > getlflag ( ) ;
$ echo = ECHO | ECHOK | ICANON ;
$ noecho = $ oterm & ~ $ echo ;
sub cbreak {
$ term - > setlflag ( $ noecho ) ;
$ term - > setcc ( VTIME , 1 ) ;
$ term - > setattr ( $ fd_stdin , TCSANOW ) ;
}
sub cooked {
$ term - > setlflag ( $ oterm ) ;
$ term - > setcc ( VTIME , 0 ) ;
$ term - > setattr ( $ fd_stdin , TCSANOW ) ;
}
sub getkey {
my $ key = '' ;
cbreak ( ) ;
sysread ( STDIN , $ key , 1 ) ;
cooked ( ) ;
return $ key ;
}
# set things up to log extra info if they want...
if ( $ ENV { LOGPROF_DEBUG } ) {
$ DEBUGGING = 1 ;
open ( DEBUG , ">/tmp/logprof_debug_$$.log" ) ;
my $ oldfd = select ( DEBUG ) ;
$| = 1 ;
select ( $ oldfd ) ;
} else {
$ DEBUGGING = 0 ;
}
}
END {
# reset the terminal state
cooked ( ) ;
$ DEBUGGING && debug "Exiting..." ;
# close the debug log if necessary
close ( DEBUG ) if $ DEBUGGING ;
}
2006-08-04 16:38:22 +00:00
# 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 ;
}
2006-04-11 21:52:54 +00:00
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 $ 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 Immunix::Ycp ; } ;
unless ( $@ ) {
import Immunix:: Ycp ;
no warnings 'all' ;
$ 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." ;
}
# 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 ;
# escape . characters
$ regexp =~ s/(?<!\\)\./SDPROF_INTERNAL_DOT/g ;
# convert ** globs to match anything
$ regexp =~ s/(?<!\\)\*\*/.SDPROF_INTERNAL_GLOB/g ;
# convert * globs to match anything at current path level
$ regexp =~ s/(?<!\\)\*/[^\/]SDPROF_INTERNAL_GLOB/g ;
# convert ? globs to match a single character at current path level
$ regexp =~ s/(?<!\\)\?/[^\/]/g ;
# convert {foo,baz} to (foo|baz)
$ regexp =~ y /\{\}\,/ \ ( \ ) \ | / if $regexp =~ / \ { . * \ , . * \ } / ;
# twiddle the escaped * chars back
$ regexp =~ s/SDPROF_INTERNAL_GLOB/\*/g ;
# twiddle the escaped . chars back
$ regexp =~ s/SDPROF_INTERNAL_DOT/\\./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 =~ /^\// ) {
$ path = $ link ; # if it's an absolute link, just replace it
} else {
$ path = $ dir . "/$link" ; # if it's relative, let abs_path handle it
}
}
}
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 complain ($) {
my $ bin = shift ;
my $ fqdbin = findexecutable ( $ bin ) or fatal_error ( sprintf ( gettext ( 'Can\'t find %s.' ) , $ bin ) ) ;
# skip directories
return unless - f $ fqdbin ;
UI_Info ( sprintf ( gettext ( 'Setting %s to complain mode.' ) , $ fqdbin ) ) ;
my $ filename = getprofilefilename ( $ fqdbin ) ;
setprofileflags ( $ filename , "complain" ) ;
}
sub enforce ($) {
my $ bin = shift ;
my $ fqdbin = findexecutable ( $ bin ) or fatal_error ( sprintf ( gettext ( 'Can\'t find %s.' ) , $ bin ) ) ;
# skip directories
return unless - f $ fqdbin ;
UI_Info ( sprintf ( gettext ( 'Setting %s to enforce mode.' ) , $ fqdbin ) ) ;
my $ filename = getprofilefilename ( $ fqdbin ) ;
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/ ;
next if $ line =~ /linux-(gate|vdso(32|64)).so/ ; # avoid new kernel 2.6 poo
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 = matchincludes ( $ profile , $ library ) ;
# if we found any matching entries, do the modes match?
next if $ combinedmode ;
$ library = globcommon ( $ library ) ;
chomp $ library ;
next unless $ library ;
if ( $ library =~ /\/lib\/ld-.+/ ) {
2006-08-04 16:38:22 +00:00
$ profile - > { path } - > { $ library } = "mpx" ;
2006-04-11 21:52:54 +00:00
} else {
2006-08-04 16:38:22 +00:00
$ profile - > { path } - > { $ library } = "mr" ;
2006-04-11 21:52:54 +00:00
}
}
return $ profile ;
}
sub autodep ($) {
my $ bin = shift ;
# 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.
my $ 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 ;
my $ profile = { flags = > "complain" ,
include = > { "abstractions/base" = > 1 } ,
2006-08-04 16:38:22 +00:00
path = > { $ fqdbin = > "mr" } } ;
2006-04-11 21:52:54 +00:00
# if the executable exists on this system, pull in extra dependencies
if ( - f $ fqdbin ) {
my $ hashbang = head ( $ fqdbin ) ;
if ( $ hashbang =~ /^#!\s*(\S+)/ ) {
my $ interpreter = get_full_path ( $ 1 ) ;
$ profile - > { path } - > { $ interpreter } = "ix" ;
if ( $ interpreter =~ /perl/ ) {
$ profile - > { include } - > { "abstractions/perl" } = 1 ;
} elsif ( $ interpreter =~ m/\/bin\/(bash|sh)/ ) {
$ profile - > { include } - > { "abstractions/bash" } = 1 ;
}
$ profile = handle_binfmt ( $ profile , $ interpreter ) ;
} else {
$ profile = handle_binfmt ( $ profile , $ fqdbin ) ;
}
}
# stick the profile into our data structure.
$ sd { $ fqdbin } { $ fqdbin } = $ profile ;
# instantiate the required infrastructure hats for this changehat application
for my $ hatglob ( keys % required_hats ) {
if ( $ fqdbin =~ /$hatglob/ ) {
for my $ hat ( split ( /\s+/ , $ required_hats { $ hatglob } ) ) {
$ sd { $ fqdbin } { $ hat } = { flags = > "complain" } ;
}
}
}
if ( - f "$profiledir/tunables/global" ) {
my $ file = getprofilefilename ( $ fqdbin ) ;
unless ( exists $ variables { $ file } ) {
$ variables { $ file } = { } ;
}
$ variables { $ file } { "#tunables/global" } = 1 ; # sorry
}
# write out the profile...
writeprofile ( $ fqdbin ) ;
}
sub getprofilefilename ($) {
my $ profile = shift ;
my $ filename = $ profile ;
$ filename =~ s/\/// ; # strip leading /
$ 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*("??\/.+?"??)\s+(flags=\(.+\)\s+)*\{\s*$/ ) {
my ( $ binary , $ flags ) = ( $ 1 , $ 2 ) ;
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 ;
}
##########################################################################
# 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 {
Immunix::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
$ yes =~ /\((\S)\)/ or fatal_error "PromptUser: Invalid hotkey for '$yes'" ;
my $ yeskey = lc ( $ 1 ) ;
$ no =~ /\((\S)\)/ or fatal_error "PromptUser: Invalid hotkey for '$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 ;
}
2006-08-04 16:38:22 +00:00
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
$ yes =~ /\((\S)\)/ or fatal_error "PromptUser: Invalid hotkey for '$yes'" ;
my $ yeskey = lc ( $ 1 ) ;
$ no =~ /\((\S)\)/ or fatal_error "PromptUser: Invalid hotkey for '$no'" ;
my $ nokey = lc ( $ 1 ) ;
$ cancel =~ /\((\S)\)/ or fatal_error "PromptUser: Invalid hotkey for '$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 ;
}
2006-04-11 21:52:54 +00:00
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 ;
}
my % CMDS = (
CMD_ALLOW = > "(A)llow" ,
CMD_DENY = > "(D)eny" ,
CMD_ABORT = > "Abo(r)t" ,
CMD_FINISHED = > "(F)inish" ,
CMD_INHERIT = > "(I)nherit" ,
CMD_PROFILE = > "(P)rofile" ,
2006-08-04 16:38:22 +00:00
CMD_PROFILE_CLEAN = > "(P)rofile Clean Exec" ,
2006-04-11 21:52:54 +00:00
CMD_UNCONFINED = > "(U)nconfined" ,
2006-08-04 16:38:22 +00:00
CMD_UNCONFINED_CLEAN = > "(U)nconfined Clean Exec" ,
2006-04-11 21:52:54 +00:00
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 SubDomain events" ,
CMD_HELP = > "(H)elp" ,
) ;
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 } ;
}
return ( $ cmd , $ arg ) ;
}
##########################################################################
# 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 ) = Immunix::Ycp:: ParseCommand ( $ _ ) ;
if ( $ ycommand && $ ycommand eq "Read" ) {
if ( $ DEBUGGING ) {
my $ debugmsg = Data::Dumper - > Dump ( [ $ data ] , [ qw( *data ) ] ) ;
debug "SendDataToYast: Sending--\n$debugmsg" ;
}
Immunix::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 ) = Immunix::Ycp:: ParseCommand ( $ _ ) ;
if ( $ DEBUGGING ) {
my $ debugmsg = Data::Dumper - > Dump ( [ $ yarg ] , [ qw( *data ) ] ) ;
debug "GetDataFromYast: Received--\n$debugmsg" ;
}
if ( $ ycmd && $ ycmd eq "Write" ) {
Immunix::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" ;
}
##########################################################################
# this is the hideously ugly function that descends down the flow/event
# trees that we've generated by parsing the logfile
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 ;
}
$ 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 ;
}
# figure out what our default hat for this application is.
my $ defaulthat ;
for my $ hatglob ( keys % defaulthat ) {
$ defaulthat = $ defaulthat { $ hatglob } if $ profile =~ /$hatglob/ ;
}
# keep track of previous answers for this run...
my $ context = $ profile ;
$ context . = " -> ^$uhat" ;
my $ ans = $ transitions { $ context } || "" ;
unless ( $ ans ) {
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" ;
push @ { $ q - > { functions } } , "CMD_ABORT" ;
push @ { $ q - > { functions } } , "CMD_FINISHED" ;
$ q - > { default } = ( $ sdmode eq "PERMITTING" ) ? "CMD_ADDHAT" : "CMD_DENY" ;
$ seenevents + + ;
my $ arg ;
( $ ans , $ arg ) = UI_PromptUser ( $ q ) ;
$ transitions { $ context } = $ ans ;
}
# ugh, there's a bug here. if they pick "abort" or "finish" and then
# say "well, no, I didn't really mean that", we need to ask the
# question again, but we currently go on to the next one. oops.
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 ( $ ans eq "CMD_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 ( ) ;
exit 0 ;
}
} elsif ( $ ans eq "CMD_FINISHED" ) {
my $ ans = UI_YesNo ( gettext ( "Are you sure you want to save the current set of profile changes and exit?" ) , "n" ) ;
if ( $ ans eq "y" ) {
UI_Info ( gettext ( "Saving all changes." ) ) ;
$ finishing = 1 ;
# XXX - BUGBUG - this is REALLY nasty, but i'm in a hurry...
goto SAVE_PROFILES ;
}
}
} 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 ) = @ entry ;
if ( ( $ p !~ /null(-complain)*-profile/ ) &&
( $ h !~ /null(-complain)*-profile/ ) ) {
$ profile = $ p ;
$ hat = $ h ;
}
next unless $ profile && $ hat ;
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 =~ s/x//g ) {
if ( - d $ exec_target ) {
$ mode . = "ix" ;
} else {
$ do_execute = 1 ;
}
}
if ( $ mode eq "link" ) {
$ mode = "l" ;
if ( $ detail =~ m/^from (.+) to (.+)$/ ) {
my ( $ path , $ target ) = ( $ 1 , $ 2 ) ;
my $ frommode = "lr" ;
if ( defined $ prelog { $ sdmode } { $ profile } { $ hat } { path } { $ path } ) {
$ frommode . = $ prelog { $ sdmode } { $ profile } { $ hat } { path } { $ path } ;
}
$ frommode = collapsemode ( $ frommode ) ;
$ prelog { $ sdmode } { $ profile } { $ hat } { path } { $ path } = $ frommode ;
my $ tomode = "lr" ;
if ( defined $ prelog { $ sdmode } { $ profile } { $ hat } { path } { $ target } ) {
$ tomode . = $ prelog { $ sdmode } { $ profile } { $ hat } { path } { $ target } ;
}
$ tomode = collapsemode ( $ tomode ) ;
$ 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 } ;
$ mode = collapsemode ( $ mode ) ;
}
$ prelog { $ sdmode } { $ profile } { $ hat } { path } { $ path } = $ mode ;
# print "$pid $profile $hat $prog $sdmode $mode $path\n";
}
if ( $ do_execute ) {
my $ context = $ profile ;
$ context . = "^$hat" if $ profile ne $ hat ;
$ context . = " -> $exec_target" ;
my $ ans = $ transitions { $ context } || "" ;
my ( $ combinedmode , $ cm , @ m ) ;
# does path match any regexps in original profile?
( $ cm , @ m ) = rematchfrag ( $ sd { $ profile } { $ hat } , $ exec_target ) ;
$ combinedmode . = $ cm if $ cm ;
# does path match anything pulled in by includes in original profile?
( $ cm , @ m ) = matchincludes ( $ sd { $ profile } { $ hat } , $ exec_target ) ;
$ combinedmode . = $ cm if $ cm ;
my $ exec_mode ;
if ( contains ( $ combinedmode , "ix" ) ) {
$ ans = "CMD_INHERIT" ;
$ exec_mode = "ixr" ;
} elsif ( contains ( $ combinedmode , "px" ) ) {
$ ans = "CMD_PROFILE" ;
$ exec_mode = "px" ;
} elsif ( contains ( $ combinedmode , "ux" ) ) {
$ ans = "CMD_UNCONFINED" ;
$ exec_mode = "ux" ;
2006-08-04 16:38:22 +00:00
} elsif ( contains ( $ combinedmode , "Px" ) ) {
$ ans = "CMD_PROFILE_CLEAN" ;
$ exec_mode = "Px" ;
} elsif ( contains ( $ combinedmode , "Ux" ) ) {
$ ans = "CMD_UNCONFINED_CLEAN" ;
$ exec_mode = "Ux" ;
2006-04-11 21:52:54 +00:00
} else {
my $ options = $ qualifiers { $ exec_target } || "ipu" ;
# force "ix" as the only option when the profiled program
# executes itself
$ options = "i" if $ exec_target eq $ 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_PROFILE" ;
} elsif ( $ options =~ /i/ ) {
$ default = "CMD_INHERIT" ;
} else {
$ default = "CMD_DENY" ;
}
2006-08-04 16:38:22 +00:00
# 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 ) ;
2006-04-11 21:52:54 +00:00
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 ;
}
push @ { $ q - > { headers } } , gettext ( "Execute" ) , $ exec_target ;
push @ { $ q - > { headers } } , gettext ( "Severity" ) , $ severity ;
$ q - > { functions } = [ ] ;
my $ prompt = "\n$context\n" ;
push @ { $ q - > { functions } } , "CMD_INHERIT" if $ options =~ /i/ ;
push @ { $ q - > { functions } } , "CMD_PROFILE" if $ options =~ /p/ ;
push @ { $ q - > { functions } } , "CMD_UNCONFINED" if $ options =~ /u/ ;
push @ { $ q - > { functions } } , "CMD_DENY" ;
push @ { $ q - > { functions } } , "CMD_ABORT" ;
push @ { $ q - > { functions } } , "CMD_FINISHED" ;
$ q - > { default } = $ default ;
$ options = join ( "|" , split ( // , $ options ) ) ;
$ seenevents + + ;
my $ arg ;
2006-08-04 16:38:22 +00:00
while ( $ ans !~ m/^CMD_(INHERIT|PROFILE|PROFILE_CLEAN|UNCONFINED|UNCONFINED_CLEAN|DENY)$/ ) {
2006-04-11 21:52:54 +00:00
( $ ans , $ arg ) = UI_PromptUser ( $ q ) ;
# check for Abort or Finish
if ( $ ans eq "CMD_ABORT" ) {
my $ ans = UI_YesNo ( gettext ( "Are you sure you want to abandon this set of profile changes and exit?" ) , "n" ) ;
$ DEBUGGING && debug "back from abort yesno" ;
if ( $ ans eq "y" ) {
UI_Info ( gettext ( "Abandoning all changes." ) ) ;
shutdown_yast ( ) ;
exit 0 ;
}
} elsif ( $ ans eq "CMD_FINISHED" ) {
my $ ans = UI_YesNo ( gettext ( "Are you sure you want to save the current set of profile changes and exit?" ) , "n" ) ;
if ( $ ans eq "y" ) {
UI_Info ( gettext ( "Saving all changes." ) ) ;
$ finishing = 1 ;
# XXX - BUGBUG - this is REALLY nasty, but i'm in a hurry...
goto SAVE_PROFILES ;
}
2006-08-04 16:38:22 +00:00
} elsif ( $ ans eq "CMD_PROFILE" ) {
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 ) ;
if ( $ ynans eq "y" ) {
$ ans = "CMD_PROFILE_CLEAN" ;
}
2006-04-11 21:52:54 +00:00
} elsif ( $ ans eq "CMD_UNCONFINED" ) {
2006-08-04 16:38:22 +00:00
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" ) {
$ ans = "CMD_UNCONFINED_CLEAN" ;
}
} else {
2006-04-11 21:52:54 +00:00
$ ans = "INVALID" ;
}
}
}
$ transitions { $ context } = $ ans ;
# if we're inheriting, things'll bitch unless we have r
if ( $ ans eq "CMD_INHERIT" ) {
$ exec_mode = "ixr" ;
} elsif ( $ ans eq "CMD_PROFILE" ) {
$ exec_mode = "px" ;
} elsif ( $ ans eq "CMD_UNCONFINED" ) {
$ exec_mode = "ux" ;
2006-08-04 16:38:22 +00:00
} elsif ( $ ans eq "CMD_PROFILE_CLEAN" ) {
$ exec_mode = "Px" ;
} elsif ( $ ans eq "CMD_UNCONFINED_CLEAN" ) {
$ exec_mode = "Ux" ;
2006-04-11 21:52:54 +00:00
} else {
# skip all remaining events if they say to deny the exec
return if $ domainchange eq "change" ;
}
unless ( $ ans eq "CMD_DENY" ) {
if ( defined $ prelog { PERMITTING } { $ profile } { $ hat } { path } { $ exec_target } ) {
$ exec_mode . = $ prelog { PERMITTING } { $ profile } { $ hat } { path } { $ exec_target } ;
$ exec_mode = collapsemode ( $ exec_mode ) ;
}
$ prelog { PERMITTING } { $ profile } { $ hat } { path } { $ exec_target } = $ exec_mode ;
$ log { PERMITTING } { $ profile } = { } ;
$ sd { $ profile } { $ hat } { path } { $ exec_target } = $ exec_mode ;
$ changed { $ profile } = 1 ; # mark this profile as changed
if ( $ ans eq "CMD_INHERIT" ) {
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 } = "ix" ;
if ( $ interpreter =~ /perl/ ) {
$ sd { $ profile } { $ hat } { include } { "abstractions/perl" } = 1 ;
} elsif ( $ interpreter =~ m/\/bin\/(bash|sh)/ ) {
$ sd { $ profile } { $ hat } { include } { "abstractions/bash" } = 1 ;
}
}
2006-08-04 16:38:22 +00:00
} elsif ( $ ans =~ /^CMD_PROFILE/ ) {
2006-04-11 21:52:54 +00:00
# if they want to use px, make sure a profile exists for the target.
unless ( - e getprofilefilename ( $ exec_target ) ) {
$ helpers { $ exec_target } = "enforce" ;
autodep ( $ exec_target ) ;
reload ( $ exec_target ) ;
}
}
}
}
#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_INHERIT" ) {
$ profilechanges { $ pid } = $ profile ;
2006-08-04 16:38:22 +00:00
} elsif ( $ ans =~ /^CMD_PROFILE/ ) {
2006-04-11 21:52:54 +00:00
if ( $ sdmode eq "PERMITTING" ) {
if ( $ domainchange eq "change" ) {
$ profile = $ exec_target ;
$ hat = $ exec_target ;
$ profilechanges { $ pid } = $ profile ;
}
}
2006-08-04 16:38:22 +00:00
} elsif ( $ ans =~ /^CMD_UNCONFINED/ ) {
2006-04-11 21:52:54 +00:00
$ profilechanges { $ pid } = "unconstrained" ;
return if $ domainchange eq "change" ;
}
}
}
}
}
}
sub do_logprof_pass {
my $ logmark = shift || "" ;
# zero out the state variables for this pass...
% t = ( ) ;
% transitions = ( ) ;
% seen = ( ) ;
% sd = ( ) ;
% profilechanges = ( ) ;
% prelog = ( ) ;
% log = ( ) ;
% changed = ( ) ;
% skip = ( ) ;
% variables = ( ) ;
UI_Info ( sprintf ( gettext ( 'Reading log entries from %s.' ) , $ filename ) ) ;
UI_Info ( sprintf ( gettext ( 'Updating subdomain profiles in %s.' ) , $ profiledir ) ) ;
readprofiles ( ) ;
my $ seenmark = $ logmark ? 0 : 1 ;
$ sevdb = new Immunix:: Severity ( "$confdir/severity.db" , gettext ( "unknown" ) ) ;
my @ log ;
my % pid ;
sub add_to_tree ($@) {
my ( $ pid , $ type , @ event ) = @ _ ;
unless ( exists $ pid { $ pid } ) {
my $ arrayref = [ ] ;
push @ log , $ arrayref ;
$ pid { $ pid } = $ arrayref ;
}
push @ { $ pid { $ pid } } , [ $ type , $ pid , @ event ] ;
}
my $ stuffed = undef ;
my $ last ;
# okay, done loading the previous profiles, get on to the good stuff...
2006-04-11 22:26:05 +00:00
open ( LOG , $ filename ) or fatal_error "Can't read AppArmor logfile $filename: $!" ;
2006-04-11 21:52:54 +00:00
while ( ( $ _ = $ stuffed ) || ( $ _ = <LOG> ) ) {
chomp ;
$ stuffed = undef ;
$ seenmark = 1 if /$logmark/ ;
next unless $ seenmark ;
# all we care about is subdomain messages
next unless ( /^.* audit\(/ || /type=APPARMOR msg=audit\([\d\.\:]+\):/ || /SubDomain/ ) ;
# workaround for syslog uglyness.
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
next if ( ( $ profile ne 'null-complain-profile' ) && ( ! profile_exists ( $ profile ) ) ) ;
add_to_tree ( $ pid , "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 ) ;
next 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
next if ( ( $ profile ne 'null-complain-profile' ) && ( ! profile_exists ( $ profile ) ) ) ;
add_to_tree ( $ pid , "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 ) ;
my $ domainchange = "nochange" ;
if ( $ mode =~ /x/ ) {
# we need to try to check if we're doing a domain transition this time
if ( $ sdmode eq "PERMITTING" ) {
2006-08-04 16:38:22 +00:00
do {
2006-04-11 21:52:54 +00:00
$ stuffed = <LOG> ;
2006-08-04 16:38:22 +00:00
} until $ stuffed =~ /AppArmor|audit/ ;
2006-04-11 21:52:54 +00:00
if ( $ stuffed =~ m/changing_profile/ ) {
$ domainchange = "change" ;
$ stuffed = undef ;
}
}
} else {
# we want to ignore duplicates for things other than executes...
next 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 ) ) ) {
$ stuffed = undef ;
next ;
}
# currently no way to stick pipe mediation in a profile, ignore any messages like this
next 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
next 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 , "exec" , $ profile , $ hat , $ prog , $ sdmode , $ mode , $ detail ) ;
} else {
add_to_tree ( $ pid , "path" , $ profile , $ hat , $ prog , $ sdmode , $ 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...
next 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
next if ( ( $ profile ne 'null-complain-profile' ) && ( ! profile_exists ( $ profile ) ) ) ;
add_to_tree ( $ pid , "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...
next 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
next 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 , "path" , $ profile , $ hat , $ prog , $ sdmode , $ 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...
next 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
next 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
next if $ path eq "/etc/krb5.conf" ;
add_to_tree ( $ pid , "path" , $ profile , $ hat , $ prog , $ sdmode , "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 ) ;
next 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
next if ( ( $ profile ne 'null-complain-profile' ) && ( ! profile_exists ( $ profile ) ) ) ;
add_to_tree ( $ pid , "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
next 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: $_" ;
}
}
close ( LOG ) ;
for my $ root ( @ log ) {
handlechildren ( undef , undef , $ root ) ;
}
for my $ pid ( sort { $ a <=> $ b } keys % profilechanges ) {
setprocess ( $ pid , $ profilechanges { $ pid } ) ;
}
collapselog ( ) ;
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 } } ) {
$ 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 $ sd { $ profile } { $ hat } { capability } { $ capability } ;
my $ severity = $ sevdb - > rank ( uc ( "cap_$capability" ) ) ;
my $ q = { } ;
$ q - > { headers } = [ ] ;
push @ { $ q - > { headers } } , gettext ( "Profile" ) , combine_name ( $ profile , $ hat ) ;
push @ { $ q - > { headers } } , gettext ( "Capability" ) , $ capability ;
push @ { $ q - > { headers } } , gettext ( "Severity" ) , $ severity ;
$ q - > { functions } = [ "CMD_ALLOW" , "CMD_DENY" , "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 ( $ ans , $ arg ) = UI_PromptUser ( $ q ) ;
if ( $ ans eq "CMD_ALLOW" ) {
# they picked (a)llow, so...
# stick the capability into the profile
$ sd { $ profile } { $ hat } { capability } { $ capability } = 1 ;
# mark this profile as changed
$ changed { $ profile } = 1 ;
# give a little feedback to the user
UI_Info ( sprintf ( gettext ( 'Adding capability %s to profile.' ) , $ capability ) ) ;
} elsif ( $ ans eq "CMD_DENY" ) {
UI_Info ( sprintf ( gettext ( 'Denying capability %s to profile.' ) , $ capability ) ) ;
} elsif ( $ ans eq "CMD_ABORT" ) {
# if we're in yast, they've already been asked for confirmation
if ( $ UI_Mode eq "yast" ) {
UI_Info ( gettext ( "Abandoning all changes." ) ) ;
shutdown_yast ( ) ;
exit 0 ;
}
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 ( ) ;
exit 0 ;
} else {
redo ;
}
} elsif ( $ ans eq "CMD_FINISHED" ) {
# if we're in yast, they've already been asked for confirmation
if ( $ UI_Mode eq "yast" ) {
UI_Info ( gettext ( "Saving all changes." ) ) ;
$ finishing = 1 ;
# XXX - BUGBUG - this is REALLY nasty, but i'm in a hurry...
goto SAVE_PROFILES ;
}
my $ ans = UI_YesNo ( gettext ( "Are you sure you want to save the current set of profile changes and exit?" ) , "n" ) ;
if ( $ ans eq "y" ) {
UI_Info ( gettext ( "Saving all changes." ) ) ;
$ finishing = 1 ;
# XXX - BUGBUG - this is REALLY nasty, but i'm in a hurry...
goto SAVE_PROFILES ;
} 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 } ;
# 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 =~ /X/ ) {
# get rid of the access() markers.
$ mode =~ s/X//g ;
my $ combinedmode = "" ;
my ( $ cm , @ m ) ;
# does path match any regexps in original profile?
( $ cm , @ m ) = rematchfrag ( $ sd { $ profile } { $ hat } , $ path ) ;
$ combinedmode . = $ cm if $ cm ;
# does path match anything pulled in by includes in original profile?
( $ cm , @ m ) = matchincludes ( $ sd { $ profile } { $ hat } , $ path ) ;
$ combinedmode . = $ cm if $ cm ;
if ( $ combinedmode ) {
if ( contains ( $ combinedmode , "ix" ) ||
contains ( $ combinedmode , "px" ) ||
2006-08-04 16:38:22 +00:00
contains ( $ combinedmode , "ux" ) ||
contains ( $ combinedmode , "Px" ) ||
contains ( $ combinedmode , "Ux" ) ) {
2006-04-11 21:52:54 +00:00
} else {
$ mode . = "ix" ;
}
} else {
$ mode . = "ix" ;
}
}
2006-08-04 16:38:22 +00:00
# if we had an mmap(PROT_EXEC) request, first check if we already
# have added an ix rule to the profile
if ( $ mode =~ /m/ ) {
my $ combinedmode = "" ;
my ( $ cm , @ m ) ;
# does path match any regexps in original profile?
( $ cm , @ m ) = rematchfrag ( $ sd { $ profile } { $ hat } , $ path ) ;
$ combinedmode . = $ cm if $ cm ;
# does path match anything pulled in by includes in original profile?
( $ cm , @ m ) = matchincludes ( $ sd { $ profile } { $ hat } , $ path ) ;
$ combinedmode . = $ cm if $ cm ;
# ix implies m. don't ask if they want to add an "m" rule when
# we already have a matching ix rule.
if ( $ combinedmode && contains ( $ combinedmode , "ix" ) ) {
$ mode =~ s/m//g ;
}
}
2006-04-11 21:52:54 +00:00
next unless $ mode ;
my $ combinedmode = "" ;
my @ matches ;
my ( $ cm , @ m ) ;
# does path match any regexps in original profile?
( $ cm , @ m ) = rematchfrag ( $ sd { $ profile } { $ hat } , $ path ) ;
if ( $ cm ) {
$ combinedmode . = $ cm ;
push @ matches , @ m ;
}
# does path match anything pulled in by includes in original profile?
( $ cm , @ m ) = matchincludes ( $ sd { $ profile } { $ hat } , $ path ) ;
if ( $ cm ) {
$ combinedmode . = $ cm ;
push @ matches , @ m ;
}
unless ( $ combinedmode && contains ( $ combinedmode , $ mode ) ) {
my $ defaultoption = 1 ;
my @ options = ( ) ;
# check the path against the available set of include files
my @ newincludes ;
for my $ incname ( keys % include ) {
# don't suggest it if we're already including it, that's dumb
next if $ sd { $ profile } { $ hat } { $ incname } ;
( $ cm , @ m ) = matchinclude ( $ incname , $ path ) ;
if ( $ cm && contains ( $ cm , $ mode ) ) {
unless ( grep { $ _ eq "/**" } @ m ) {
push @ newincludes , $ incname if $ incname =~ /abstractions/ ;
}
}
}
# 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 ) ;
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 ( $ combinedmode ) {
$ combinedmode = collapsemode ( $ combinedmode ) ;
push @ { $ q - > { headers } } , gettext ( "Old Mode" ) , $ combinedmode ;
$ mode = collapsemode ( "$mode$combinedmode" ) ;
push @ { $ q - > { headers } } , gettext ( "New Mode" ) , $ mode ;
} else {
push @ { $ q - > { headers } } , gettext ( "Mode" ) , $ mode ;
}
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" ] ;
$ 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_ALLOW" ) {
$ path = $ selected ;
$ done = 1 ;
if ( $ path =~ m/^#include <(.+)>$/ ) {
my $ inc = $ 1 ;
my $ deleted = 0 ;
for my $ entry ( keys % { $ sd { $ profile } { $ hat } { path } } ) {
next if $ path eq $ entry ;
my $ cm = matchinclude ( $ inc , $ entry ) ;
if ( $ cm && contains ( $ cm , $ sd { $ profile } { $ hat } { path } { $ entry } ) ) {
delete $ sd { $ profile } { $ hat } { path } { $ entry } ;
$ deleted + + ;
}
}
# 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 } { path } { $ path } ) {
$ mode = collapsemode ( $ mode . $ sd { $ profile } { $ hat } { path } { $ path } ) ;
}
my $ deleted = 0 ;
for my $ entry ( keys % { $ sd { $ profile } { $ hat } { path } } ) {
next if $ path eq $ entry ;
if ( matchregexp ( $ path , $ entry ) ) {
# regexp matches, add it's mode to the list to check against
if ( contains ( $ mode , $ sd { $ profile } { $ hat } { path } { $ entry } ) ) {
delete $ sd { $ profile } { $ hat } { path } { $ entry } ;
$ deleted + + ;
}
}
}
# record the new entry
$ sd { $ profile } { $ hat } { path } { $ path } = $ mode ;
$ changed { $ profile } = 1 ;
UI_Info ( sprintf ( gettext ( 'Adding %s %s to profile.' ) , $ path , $ mode ) ) ;
UI_Info ( sprintf ( gettext ( 'Deleted %s previous matching profile entries.' ) , $ deleted ) ) if $ deleted ;
}
} elsif ( $ ans eq "CMD_DENY" ) {
# go on to the next entry without saving this one
$ done = 1 ;
} elsif ( $ ans eq "CMD_NEW" ) {
if ( $ selected !~ /^#include/ ) {
$ ans = UI_GetString ( gettext ( "Enter new path: " ) , $ selected ) ;
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
unless ( $ selected =~ /^#include/ ) {
my $ newpath = $ selected ;
# 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
unless ( $ selected =~ /^#include/ ) {
my $ newpath = $ selected ;
# 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 ;
} elsif ( $ ans eq "CMD_ABORT" ) {
$ 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 ( ) ;
exit 0 ;
}
} elsif ( $ ans eq "CMD_FINISHED" ) {
$ ans = UI_YesNo ( gettext ( "Are you sure you want to save the current set of profile changes and exit?" ) , "n" ) ;
if ( $ ans eq "y" ) {
UI_Info ( gettext ( "Saving all changes." ) ) ;
$ finishing = 1 ;
# XXX - BUGBUG - this is REALLY nasty, but i'm in a hurry...
goto SAVE_PROFILES ;
}
}
}
}
}
}
}
}
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 .
All 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 ( ) ;
}
}
}
SAVE_PROFILES:
# make sure the profile changes we've made are saved to disk...
for my $ profile ( sort keys % changed ) {
writeprofile ( $ profile ) ;
reload ( $ profile ) ;
}
# 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 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> ;
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 = "" ;
# is it in the original profile?
if ( $ sd { $ profile } { $ hat } { path } { $ path } ) {
$ combinedmode . = $ sd { $ profile } { $ hat } { path } { $ path } ;
}
# does path match any regexps in original profile?
$ combinedmode . = rematchfrag ( $ sd { $ profile } { $ hat } , $ path ) ;
# does path match anything pulled in by includes in original profile?
$ combinedmode . = matchincludes ( $ sd { $ profile } { $ hat } , $ path ) ;
# if we found any matching entries, do the modes match?
unless ( $ combinedmode && contains ( $ combinedmode , $ mode ) ) {
# merge in any previous modes from this run
if ( $ log { $ sdmode } { $ profile } { $ hat } { path } { $ path } ) {
$ mode = collapsemode ( $ mode . $ log { $ sdmode } { $ profile } { $ hat } { path } { $ path } ) ;
}
# record the new entry
$ log { $ sdmode } { $ profile } { $ hat } { path } { $ path } = collapsemode ( $ 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 } { capability } { $ capability } ) {
$ log { $ sdmode } { $ profile } { $ hat } { capability } { $ capability } = 1 ;
}
}
}
}
}
}
sub profilemode ($) {
my $ mode = shift ;
2006-08-04 16:38:22 +00:00
my $ modifier = ( $ mode =~ m/[iupUP]/ ) [ 0 ] ;
2006-04-11 21:52:54 +00:00
if ( $ modifier ) {
2006-08-04 16:38:22 +00:00
$ mode =~ s/[iupUPx]//g ;
2006-04-11 21:52:54 +00:00
$ 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 ;
}
sub collapsemode ($) {
my $ old = shift ;
my % seen ;
my $ new = join "" ,
sort
grep { ! $ seen { $ _ } + + }
2006-08-04 16:38:22 +00:00
$ old =~ m/\G(r|w|l|m|ix|px|ux|Px|Ux)/g ;
2006-04-11 21:52:54 +00:00
return $ new ;
}
sub contains ($$) {
my ( $ glob , $ single ) = @ _ ;
$ glob = "" unless defined $ glob ;
my % h ;
2006-08-04 16:38:22 +00:00
$ h { $ _ } + + for ( $ glob =~ m/\G(r|w|l|m|ix|px|ux|Px|Ux)/g ) ;
2006-04-11 21:52:54 +00:00
2006-08-04 16:38:22 +00:00
for my $ mode ( $ single =~ m/\G(r|w|l|m|ix|px|ux|Px|Ux)/g ) {
2006-04-11 21:52:54 +00:00
return 0 unless $ h { $ mode } ;
}
return 1 ;
}
sub readprofiles () {
2006-04-11 22:26:05 +00:00
opendir ( SDDIR , $ profiledir ) or fatal_error "Can't read AppArmor profiles in $profiledir." ;
2006-04-11 21:52:54 +00:00
for my $ file ( grep { - f "$profiledir/$_" } readdir ( SDDIR ) ) {
next if $ file =~ /\.rpm(save|new)$/ ;
readprofile ( "$profiledir/$file" ) ;
}
closedir ( SDDIR ) ;
}
sub readprofile ($) {
my $ file = shift ;
if ( open ( SDPROF , "$file" ) ) {
my ( $ profile , $ hat , $ in_contained_hat ) ;
my $ initial_comment = "" ;
while ( <SDPROF> ) {
chomp ;
# we don't care about blank lines
next if /^\s*$/ ;
# start of a profile...
if ( m/^\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 ) {
fatal_error "$profile profile in $file contains syntax errors." ;
}
# we hit the start of a profile, keep track of it...
$ profile = $ 1 ;
my $ flags = $ 2 ;
$ in_contained_hat = 0 ;
# hat is same as profile name if we're not in a hat
( $ profile , $ hat ) = split /\^/ , $ profile ;
# deal with whitespace in profile and hat names.
$ profile = $ 1 if $ profile =~ /^"(.+)"$/ ;
$ hat = $ 1 if $ hat =~ /^"(.+)"$/ ;
# if we run into old-style hat declarations mark the profile as
# changed so we'll write it out as new-style
if ( $ hat && $ hat ne $ profile ) {
$ changed { $ profile } = 1 ;
}
$ hat || = $ profile ;
# keep track of profile flags
if ( $ flags && $ flags =~ /^flags=\((.+)\)\s*$/ ) {
$ flags = $ 1 ;
$ sd { $ profile } { $ hat } { flags } = $ flags ;
}
$ sd { $ profile } { $ hat } { netdomain } = [] ;
# store off initial comment if they have one
$ sd { $ profile } { $ hat } { initial_comment } = $ initial_comment if $ initial_comment ;
$ initial_comment = "" ;
} 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 ) {
fatal_error ( sprintf ( gettext ( '%s contains syntax errors.' ) , $ file ) ) ;
}
if ( $ in_contained_hat ) {
$ hat = $ profile ;
$ in_contained_hat = 0 ;
} else {
# if we're finishing a profile, make sure that any required
# infrastructure hats for this changehat application exist
for my $ hatglob ( keys % required_hats ) {
if ( $ profile =~ /$hatglob/ ) {
for my $ hat ( split ( /\s+/ , $ required_hats { $ hatglob } ) ) {
unless ( $ sd { $ profile } { $ hat } ) {
$ sd { $ profile } { $ hat } = { } ;
# if we had to auto-instantiate a hat, we want to write out
# an updated version of the profile
$ changed { $ profile } = 1 ;
}
}
}
}
# mark that we're outside of a profile now...
$ profile = undef ;
$ initial_comment = "" ;
}
} elsif ( m/^\s*capability\s+(\S+)\s*,\s*$/ ) { # capability entry
if ( not $ profile ) {
fatal_error ( sprintf ( gettext ( '%s contains syntax errors.' ) , $ file ) ) ;
}
my $ capability = $ 1 ;
$ sd { $ profile } { $ hat } { capability } { $ capability } = 1 ;
} elsif ( /^\s*(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*=\s*(true|false)\s*$/i ) { # boolean definition
} elsif ( /^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\+=\s*(.+)\s*$/ ) { # variable additions
} elsif ( /^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*=\s*(.+)\s*$/ ) { # variable definitions
} 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*([\"\@\/].*)\s+(\S+)\s*,\s*$/ ) { # path entry
if ( not $ profile ) {
fatal_error ( sprintf ( gettext ( '%s contains syntax errors.' ) , $ file ) ) ;
}
my ( $ path , $ mode ) = ( $ 1 , $ 2 ) ;
# strip off any trailing spaces.
$ path =~ s/\s+$// ;
$ path = $ 1 if $ path =~ /^"(.+)"$/ ;
# make sure they don't have broken regexps in the profile
my $ p_re = convert_regexp ( $ path ) ;
eval { "foo" =~ m/^$p_re$/ ; } ;
if ( $@ ) {
fatal_error sprintf ( gettext ( 'Profile %s contains invalid regexp %s.' ) , $ file , $ path ) ;
}
$ sd { $ profile } { $ hat } { path } { $ path } = $ mode ;
} elsif ( m/^\s*#include <(.+)>\s*$/ ) { # include stuff
my $ include = $ 1 ;
if ( $ profile ) {
$ sd { $ profile } { $ hat } { include } { $ include } = 1 ;
} else {
unless ( exists $ variables { $ file } ) {
$ variables { $ file } = { } ;
}
$ variables { $ file } { "#" . $ include } = 1 ; # sorry
}
loadinclude ( $ include ) ;
} elsif ( /^\s*(tcp_connect|tcp_accept|udp_send|udp_receive)/ ) {
if ( not $ profile ) {
fatal_error ( sprintf ( gettext ( '%s contains syntax errors.' ) , $ file ) ) ;
}
# XXX - BUGBUGBUG - don't strip netdomain entries
unless ( $ sd { $ profile } { $ hat } { netdomain } ) {
$ sd { $ profile } { $ hat } { netdomain } = [ ] ;
}
# strip leading spaces and trailing comma
s/^\s+// ;
s/,\s*$// ;
# keep track of netdomain entries...
push @ { $ sd { $ profile } { $ hat } { netdomain } } , $ _ ;
} elsif ( m/^\s*\^(\"?.+?)\s+(flags=\(.+\)\s+)*\{\s*$/ ) { # start of a hat
# if we hit the start of a contained hat when we're not in a profile
# something is wrong...
if ( not $ profile ) {
fatal_error ( sprintf ( gettext ( '%s contains syntax errors.' ) , $ file ) ) ;
}
$ in_contained_hat = 1 ;
# we hit the start of a hat inside the current profile
$ hat = $ 1 ;
my $ flags = $ 2 ;
# deal with whitespace in hat names.
$ hat = $ 1 if $ hat =~ /^"(.+)"$/ ;
# keep track of profile flags
if ( $ flags && $ flags =~ /^flags=\((.+)\)\s*$/ ) {
$ flags = $ 1 ;
$ sd { $ profile } { $ hat } { flags } = $ flags ;
}
$ sd { $ profile } { $ hat } { path } = { } ;
$ sd { $ profile } { $ hat } { netdomain } = [] ;
# store off initial comment if they have one
$ sd { $ profile } { $ hat } { initial_comment } = $ initial_comment if $ initial_comment ;
$ initial_comment = "" ;
} 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:/ ;
$ initial_comment . = "$_\n" ;
}
} else {
# we hit something we don't understand in a profile...
fatal_error ( sprintf ( gettext ( '%s contains syntax errors.' ) , $ file ) ) ;
}
}
# if we're still in a profile when we hit the end of the file, it's bad
if ( $ profile ) {
fatal_error "Reached the end of $file while we were still inside the $profile profile." ;
}
close ( SDPROF ) ;
} else {
$ DEBUGGING && debug "readprofile: can't read $file - skipping" ;
}
}
sub escape ($) {
my $ dangerous = shift ;
if ( $ dangerous =~ m/^"(.+)"$/ ) {
$ dangerous = $ 1 ;
}
$ dangerous =~ s/((?<!\\))"/$1\\"/g ;
if ( $ dangerous =~ m/(\s|^$|")/ ) {
$ dangerous = "\"$dangerous\"" ;
}
return $ dangerous ;
}
sub writeheader ($$$$) {
my ( $ fh , $ profile , $ hat , $ indent ) = @ _ ;
# deal with whitespace in profile names...
my $ p = $ profile ;
$ p = "\"$p\"" if $ p =~ /\s/ ;
if ( $ sd { $ profile } { $ hat } { flags } ) {
print $ fh "$p flags=($sd{$profile}{$hat}{flags}) {\n" ;
} else {
print $ fh "$p {\n" ;
}
}
sub writeincludes ($$$$) {
my ( $ fh , $ profile , $ hat , $ indent ) = @ _ ;
# dump out the includes
if ( exists $ sd { $ profile } { $ hat } { include } ) {
for my $ include ( sort keys % { $ sd { $ profile } { $ hat } { include } } ) {
print $ fh "$indent #include <$include>\n" ;
}
print $ fh "\n" if keys % { $ sd { $ profile } { $ hat } { include } } ;
}
}
sub writecapabilities ($$$$) {
my ( $ fh , $ profile , $ hat , $ indent ) = @ _ ;
# dump out the capability entries...
if ( exists $ sd { $ profile } { $ hat } { capability } ) {
for my $ capability ( sort keys % { $ sd { $ profile } { $ hat } { capability } } ) {
print $ fh "$indent capability $capability,\n" ;
}
print $ fh "\n" if keys % { $ sd { $ profile } { $ hat } { capability } } ;
}
}
sub writenetdomain ($$$$) {
my ( $ fh , $ profile , $ hat , $ indent ) = @ _ ;
# dump out the netdomain entries...
if ( exists $ sd { $ profile } { $ hat } { netdomain } ) {
for my $ nd ( sort @ { $ sd { $ profile } { $ hat } { netdomain } } ) {
print $ fh "$indent $nd,\n" ;
}
print $ fh "\n" if @ { $ sd { $ profile } { $ hat } { netdomain } } ;
}
}
sub writepaths ($$$$) {
my ( $ fh , $ profile , $ hat , $ indent ) = @ _ ;
if ( exists $ sd { $ profile } { $ hat } { path } ) {
for my $ path ( sort keys % { $ sd { $ profile } { $ hat } { path } } ) {
my $ mode = $ sd { $ profile } { $ hat } { path } { $ path } ;
# strip out any fake access() modes that might have slipped through
$ mode =~ s/X//g ;
# deal with whitespace in path names
if ( $ path =~ /\s/ ) {
print $ fh "$indent \"$path\" $mode,\n" ;
} else {
print $ fh "$indent $path $mode,\n" ;
}
}
}
}
sub writepiece ($$) {
my ( $ sdprof , $ profile ) = @ _ ;
writeheader ( $ sdprof , $ profile , $ profile , "" ) ;
writeincludes ( $ sdprof , $ profile , $ profile , "" ) ;
writecapabilities ( $ sdprof , $ profile , $ profile , "" ) ;
writenetdomain ( $ sdprof , $ profile , $ profile , "" ) ;
writepaths ( $ sdprof , $ profile , $ profile , "" ) ;
for my $ hat ( grep { $ _ ne $ profile } sort keys % { $ sd { $ profile } } ) {
# deal with whitespace in profile names...
my $ h = $ hat ;
$ h = "\"$h\"" if $ h =~ /\s/ ;
if ( $ sd { $ profile } { $ hat } { flags } ) {
print $ sdprof "\n ^$h flags=($sd{$profile}{$hat}{flags}) {\n" ;
} else {
print $ sdprof "\n ^$h {\n" ;
}
writeincludes ( $ sdprof , $ profile , $ hat , " " ) ;
writecapabilities ( $ sdprof , $ profile , $ hat , " " ) ;
writenetdomain ( $ sdprof , $ profile , $ hat , " " ) ;
writepaths ( $ sdprof , $ profile , $ hat , " " ) ;
print $ sdprof " }\n" ;
}
print $ sdprof "}\n" ;
}
sub writeprofile ($) {
my $ profile = shift ;
UI_Info ( sprintf ( gettext ( 'Writing updated profile for %s.' ) , $ profile ) ) ;
my $ filename = getprofilefilename ( $ profile ) ;
2006-04-11 22:26:05 +00:00
open ( SDPROF , ">$filename" ) or fatal_error "Can't write new AppArmor profile $filename: $!" ;
2006-04-11 21:52:54 +00:00
# stick in a vim mode line to turn on subdomain syntax highlighting
print SDPROF "# vim:syntax=apparmor\n" ;
# keep track of when the file was last updated
print SDPROF "# Last Modified: " . localtime ( time ) . "\n" ;
# print out initial comment
if ( $ sd { $ profile } { $ profile } { initial_comment } ) {
$ sd { $ profile } { $ profile } { initial_comment } =~ s/\\n/\n/g ;
print SDPROF $ sd { $ profile } { $ profile } { initial_comment } ;
print SDPROF "\n" ;
}
# 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" ;
}
}
print SDPROF "\n" ;
writepiece ( \ * SDPROF , $ profile ) ;
close ( SDPROF ) ;
}
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 ) ;
$ flags =~ s/flags=\((.+)\)/$1/ ;
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 ;
}
sub reload ($) {
my $ bin = shift ;
# don't try to reload profile if subdomain is not running
return unless check_for_subdomain ( ) ;
# don't reload the profile if the corresponding executable doesn't exist
my $ fqdbin = findexecutable ( $ bin ) or return ;
my $ filename = getprofilefilename ( $ fqdbin ) ;
system ( "/bin/cat '$filename' | $parser -I$profiledir -r >/dev/null 2>&1" ) ;
}
sub loadinclude {
my $ which = shift ;
# don't bother loading it again if we already have
return if $ include { $ which } ;
my @ loadincludes = ( $ which ) ;
while ( my $ incfile = shift @ loadincludes ) {
# load the include from the directory we found earlier...
open ( INCLUDE , "$profiledir/$incfile" ) or fatal_error "Can't find include file $incfile: $!" ;
while ( <INCLUDE> ) {
chomp ;
if ( /^\s*(\$\{?[[:alpha:]][[:alnum:]_]*\}?)\s*=\s*(true|false)\s*$/i ) { # boolean definition
} elsif ( /^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*\+=\s*(.+)\s*$/ ) { # variable additions
} elsif ( /^\s*(@\{?[[:alpha:]][[:alnum:]_]+\}?)\s*=\s*(.+)\s*$/ ) { # variable definitions
} 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*\}\s*$/ ) { # end of a profile or conditional
} elsif ( m/^\s*([\"\@\/].*)\s+(\S+)\s*,\s*$/ ) { # path entry
my ( $ path , $ mode ) = ( $ 1 , $ 2 ) ;
# strip off any trailing spaces.
$ path =~ s/\s+$// ;
$ path = $ 1 if $ path =~ /^"(.+)"$/ ;
# make sure they don't have broken regexps in the profile
my $ p_re = convert_regexp ( $ path ) ;
eval { "foo" =~ m/^$p_re$/ ; } ;
if ( $@ ) {
fatal_error sprintf ( gettext ( 'Include %s contains invalid regexp %s.' ) , $ incfile , $ path ) ;
}
$ include { $ incfile } { path } { $ path } = $ mode ;
} elsif ( /^\s*capability\s+(.+)\s*,\s*$/ ) {
my $ capability = $ 1 ;
$ include { $ incfile } { capability } { $ capability } = 1 ;
} elsif ( /^\s*#include <(.+)>\s*$/ ) { # include stuff
my $ newinclude = $ 1 ;
push @ loadincludes , $ newinclude unless $ include { $ newinclude } ;
$ include { $ incfile } { include } { $ newinclude } = 1 ;
} elsif ( /^\s*(tcp_connect|tcp_accept|udp_send|udp_receive)/ ) {
} else {
# we don't care about blank lines or comments
next if /^\s*$/ ;
next if /^\s*\#/ ;
# we hit something we don't understand in a profile...
fatal_error sprintf ( gettext ( '%s contains syntax errors.' ) , $ incfile ) ;
}
}
close ( INCLUDE ) ;
}
}
sub rematchfrag {
my ( $ frag , $ path ) = @ _ ;
my $ combinedmode = "" ;
my @ matches ;
for my $ entry ( keys % { $ frag - > { 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 - > { path } { $ entry } ;
push @ matches , $ entry ;
}
}
return wantarray ? ( $ combinedmode , @ matches ) : $ combinedmode ;
}
sub matchincludes {
my ( $ frag , $ path ) = @ _ ;
my $ combinedmode = "" ;
my @ matches ;
# scan the include fragments for this profile looking for matches
my @ includelist = keys % { $ frag - > { include } } ;
while ( my $ include = shift @ includelist ) {
loadinclude ( $ include ) ;
my ( $ cm , @ m ) = rematchfrag ( $ include { $ include } , $ path ) ;
if ( $ cm ) {
$ combinedmode . = $ cm ;
push @ matches , @ m ;
}
# check if a literal version is in the current include fragment
if ( $ include { $ include } { path } { $ path } ) {
$ combinedmode . = $ include { $ include } { path } { $ path } ;
}
# if this fragment includes others, check them too
if ( keys % { $ include { $ include } { include } } ) {
push @ includelist , keys % { $ include { $ include } { include } } ;
}
}
return wantarray ? ( $ combinedmode , @ matches ) : $ combinedmode ;
}
sub matchinclude {
my ( $ incname , $ path ) = @ _ ;
my $ combinedmode = "" ;
my @ matches ;
# scan the include fragments for this profile looking for matches
my @ includelist = ( $ incname ) ;
while ( my $ include = shift @ includelist ) {
my ( $ cm , @ m ) = rematchfrag ( $ include { $ include } , $ path ) ;
if ( $ cm ) {
$ combinedmode . = $ cm ;
push @ matches , @ m ;
}
# check if a literal version is in the current include fragment
if ( $ include { $ include } { path } { $ path } ) {
$ combinedmode . = $ include { $ include } { path } { $ path } ;
}
# if this fragment includes others, check them too
if ( keys % { $ include { $ include } { include } } ) {
push @ includelist , keys % { $ include { $ include } { include } } ;
}
}
if ( $ combinedmode ) {
return wantarray ? ( $ combinedmode , @ matches ) : $ combinedmode ;
} else {
return ;
}
}
sub readconfig () {
my $ which ;
if ( open ( LPCONF , "$confdir/logprof.conf" ) ) {
while ( <LPCONF> ) {
chomp ;
next if /^\s*#/ ;
if ( m/^\[(\S+)\]/ ) {
$ which = $ 1 ;
} elsif ( m/^\s*(\S+)\s*=\s*(.+)\s*$/ ) {
my ( $ key , $ value ) = ( $ 1 , $ 2 ) ;
if ( $ which eq "defaulthat" ) {
$ defaulthat { $ key } = $ value ;
} elsif ( $ which eq "qualifiers" ) {
$ qualifiers { $ key } = $ value ;
} elsif ( $ which eq "globs" ) {
$ globmap { $ key } = $ value ;
} elsif ( $ which eq "required_hats" ) {
$ required_hats { $ key } = $ value ;
}
}
}
close ( LPCONF ) ;
}
}
sub loadincludes {
if ( opendir ( SDDIR , $ profiledir ) ) {
my @ incdirs = grep { ( ! /^\./ ) && ( - d "$profiledir/$_" ) } readdir ( SDDIR ) ;
close ( SDDIR ) ;
while ( my $ id = shift @ incdirs ) {
if ( opendir ( SDDIR , "$profiledir/$id" ) ) {
for my $ path ( grep { ! /^\./ } readdir ( SDDIR ) ) {
chomp ( $ path ) ;
next if $ path =~ /\.rpm(save|new)$/ ;
if ( - f "$profiledir/$id/$path" ) {
my $ file = "$id/$path" ;
$ file =~ s/$profiledir\/// ;
loadinclude ( $ file ) ;
} 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 % globmap ) {
if ( $ path =~ /$glob/ ) {
my $ globbedpath = $ path ;
$ globbedpath =~ s/$glob/$globmap{$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 @ headers = ( @ { $ question - > { headers } } ) ;
my @ functions = ( @ { $ question - > { functions } } ) ;
my $ default = $ question - > { default } ;
my $ options = $ question - > { options } ;
my $ selected = $ question - > { selected } ;
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
fatal_error "PromptUser: Unknown command $cmd" 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
$ menutext =~ /\((\S)\)/ or fatal_error "PromptUser: Invalid hotkey in '$menutext'" ;
# 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
fatal_error "PromptUser: Duplicate hotkey for $cmd: $menutext" 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
$ defaulttext =~ /\((\S)\)/ or fatal_error "PromptUser: Invalid hotkey in default item '$defaulttext'" ;
# we want case insensitive comparisons so we'll force things to lowercase
$ default_key = lc ( $ 1 ) ;
fatal_error "PromptUser: Invalid default $default" 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" ;
my @ poo = @ headers ;
while ( my $ header = shift @ poo ) {
my $ value = shift @ poo ;
$ prompt . = sprintf ( $ format , "$header:" , $ value ) ;
}
$ prompt . = "\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 && $ keys { $ ans } && $ keys { $ ans } eq "CMD_HELP" ) {
print "\n$helptext\n" ;
$ ans = undef ;
}
# pick the default if they hit return...
$ ans = $ default_key if ord ( $ ans ) == 10 ;
# ugly code to handle escape sequences so you can up/down in the list
if ( ord ( $ ans ) == 27 ) {
$ ans = getkey ;
if ( ord ( $ ans ) == 91 ) {
$ ans = getkey ;
if ( ord ( $ ans ) == 65 ) {
if ( $ options ) {
if ( $ selected > 0 ) {
$ ans = $ selected ;
} else {
$ ans = "again" ;
}
} else {
$ ans = "again" ;
}
} elsif ( ord ( $ ans ) == 66 ) {
if ( $ options ) {
if ( $ selected <= scalar ( @$ options ) ) {
$ ans = $ selected + 2 ;
} else {
$ ans = "again" ;
}
}
} else {
$ ans = "again" ;
}
} else {
$ ans = "again" ;
}
}
# handle option poo
if ( $ options && ( $ ans =~ /^\d$/ ) ) {
if ( $ ans > 0 && $ ans <= scalar ( @$ options ) ) {
$ selected = $ ans - 1 ;
}
$ ans = undef ;
}
}
# pull our command back from our hotkey map
$ ans = $ keys { $ ans } if $ keys { $ ans } ;
# if($options) {
# die "ERROR: not looking for array when options passed" unless wantarray;
if ( $ options ) {
return ( $ ans , $ options - > [ $ selected ] ) ;
} else {
return ( $ ans , $ selected ) ;
}
# } else {
# die "ERROR: looking for list when options not passed" if wantarray;
# return $ans;
# }
}
unless ( - x $ ldd ) {
$ ldd = which ( "ldd" ) or fatal_error "Can't find ldd." ;
}
unless ( - x $ parser ) {
$ parser = which ( "apparmor_parser" ) || which ( "subdomain_parser" )
or fatal_error "Can't find apparmor_parser." ;
}
1 ;