#
# Copyright 2009 Sun Microsystems, Inc. All rights reserved.
# Use is subject to license terms.
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
# WARNING -- this package implements a Sun private interface; it may
# change without notice.
package Sun::Solaris::BSM::_BSMparse;
require 5.005;
use strict;
use Exporter;
use Sun::Solaris::Utils qw(gettext);
use vars qw($VERSION $failedOpen
%EXPORT_TAGS @ISA @EXPORT_OK @EXPORT_FAIL);
$VERSION = '1.01';
@ISA = qw(Exporter);
my @constants = qw();
@EXPORT_OK = qw(readAttr readEvent readClass filterLabel filterCallName
readControl getPathList readUser ckAttrEvent);
@EXPORT_FAIL = qw($failedOpen);
%EXPORT_TAGS = (ALL => \@EXPORT_OK);
$failedOpen = gettext("failed to open %s: %s");
sub new {
my $obj = shift;
my $debug = shift; # bool
my $filters = shift; # options for filtering
my $dir = '/etc/security';
my $attrDir = '/usr/lib/audit';
my $configDir = $dir;
$attrDir = shift if (@_); # override for test
$configDir = shift if (@_); # ditto
my $suffix = '';
$suffix = shift if (@_); # test, again
$obj = ref($obj) || $obj;
my ($recordf, $classf, $controlf, $eventf, $userf) =
("$attrDir/audit_record_attr$suffix",
"$configDir/audit_class$suffix",
"$configDir/audit_control$suffix",
"$configDir/audit_event$suffix",
"$configDir/audit_user$suffix");
return (bless {
'attrFile' => $recordf,
'classFile' => $classf,
'classFilter' => $filters->{'classFilter'},
'controlFile' => $controlf,
'debug' => $debug,
'eventFile' => $eventf,
'eventFilter' => $filters->{'eventFilter'},
'idFilter' => $filters->{'idFilter'},
'havePath' => 0,
'kernelDefault' => '',
'userDefault' => '',
'userFile' => $userf}, $obj);
}
# readAttr
# read the hand edited attrFile file
#
# return a hash reference
sub readAttr {
my $obj = shift;
my $file = $obj->{'attrFile'};
my $fileHandle = do {local *FileHandle; *FileHandle};
open($fileHandle, $file) or die sprintf("$failedOpen\n", $file, $!);
my $count = 0;
my $lastAttr = '';
my $lastMacro = '';
my $attrState = -1;
my $caseState = 0;
my $label;
my $callName = '';
my $skip = '';
my $description = 'none';
my $format = 'none';
my $comment = '';
my $title = 'none';
my $note = '';
my $case = '';
my @case = ();
my %skipClass;
my %attr = ();
my %token = ();
my $classFilter = $obj->{'classFilter'};
$classFilter = '' unless (defined ($classFilter));
my %noteAlias = ();
while (<$fileHandle>) {
chomp;
s/#.*//; # remove comment
next if (/^\s*$/);
if ($attrState < 0) { # initial state: header info
# continue assigning lines to multiline macros
# type: message
if ( $lastMacro ne '' ) {
my ($mcr, $attr) = split(/\s*:\s*/, $lastMacro);
if ($mcr eq "message") {
chomp($noteAlias{$attr});
chop($noteAlias{$attr});
$_ =~ /^\s*(.*)/i;
$noteAlias{$attr} .= $1;
$lastMacro = chkBslash($lastMacro, \$1);
}
next;
}
$lastMacro = '';
if (/^\s*skipClass\s*=\s*(.*)/i) {
my $class = $1;
# don't skip what you're searching for
next if (index(lc($classFilter),lc($class)) > -1);
$skipClass{$1} = 1;
next;
}
elsif (/^\s*token\s*=\s*(.*)/i) {
my ($attr, $value) = split(/\s*:\s*/, $1);
$token{$attr} = $value;
next;
}
elsif (/^\s*message\s*=\s*(.*)/i) {
my ($attr, $value) = split(/\s*:\s*/, $1);
$noteAlias{$attr} = $value;
$lastMacro = chkBslash("message:$attr", \$1);
next;
}
elsif (/^\s*kernel\s*=\s*(.*)/i) {
my ($attr, $value) = split(/\s*:\s*/, $1);
$obj->{'kernelDefault'} = $1;
next;
}
elsif (/^\s*user\s*=\s*(.*)/i) {
my ($attr, $value) = split(/\s*:\s*/, $1);
$obj->{'userDefault'} = $1;
next;
}
}
# continue assigning lines to multiline attributes
# type: case, comment, note, format
if ( $lastAttr ne '' ) {
my $curAttrVal = '';
eval "\$curAttrVal = \$$lastAttr";
chomp($curAttrVal);
chop($curAttrVal);
$_ =~ /^\s*(.*)/i;
$curAttrVal .= $1;
eval "\$$lastAttr = \$curAttrVal";
$lastAttr = chkBslash($lastAttr, \$1);
next;
}
$lastAttr = '';
if (/^\s*label\s*=\s*(.*)/i) {
$attrState = 0 if ($attrState < 0);
my $newLabel = $1;
if ($obj->{'debug'}) {
print STDERR qq{
$newLabel is duplicated in the attribute file (line $.)
} if ($attr{$newLabel});
}
# if $attrState not zero, an unwritten record exists
if ($attrState) {
$callName = $obj->filterCallName($label,
$callName);
push(@case, [$case, $format, $comment, $note]);
if ($obj->filterLabel($label)) {
$attr{$label} =
[$callName, $description, $title,
$skip, @case];
$count++;
}
$format = $description = $title = 'none';
$case = $note = $comment = $skip = $callName
= '';
@case = ();
$caseState = 0;
}
$label = $newLabel;
$attrState = 1;
}
elsif (/^\s*skip\s*=\s*(.*)/i) {
$skip = $1;
}
elsif (/^\s*syscall\s*=\s*(.*)/i) {
$callName = $1;
}
elsif (/^\s*program\s*=\s*(.*)/i) {
$callName = $1;
}
elsif (/^\s*title\s*=\s*(.*)/i) {
$title = $1;
}
elsif (/^\s*see\s*=\s*(.*)/i) {
$description = $1;
}
elsif (/^\s*format\s*=\s*(.*)/i) {
$format = $1;
$lastAttr = chkBslash("format", \$1);
}
elsif (/^\s*comment\s*=\s*(.*)/i) {
$comment .= $1;
$lastAttr = chkBslash("comment", \$1);
}
elsif (/^\s*note\s*=\s*(.*)/i) {
$note .= $1;
$lastAttr = chkBslash("note", \$1);
}
elsif (/^\s*case\s*=\s*(.*)/i) {
if ($caseState) {
push(@case, [$case, $format, $comment, $note]);
$format = 'none';
$comment = $note = '';
}
$case = $1;
$lastAttr = chkBslash("case", \$1);
$caseState = 1;
}
}
if ($attrState) {
$callName = $obj->filterCallName($label, $callName);
push(@case, [$case, $format, $comment, $note]);
if ($obj->filterLabel($label)) {
$attr{$label} = [$callName, $description, $title, $skip,
@case];
$count++;
}
}
close $fileHandle;
print STDERR "found $count audit attribute entries\n" if ($obj->{'debug'});
return ($obj->{'attr'} = \%attr, \%token, \%skipClass, \%noteAlias);
}
# readEvent
# read eventFile and extract audit event information, including
# which classes are associated with each event and what call is
# related.
sub readEvent {
my $obj = shift;
my %event = ();
my $file = $obj->{'eventFile'};
my $fileHandle = do {local *FileHandle; *FileHandle};
open($fileHandle, $file) or die sprintf("$failedOpen\n", $file, $!);
my $count = 0;
unless (defined $obj->{'class'} && (scalar keys %{$obj->{'class'}} > 1)) {
$obj->readClass();
}
my @classFilterMasks = ();
my $classFilter = $obj->{'classFilter'};
if ($classFilter) {
foreach (split(',', $classFilter)) {
push @classFilterMasks, $obj->{'class'}{$_};
}
}
# ignore customer-supplied audit events (id > 32767)
while (<$fileHandle>) {
chomp;
s/#.*//; # remove comment
next if (/^\s*$/);
if (/^\s*(\d+):(\w+):([^:]+):(.*)/) {
my $id = $1;
my $label = $2;
my $description = $3;
my $class = $4;
if ($id !~ /\d+/) {
print STDERR "$id is not numeric (line $.)\n";
next;
}
next if ($id > 32767);
$class =~ s/\s*$//;
if ($obj->{'debug'}) {
print STDERR qq{
$label is duplicated in the event file (line $.)
} if ($event{$label});
}
next unless ($obj->filterLabel($label));
my $mask = 0;
if ($classFilter) {
foreach (split(/\s*,\s*/, $class)) {
$mask |= $obj->{'class'}{$_};
}
my $skip = 0;
foreach my $filterMask (@classFilterMasks) {
unless ($mask & $filterMask) {
$skip = 1;
last;
}
}
next if $skip;
}
if ($obj->{'idFilter'}) {
next unless ($obj->{'idFilter'} == $id);
}
$event{$label} = [$id, $class, $description];
$count++;
}
}
close $fileHandle;
print STDERR "found $count audit events\n" if ($obj->{'debug'});
return ($obj->{'event'} = \%event);
}
# readClass
# read classFile and extract audit class information
sub readClass {
my $obj = shift;
my %class = ();
my $file = $obj->{'classFile'};
my $fileHandle = do {local *FileHandle; *FileHandle};
open($fileHandle, $file) or die sprintf("$failedOpen\n", $file, $!);
my $count = 0;
while (<$fileHandle>) {
chomp;
s/#.*//; # remove comment
next if (/^\s*$/);
my ($mask1, $class) = split(/:/); # third field not used
my $mask2 = hex($mask1); # integer
$class{$class} = $mask2;
$count++;
}
close $fileHandle;
print STDERR "found $count audit classes\n" if ($obj->{'debug'});
return ($obj->{'class'} = \%class);
}
sub filterLabel {
my $obj = shift;
my $label = shift;
my $eventFilter = $obj->{'eventFilter'};
my $keepIt = 1;
$keepIt = 0 if ($eventFilter && ($label !~ /$eventFilter/i));
return ($keepIt);
}
# Normally, the root of the event label is the system call. The
# attrFile attribute syscall or program overrides this.
sub filterCallName {
my $obj = shift;
my $label = shift;
my $callName = shift;
return ($callName) if ($callName);
$label =~ /AUE_(.*)/;
my $name = $1;
return (lc ($name));
}
# readControl
# read controlFile and extract flags and naflags information
# at present, minfree, maxfree and the audit partitions are not
# checked.
sub readControl {
my $obj = shift;
my $failMode = shift;
my $cError = 0;
my $errors = '';
my $file = $obj->{'controlFile'};
my $invalidClass = gettext('invalid class, %s, in audit_control: %s');
my $fileHandle = do {local *FileHandle; *FileHandle};
unless (open($fileHandle, $file)) {
die sprintf("$failedOpen\n", $file, $!)
unless ($failMode eq 'ignore');
return (0, '');
}
my %class = $obj->{'class'};
my @paths = $obj->{'paths'};
while (<$fileHandle>) {
chomp;
s/#.*//; # remove comment
next if (/^\s*$/);
if ((/^\s*flags:/i) || (/^\s*naflags:/i)) {
my ($class) = /flags:\s*(.*)/;
my @class = split(/\s*,\s*/, $class);
foreach $class (@class) {
$class =~ s/^[-+^]+//;
unless (defined ($class{$class})) {
$errors .=
sprintf("$invalidClass\n",
$class, $_);
$cError++;
}
}
}
elsif (/^\s*dir:\s*(.*)/) {
push (@paths, $1);
$obj->{'havePath'} = 1;
}
}
close $fileHandle;
return ($cError, $errors);
}
sub getPathList {
my $obj = shift;
$obj->readControl() unless ($obj->{'havePath'});
return ($obj->{'paths'});
}
# readUser
# read userFile and extract audit information for validation
sub readUser {
my $obj = shift;
my $failMode = shift;
my $cError = 0;
my $error = '';
my $file = $obj->{'userFile'};
my $fileHandle = do {local *FileHandle; *FileHandle};
unless (open($fileHandle, $file)) {
die sprintf("$failedOpen\n", $file, $!)
unless ($failMode eq 'ignore');
return (0, '');
}
# these strings are defined here mostly to avoid indentation problems
my $emptyErr = gettext('empty audit mask in audit_user: %s');
my $syntaxErr1 = gettext(
'incorrect syntax (exactly two colons req\'d) in audit_user: %s');
my $syntaxErr2 = gettext('incorrect syntax in audit_user: %s');
my $invalidErr = gettext('invalid class, %s, in audit_user: %s');
my $undefined = gettext('undefined user name in audit_user: %s');
my %class = $obj->{'class'};
while (<$fileHandle>) {
chomp;
s/#.*//; # remove comment
next if (/^\s*$/);
my $colonCount = tr/:/:/;
if ($colonCount != 2) {
$error .= sprintf("$syntaxErr1\n", $_);
$cError++;
}
my ($user, $always, $never) = split(/\s*:\s*/);
unless (defined($user)) {
$error .= sprintf("$syntaxErr2\n", $_);
$cError++;
next;
}
$error .= sprintf("$emptyErr\n", $_) unless ($always);
my ($name) = getpwnam($user);
unless (defined($name)) {
$error .= sprintf("$undefined\n", $user);
$cError++;
}
unless (defined($always) && defined($never)) {
$error .= sprintf("$emptyErr\n", $_);
$cError++;
next;
}
my $verify = $always . ',' . $never;
my @class = split(/\s*,\s*/, $verify);
my $thisClass;
foreach $thisClass (@class) {
$thisClass =~ s/^[-+^]+//;
unless (defined $class{$thisClass}) {
$error .= sprintf("$invalidErr\n", $thisClass,
$_);
$cError++;
}
}
}
close $fileHandle;
return ($cError, $error);
}
# ckAttrEvent complains if controlFile and attrFile don''t contain the
# same list of events.
sub ckAttrEvent {
my $obj = shift;
my $cError = 0;
my $error = '';
my $cAttr = 0;
my $label;
my $attrErr = gettext(
'%s entry in attribute file but not in event file');
my $eventErr = gettext(
'%s entry in event file but not in attribute file');
my %attr = %{$obj->{'attr'}};
my %event = %{$obj->{'event'}};
foreach $label (keys %attr) {
$cAttr++;
unless ($event{$label}) {
$error .= sprintf("$attrErr\n", $label);
$cError++;
}
}
my $cEvent = 0;
foreach $label (keys %event) {
$cEvent++;
unless ($attr{$label}) {
$error .= sprintf("$eventErr\n", $label);
$cError++;
}
}
# debug only; not I18N'd
print STDERR
"$cAttr audit_record_attr entries and $cEvent audit_event entries\n"
if ($obj->{'debug'});
return ($cError, $error);
}
# chkBslash (helper)
# check the given string for backslash character at the end; if found
# return the string sent as a first argument, otherwise return empty
# string.
sub chkBslash ($$) {
my $retStr = shift;
my $strPtr = shift;
if ( $$strPtr !~ /\\$/ ) {
$retStr = '';
}
return $retStr;
}
1;