1#
2# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
3# Use is subject to license terms.
4#
5# CDDL HEADER START
6#
7# The contents of this file are subject to the terms of the
8# Common Development and Distribution License (the "License").
9# You may not use this file except in compliance with the License.
10#
11# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
12# or http://www.opensolaris.org/os/licensing.
13# See the License for the specific language governing permissions
14# and limitations under the License.
15#
16# When distributing Covered Code, include this CDDL HEADER in each
17# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
18# If applicable, add the following below this CDDL HEADER, with the
19# fields enclosed by brackets "[]" replaced with your own identifying
20# information: Portions Copyright [yyyy] [name of copyright owner]
21#
22# CDDL HEADER END
23#
24
25# WARNING -- this package implements a Sun private interface; it may
26# change without notice.
27
28package Sun::Solaris::BSM::_BSMparse;
29require 5.005;
30use strict;
31use Exporter;
32use Sun::Solaris::Utils qw(gettext);
33
34use vars qw($VERSION $failedOpen
35    %EXPORT_TAGS @ISA @EXPORT_OK @EXPORT_FAIL);
36
37$VERSION = '1.01';
38
39@ISA = qw(Exporter);
40my @constants = qw();
41@EXPORT_OK = qw(readAttr readEvent readClass filterLabel filterCallName
42		readControl getPathList readUser ckAttrEvent);
43@EXPORT_FAIL = qw($failedOpen);
44%EXPORT_TAGS = (ALL => \@EXPORT_OK);
45
46$failedOpen = gettext("failed to open %s: %s");
47
48sub new {
49	my $obj = shift;
50	my $debug = shift;	# bool
51	my $filters = shift;	# options for filtering
52
53	my $dir = '/etc/security';
54	my $attrDir = '/usr/lib/audit';
55        my $configDir = $dir;
56	$attrDir = shift if (@_);	# override for test
57	$configDir = shift if (@_);	# ditto
58
59	my $suffix = '';
60	$suffix = shift if (@_);	# test, again
61
62	$obj = ref($obj) || $obj;
63
64	my ($recordf, $classf, $controlf, $eventf, $userf) =
65	   ("$attrDir/audit_record_attr$suffix",
66	    "$configDir/audit_class$suffix",
67	    "$configDir/audit_control$suffix",
68	    "$configDir/audit_event$suffix",
69	    "$configDir/audit_user$suffix");
70
71	return (bless {
72		'attrFile'	=> $recordf,
73		'classFile'	=> $classf,
74		'classFilter'	=> $filters->{'classFilter'},
75		'controlFile'	=> $controlf,
76		'debug'		=> $debug,
77		'eventFile'	=> $eventf,
78		'eventFilter'	=> $filters->{'eventFilter'},
79		'idFilter'	=> $filters->{'idFilter'},
80		'havePath'	=> 0,
81		'kernelDefault'	=> '',
82		'userDefault'	=> '',
83		'userFile'	=> $userf}, $obj);
84}
85
86# readAttr
87# read the hand edited attrFile file
88#
89# return a hash reference
90
91sub readAttr {
92	my $obj = shift;
93
94	my $file = $obj->{'attrFile'};
95	my $fileHandle = do {local *FileHandle; *FileHandle};
96	open($fileHandle, $file) or die sprintf("$failedOpen\n", $file, $!);
97
98	my $count = 0;
99	my $lastAttr = '';
100	my $lastMacro = '';
101
102	my $attrState = -1;
103	my $caseState = 0;
104	my $label;
105	my $callName = '';
106	my $skip = '';
107	my $description = 'none';
108	my $format = 'none';
109	my $comment = '';
110	my $title = 'none';
111	my $note = '';
112	my $case = '';
113	my @case = ();
114	my %skipClass;
115	my %attr = ();
116	my %token = ();
117	my $classFilter = $obj->{'classFilter'};
118	$classFilter = '' unless (defined ($classFilter));
119
120	my %noteAlias = ();
121	while (<$fileHandle>) {
122		chomp;
123		s/#.*//;	 # remove comment
124		next if (/^\s*$/);
125
126		if ($attrState < 0) {  # initial state:  header info
127			# continue assigning lines to multiline macros
128			# type: message
129			if ( $lastMacro ne '' ) {
130				my ($mcr, $attr) = split(/\s*:\s*/, $lastMacro);
131
132				if ($mcr eq "message") {
133					chomp($noteAlias{$attr});
134					chop($noteAlias{$attr});
135
136					$_ =~ /^\s*(.*)/i;
137					$noteAlias{$attr} .= $1;
138
139					$lastMacro = chkBslash($lastMacro, \$1);
140				}
141				next;
142			}
143
144			$lastMacro = '';
145			if (/^\s*skipClass\s*=\s*(.*)/i) {
146				my $class = $1;
147				# don't skip what you're searching for
148				next if (index(lc($classFilter),lc($class)) > -1);
149				$skipClass{$1} = 1;
150				next;
151			}
152			elsif (/^\s*token\s*=\s*(.*)/i) {
153				my ($attr, $value) = split(/\s*:\s*/, $1);
154				$token{$attr} = $value;
155				next;
156			}
157			elsif (/^\s*message\s*=\s*(.*)/i) {
158				my ($attr, $value) = split(/\s*:\s*/, $1);
159				$noteAlias{$attr} = $value;
160				$lastMacro = chkBslash("message:$attr", \$1);
161				next;
162			}
163			elsif (/^\s*kernel\s*=\s*(.*)/i) {
164				my ($attr, $value) = split(/\s*:\s*/, $1);
165				$obj->{'kernelDefault'} = $1;
166				next;
167			}
168			elsif (/^\s*user\s*=\s*(.*)/i) {
169				my ($attr, $value) = split(/\s*:\s*/, $1);
170				$obj->{'userDefault'} = $1;
171				next;
172			}
173		}
174
175		# continue assigning lines to multiline attributes
176		# type: case, comment, note, format
177		if ( $lastAttr ne '' ) {
178			my $curAttrVal = '';
179
180			eval "\$curAttrVal = \$$lastAttr";
181			chomp($curAttrVal);
182			chop($curAttrVal);
183
184			$_ =~ /^\s*(.*)/i;
185			$curAttrVal .= $1;
186
187			eval "\$$lastAttr = \$curAttrVal";
188
189			$lastAttr = chkBslash($lastAttr, \$1);
190			next;
191		}
192
193		$lastAttr = '';
194		if (/^\s*label\s*=\s*(.*)/i) {
195			$attrState = 0 if ($attrState < 0);
196			my $newLabel = $1;
197
198			if ($obj->{'debug'}) {
199				print STDERR qq{
200$newLabel is duplicated in the attribute file (line $.)
201				} if ($attr{$newLabel});
202			}
203			# if $attrState not zero, an unwritten record exists
204			if ($attrState) {
205				$callName = $obj->filterCallName($label,
206				    $callName);
207				push(@case, [$case, $format, $comment, $note]);
208
209				if ($obj->filterLabel($label)) {
210					$attr{$label} =
211					    [$callName, $description, $title,
212					    $skip, @case];
213					$count++;
214				}
215				$format = $description = $title = 'none';
216				$case = $note = $comment = $skip = $callName
217				    = '';
218				@case = ();
219				$caseState = 0;
220			}
221			$label = $newLabel;
222			$attrState = 1;
223		}
224		elsif (/^\s*skip\s*=\s*(.*)/i) {
225			$skip = $1;
226		}
227		elsif (/^\s*syscall\s*=\s*(.*)/i) {
228			$callName = $1;
229		}
230		elsif (/^\s*program\s*=\s*(.*)/i) {
231			$callName = $1;
232		}
233		elsif (/^\s*title\s*=\s*(.*)/i) {
234			$title = $1;
235		}
236		elsif (/^\s*see\s*=\s*(.*)/i) {
237			$description = $1;
238		}
239		elsif (/^\s*format\s*=\s*(.*)/i) {
240			$format = $1;
241			$lastAttr = chkBslash("format", \$1);
242		}
243		elsif (/^\s*comment\s*=\s*(.*)/i) {
244			$comment .= $1;
245			$lastAttr = chkBslash("comment", \$1);
246		}
247		elsif (/^\s*note\s*=\s*(.*)/i) {
248			$note .= $1;
249			$lastAttr = chkBslash("note", \$1);
250		}
251		elsif (/^\s*case\s*=\s*(.*)/i) {
252			if ($caseState) {
253				push(@case, [$case, $format, $comment, $note]);
254				$format = 'none';
255				$comment = $note = '';
256			}
257			$case = $1;
258			$lastAttr = chkBslash("case", \$1);
259			$caseState = 1;
260		}
261	}
262	if ($attrState) {
263		$callName = $obj->filterCallName($label, $callName);
264		push(@case, [$case, $format, $comment, $note]);
265		if ($obj->filterLabel($label)) {
266			$attr{$label} = [$callName, $description, $title, $skip,
267			    @case];
268			$count++;
269		}
270	}
271	close $fileHandle;
272	print STDERR "found $count audit attribute entries\n" if ($obj->{'debug'});
273
274	return ($obj->{'attr'} = \%attr, \%token, \%skipClass, \%noteAlias);
275}
276
277# readEvent
278# read eventFile and extract audit event information, including
279# which classes are associated with each event and what call is
280# related.
281
282sub readEvent {
283	my $obj = shift;
284
285	my %event = ();
286	my $file = $obj->{'eventFile'};
287
288	my $fileHandle = do {local *FileHandle; *FileHandle};
289	open($fileHandle, $file) or die sprintf("$failedOpen\n", $file, $!);
290
291	my $count = 0;
292
293	unless (defined $obj->{'class'} && (scalar keys %{$obj->{'class'}} > 1)) {
294		$obj->readClass();
295	}
296
297	my @classFilterMasks = ();
298	my $classFilter = $obj->{'classFilter'};
299	if ($classFilter) {
300		foreach (split(',', $classFilter)) {
301			push @classFilterMasks, $obj->{'class'}{$_};
302		}
303	}
304	# ignore customer-supplied audit events (id > 32767)
305
306	while (<$fileHandle>) {
307		chomp;
308		s/#.*//;	# remove comment
309		next if (/^\s*$/);
310		if (/^\s*(\d+):(\w+):([^:]+):(.*)/) {
311			my $id = $1;
312			my $label = $2;
313			my $description = $3;
314			my $class = $4;
315
316			if ($id !~ /\d+/) {
317				print STDERR "$id is not numeric (line $.)\n";
318				next;
319			}
320			next if ($id > 32767);
321
322			$class =~ s/\s*$//;
323
324			if ($obj->{'debug'}) {
325				print STDERR qq{
326$label is duplicated in the event file (line $.)
327				} if ($event{$label});
328			}
329			next unless ($obj->filterLabel($label));
330			my $mask = 0;
331			if ($classFilter) {
332				foreach (split(/\s*,\s*/, $class)) {
333					$mask |= $obj->{'class'}{$_};
334				}
335				my $skip = 0;
336				foreach my $filterMask (@classFilterMasks) {
337					unless ($mask & $filterMask) {
338						$skip = 1;
339						last;
340					}
341				}
342				next if $skip;
343			}
344			if ($obj->{'idFilter'}) {
345				next unless ($obj->{'idFilter'} == $id);
346			}
347			$event{$label} = [$id, $class, $description];
348
349			$count++;
350		}
351	}
352	close $fileHandle;
353	print STDERR "found $count audit events\n" if ($obj->{'debug'});
354
355	return ($obj->{'event'} = \%event);
356}
357
358# readClass
359# read classFile and extract audit class information
360
361sub readClass {
362	my $obj = shift;
363
364	my %class = ();
365	my $file = $obj->{'classFile'};
366
367	my $fileHandle = do {local *FileHandle; *FileHandle};
368	open($fileHandle, $file) or die sprintf("$failedOpen\n", $file, $!);
369
370	my $count = 0;
371
372	while (<$fileHandle>) {
373		chomp;
374		s/#.*//;	# remove comment
375		next if (/^\s*$/);
376		my ($mask1, $class) = split(/:/);  # third field not used
377		my $mask2 = hex($mask1);  # integer
378		$class{$class} = $mask2;
379		$count++;
380	}
381	close $fileHandle;
382	print STDERR "found $count audit classes\n" if ($obj->{'debug'});
383
384	return ($obj->{'class'} = \%class);
385}
386
387sub filterLabel {
388	my $obj = shift;
389	my $label = shift;
390
391	my $eventFilter = $obj->{'eventFilter'};
392	my $keepIt = 1;
393
394	$keepIt = 0 if ($eventFilter && ($label !~ /$eventFilter/i));
395
396	return ($keepIt);
397}
398
399# Normally, the root of the event label is the system call.  The
400# attrFile attribute syscall or program overrides this.
401
402sub filterCallName {
403	my $obj = shift;
404	my $label = shift;
405	my $callName = shift;
406
407	return ($callName) if ($callName);
408
409	$label =~ /AUE_(.*)/;
410
411	my $name = $1;
412
413	return (lc ($name));
414}
415
416# readControl
417# read controlFile and extract flags and naflags information
418# at present, minfree, maxfree and the audit partitions are not
419# checked.
420
421sub readControl {
422	my $obj = shift;
423	my $failMode = shift;
424
425	my $cError = 0;
426	my $errors = '';
427	my $file = $obj->{'controlFile'};
428	my $invalidClass = gettext('invalid class, %s, in audit_control: %s');
429
430	my $fileHandle = do {local *FileHandle; *FileHandle};
431	unless (open($fileHandle, $file)) {
432		die sprintf("$failedOpen\n", $file, $!)
433			unless ($failMode eq 'ignore');
434		return (0, '');
435	}
436	my %class = $obj->{'class'};
437	my @paths = $obj->{'paths'};
438	while (<$fileHandle>) {
439		chomp;
440		s/#.*//;	# remove comment
441		next if (/^\s*$/);
442		if ((/^\s*flags:/i) || (/^\s*naflags:/i)) {
443			my ($class) = /flags:\s*(.*)/;
444			my @class = split(/\s*,\s*/, $class);
445
446			foreach $class (@class) {
447				$class =~ s/^[-+^]+//;
448				unless (defined ($class{$class})) {
449					$errors .=
450					    sprintf("$invalidClass\n",
451					    $class, $_);
452					$cError++;
453				}
454			}
455		}
456		elsif (/^\s*dir:\s*(.*)/) {
457			push (@paths, $1);
458			$obj->{'havePath'} = 1;
459		}
460	}
461	close $fileHandle;
462	return ($cError, $errors);
463}
464
465sub getPathList {
466	my $obj = shift;
467
468	$obj->readControl() unless ($obj->{'havePath'});
469
470	return ($obj->{'paths'});
471}
472
473# readUser
474# read userFile and extract audit information for validation
475
476sub readUser {
477	my $obj = shift;
478	my $failMode = shift;
479
480	my $cError = 0;
481	my $error = '';
482	my $file = $obj->{'userFile'};
483
484	my $fileHandle = do {local *FileHandle; *FileHandle};
485	unless (open($fileHandle, $file)) {
486		die sprintf("$failedOpen\n", $file, $!)
487			unless ($failMode eq 'ignore');
488		return (0, '');
489	}
490	# these strings are defined here mostly to avoid indentation problems
491	my $emptyErr   = gettext('empty audit mask in audit_user: %s');
492	my $syntaxErr1 = gettext(
493	    'incorrect syntax (exactly two colons req\'d) in audit_user: %s');
494	my $syntaxErr2 = gettext('incorrect syntax in audit_user: %s');
495	my $invalidErr = gettext('invalid class, %s, in audit_user: %s');
496	my $undefined  = gettext('undefined user name in audit_user: %s');
497
498	my %class = $obj->{'class'};
499	while (<$fileHandle>) {
500		chomp;
501		s/#.*//;        # remove comment
502		next if (/^\s*$/);
503		my $colonCount = tr/:/:/;
504
505		if ($colonCount != 2) {
506			$error .= sprintf("$syntaxErr1\n", $_);
507			$cError++;
508		}
509		my ($user, $always, $never) = split(/\s*:\s*/);
510		unless (defined($user)) {
511			$error .= sprintf("$syntaxErr2\n", $_);
512			$cError++;
513			next;
514		}
515		$error .= sprintf("$emptyErr\n", $_) unless ($always);
516
517		my ($name) = getpwnam($user);
518		unless (defined($name)) {
519			$error .= sprintf("$undefined\n", $user);
520			$cError++;
521		}
522		unless (defined($always) && defined($never)) {
523			$error .= sprintf("$emptyErr\n", $_);
524			$cError++;
525			next;
526		}
527		my $verify = $always . ',' . $never;
528		my @class = split(/\s*,\s*/, $verify);
529		my $thisClass;
530
531		foreach $thisClass (@class) {
532			$thisClass =~ s/^[-+^]+//;
533			unless (defined $class{$thisClass}) {
534				$error .= sprintf("$invalidErr\n", $thisClass,
535				    $_);
536				$cError++;
537			}
538		}
539	}
540	close $fileHandle;
541	return ($cError, $error);
542}
543
544# ckAttrEvent complains if controlFile and attrFile don''t contain the
545# same list of events.
546
547sub ckAttrEvent {
548	my $obj = shift;
549
550	my $cError = 0;
551	my $error = '';
552	my $cAttr = 0;
553	my $label;
554	my $attrErr  = gettext(
555	    '%s entry in attribute file but not in event file');
556	my $eventErr = gettext(
557	    '%s entry in event file but not in attribute file');
558
559	my %attr = %{$obj->{'attr'}};
560	my %event = %{$obj->{'event'}};
561	foreach $label (keys %attr) {
562		$cAttr++;
563		unless ($event{$label}) {
564			$error .= sprintf("$attrErr\n", $label);
565			$cError++;
566		}
567	}
568	my $cEvent = 0;
569	foreach $label (keys %event) {
570		$cEvent++;
571		unless ($attr{$label}) {
572			$error .= sprintf("$eventErr\n", $label);
573			$cError++;
574		}
575	}
576	# debug only; not I18N'd
577	print STDERR
578	    "$cAttr audit_record_attr entries and $cEvent audit_event entries\n"
579		if ($obj->{'debug'});
580	return ($cError, $error);
581}
582
583# chkBslash (helper)
584# check the given string for backslash character at the end; if found
585# return the string sent as a first argument, otherwise return empty
586# string.
587sub chkBslash ($$) {
588	my $retStr = shift;
589	my $strPtr = shift;
590
591	if ( $$strPtr !~ /\\$/ ) {
592		 $retStr = '';
593	}
594
595	return $retStr;
596}
597
5981;
599