libaudit.pm.in 17.3 KB
Newer Older
1 2 3
#!/usr/bin/perl -w

#
4
# Copyright (c) 2000-2019 University of Utah and the Flux Group.
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file 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 Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
24 25 26 27 28 29 30
#

package libaudit;
use Exporter;

@ISA = "Exporter";
@EXPORT =
31
    qw ( AuditStart AuditEnd AuditAbort AuditFork AuditSetARGV AuditGetARGV
32
	 AddAuditInfo
33
	 LogStart LogEnd LogAbort AuditDisconnect AuditPrefork
34
	 LIBAUDIT_NODAEMON LIBAUDIT_DAEMON LIBAUDIT_LOGONLY
35
	 LIBAUDIT_NODELETE LIBAUDIT_FANCY LIBAUDIT_LOGTBOPS LIBAUDIT_LOGTBLOGS
36
	 LIBAUDIT_DEBUG LIBAUDIT_NOCHILD
37
       );
38 39 40

# After package decl.
use English;
41
use POSIX qw(isatty setsid dup2 dup);
42 43
use File::Basename;
use IO::Handle;
44
use Carp;
45 46 47 48 49

#
# Testbed Support libraries
#
use libtestbed;
50
use Brand;
51 52 53 54

my $TBOPS	= "@TBOPSEMAIL@";
my $TBAUDIT	= "@TBAUDITEMAIL@";
my $TBLOGS	= "@TBLOGSEMAIL@";
55
my $OURDOMAIN   = "@OURDOMAIN@";
56 57
my $SCRIPTNAME	= "Unknown";
my $USERNAME    = "Unknown";
58
my $GCOS        = "Unknown";
59 60
my @SAVEARGV	= @ARGV;
my $SAVEPID	= $PID;
61
my $PREFORKFILE = "/var/tmp/auditfork_$PID";
62 63
my $SAVE_STDOUT = 0;
my $SAVE_STDERR = 0;
64 65 66 67 68 69 70

# Indicates, this script is being audited.
my $auditing	= 0;

# Where the log is going. When not defined, do not send it in email!
my $logfile;

71 72 73
# Sleazy.
my $prefork;

74 75
# Logonly, not to audit list.
my $logonly     = 0;
76 77
# Log to tbops or tblogs
my $logtblogs   = 0;
78

79 80 81
# Save log when logging only.
my $savelog     = 0;

82 83 84 85
# If set than send "fancy" email and also call tblog_find_error
# on errors
my $fancy       = 0;

86 87 88
# We be forked.
my $forked      = 0;

89 90 91
# Do not send email from children, just the parent.
my $nochild     = 0;

92 93 94
# Branding for email. Set via audit info.
my $brand;

95 96 97
# Extra info used when AUDIT_FANCY is set
my %AUDIT_INFO;

98 99 100 101 102 103 104 105 106
# Untainted scriptname for email below.
if ($PROGRAM_NAME =~ /^([-\w\.\/]+)$/) {
    $SCRIPTNAME = basename($1);
}
else {
    $SCRIPTNAME = "Tainted";
}

# The user running the script.
107
if (my ($name,undef,undef,undef,undef,undef,$gcos) = getpwuid($UID)) {
108
    $USERNAME = $name;
109
    $GCOS     = $gcos;
110 111
}

112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
#
# Debugging audit is a pain.
#
my $debugfile;

sub DebugAudit($)
{
    my ($msg) = @_;
    
    if (defined($debugfile)) {
	system("/bin/date >> $debugfile");
	system("/bin/echo '$msg' >> $debugfile");
    }
}

127 128 129 130 131 132 133
#
# Options to AuditStart.
#
sub LIBAUDIT_NODAEMON	{ 0; }
sub LIBAUDIT_DAEMON	{ 0x01; }
sub LIBAUDIT_LOGONLY	{ 0x02; }
sub LIBAUDIT_NODELETE	{ 0x04; }
134 135
sub LIBAUDIT_FANCY      { 0x08; } # Only use if libdb and libtblog are
                                  # already in use
136
sub LIBAUDIT_LOGTBOPS	{ 0x10; }
137
sub LIBAUDIT_LOGTBLOGS	{ 0x20; }
138
sub LIBAUDIT_DEBUG	{ 0x40; }
139
sub LIBAUDIT_NOCHILD	{ 0x80; }
140

141 142 143 144 145 146
#
# Start an audit (or log) of a script. First arg is a flag indicating if
# the script should fork/detach. The second (optional) arg is a file name
# into which the log should be written. The return value is non-zero in the
# parent, and zero in the child (if detaching).
# 
147
sub AuditStart($;$$)
148
{
149
    my($daemon, $logname, $options) = @_;
150 151 152 153 154 155 156

    #
    # If we are already auditing, then do not audit a child script. This
    # would result in a blizzard of email! We wrote the scripts, so we
    # should now what they do!
    #
    if (defined($ENV{'TBAUDITON'})) {
157
	return 0;
158 159
    }

160 161 162 163 164 165 166 167
    # Reset to default for rentry in log running script.
    $logfile    = undef;
    $prefork    = undef;
    $logonly    = 0;
    $logtblogs  = 0;
    $savelog    = 0;
    $fancy      = 0;
    $forked     = 0;
168
    $brand      = Brand->Create(); # Default Brand.
169

170
    # Logging instead of "auditing" ...
171
    if (defined($options)) {
172 173 174
	if ($options & LIBAUDIT_NODELETE()) {
	    $savelog = 1;
	}
175 176 177
	if ($options & LIBAUDIT_NOCHILD()) {
	    $nochild = 1;
	}
178 179 180
	if ($options & LIBAUDIT_DEBUG()) {
	    $debugfile = "/var/tmp/auditdebug.$$";
	}
181 182 183
	if ($options & LIBAUDIT_LOGONLY()) {
	    $logonly = 1;

184 185 186
	    if ($options & LIBAUDIT_LOGTBOPS()) {
		$logtbops = 1;
	    }
187 188 189
	    elsif ($options & LIBAUDIT_LOGTBLOGS()) {
		$logtblogs = 1;
	    }
190
	}
191 192 193 194 195 196
	if ($options & LIBAUDIT_FANCY()) {
	    if (!$INC{"libdb.pm"} || !$INC{"libtblog.pm"}) {
		croak "libdb and libtblog must be loaded when using LIBAUDIT_FANCY";
	    }
	    $fancy = 1;
	}
197 198
    }

199 200 201 202 203 204 205
    #
    # If this is an interactive session, then do not bother with a log
    # file. Just send it to the output and hope the user is smart enough to
    # save it off. We still want to audit the operation though, sending a
    # "what was done" message to the audit list, and CC it to tbops if it
    # exits with an error. But the log is the responsibility of the user.
    #
206
    if (!$daemon && isatty(STDOUT)) {
207 208
	$auditing = 1;
	$ENV{'TBAUDITON'} = "$SCRIPTNAME:$USERNAME";
209
	return 0;
210
    }
211 212 213
    # Clear this in case left behind, as for long running process.
    unlink($PREFORKFILE)
	if (-e $PREFORKFILE);
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232

    if (!defined($logname)) {
	$logfile = TBMakeLogname("$SCRIPTNAME");
    }
    else {
	$logfile = $logname;
    }
    $ENV{'TBAUDITLOG'} = $logfile;
    $ENV{'TBAUDITON'}  = "$SCRIPTNAME:$USERNAME";

    #
    # Okay, daemonize.
    #
    if ($daemon) {
	my $mypid = fork();
	if ($mypid) {
	    select(undef, undef, undef, 0.2);
	    return $mypid;
	}
233 234 235
	if (defined(&libtblog::tblog_new_child_process)) {
	    libtblog::tblog_new_child_process();
	}
236 237 238 239 240 241 242 243 244 245 246
    }
    $auditing = 1;

    #
    # If setuid, lets reset the owner/mode of the log file. Otherwise its
    # owned by root, mode 600 and a pain to deal with later, especially if
    # the script drops its privs!
    #
    if ($UID != $EUID) {
	chown($UID, $EUID, $logfile);
    }
247
    chmod(0666, $logfile);
248

249
    # Save old stderr and stdout.
250 251 252
    if (!$daemon) {
	$libaudit::SAVE_STDOUT = POSIX::dup(fileno(STDOUT));
	$libaudit::SAVE_STDERR = POSIX::dup(fileno(STDERR));
253
    }
254 255 256 257 258 259 260 261 262 263 264
    open(STDOUT, ">> $logfile") or
	die("opening $logfile for STDOUT: $!");
    open(STDERR, ">> $logfile") or
	die("opening $logfile for STDERR: $!");

    #
    # Turn off line buffering on output
    #
    STDOUT->autoflush(1);
    STDERR->autoflush(1);

265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
    if ($daemon) {
	#
	# We have to disconnect from the caller by redirecting both
	# STDIN and STDOUT away from the pipe. Otherwise the caller
	# will continue to wait even though the parent has exited.
	#
	open(STDIN, "< /dev/null") or
	    die("opening /dev/null for STDIN: $!");

	#
	# Create a new session to ensure we are clear of any process group
	#
        POSIX::setsid() or
	    die("setsid failed: $!");
    }

281 282 283
    return 0;
}

284
# Logging, not auditing.
285
sub LogStart($;$$)
286
{
287 288 289
    my($daemon, $logname, $options) = @_;
    $options = 0
	if (!defined($options));
290

291
    return AuditStart($daemon, $logname, $options|LIBAUDIT_LOGONLY());
292 293
}

294
sub LogEnd(;$)
295
{
296 297 298
    my ($status) = @_;
    
    return AuditEnd($status);
299 300
}

301 302 303 304 305
sub LogAbort()
{
    return AuditAbort();
}

306 307 308
#
# Finish an Audit. 
#
309
sub AuditEnd(;$)
310
{
311 312 313 314 315 316
    my ($status) = @_;

    $status = 0
	if (!defined($status));
    
    SendAuditMail($status);
317
    delete @ENV{'TBAUDITLOG', 'TBAUDITON'};
318
    DeleteLogFile()
319
	if (defined($logfile) && !$savelog);
320 321 322
    return 0;
}

323 324 325 326 327 328 329 330
#
# Overwrite our saved argv. Usefull when script contains something that
# should not go into a mail log.
#
sub AuditSetARGV(@)
{
    @SAVEARGV = @_;
}
331 332 333 334
sub AuditGetARGV()
{
    return @SAVEARGV;
}
335

336 337 338
#
# Basically, we saying we are not going back.
#
339 340 341
sub AuditDisconnect()
{
    if ($auditing) {
342
	if (!$daemon && $libaudit::SAVE_STDOUT) {
343 344
	    open(FOO, "> /dev/null");
	    
345 346
	    POSIX::close($libaudit::SAVE_STDOUT);
	    POSIX::close($libaudit::SAVE_STDERR);
347

348 349 350
	    $libaudit::SAVE_STDOUT = POSIX::dup(fileno(FOO));
	    $libaudit::SAVE_STDERR = POSIX::dup(fileno(FOO));
	    close(FOO);
351 352 353 354
	}
    }
}

355 356 357 358 359 360 361 362
#
# Abort an Audit. Dump the log file and do not send email.
#
sub AuditAbort()
{
    if ($auditing) {
	$auditing = 0;

363 364 365
	if (!$daemon && $libaudit::SAVE_STDOUT) {
	    POSIX::dup2($libaudit::SAVE_STDOUT, fileno(STDOUT));
	    POSIX::dup2($libaudit::SAVE_STDERR, fileno(STDERR));
366 367 368 369
	    POSIX::close($libaudit::SAVE_STDOUT);
	    POSIX::close($libaudit::SAVE_STDERR);
	    $libaudit::SAVE_STDOUT = 0;
	    $libaudit::SAVE_STDERR = 0;
370 371
	}

372 373 374 375 376
	if (defined($logfile)) {
	    #
	    # This should be okay; the process will keep writing to it,
	    # but will be deleted once the process ends and its closed.
	    #
377
	    DeleteLogFile()
378
	    	if (!$savelog);
379 380
	    undef($logfile);
	}
381
	delete @ENV{'TBAUDITLOG', 'TBAUDITON'};
382 383 384 385 386 387
	
	if (defined($prefork)) {
	    my $oldmask = umask(0000);
	    system("/usr/bin/touch $prefork");
	    umask($oldmask);
	}
388 389 390 391
    }
    return 0;
}

392 393 394 395 396 397 398 399 400 401 402
#
# Indicate we are about to fork. In general, we want the parent to exit
# first so the first part of the logging email gets sent. Otherwise the
# file might get deleted out from under the child.
# So we use some sleaze to make sure that happens.
#
sub AuditPrefork()
{
    return 0
	if (!$auditing);

403
    $prefork = $PREFORKFILE;
404 405
}

406 407 408 409 410 411
#
# Ug, forked children result in multiple copies. It does not happen often
# since most forks result in an exec.
#
sub AuditFork()
{
412 413 414
    return 0
	if (!$auditing || !defined($logfile));

415 416 417 418 419 420 421 422 423 424 425 426
    #
    # If prefork is set, we want the parent to exit first. So we wait for
    # that to happen.
    #
    if (defined($prefork)) {
	while (! -e $prefork) {
	    sleep(1);
	}
	unlink($prefork);
    }
    $prefork = undef;

427 428
    open(LOG, ">> $logfile") or
	die("opening $logfile for $logfile: $!");
429

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
    close(STDOUT);
    close(STDERR);
    POSIX::dup2(fileno(LOG), 1);
    POSIX::dup2(fileno(LOG), 2);
    STDOUT->fdopen(1, "a");
    STDERR->fdopen(2, "a");
    close(LOG);

    #
    # Turn off line buffering on output
    #
    STDOUT->autoflush(1);
    STDERR->autoflush(1);

    #
445
    # Need to close these so that this side of the fork is disconnected.
446 447
    # Do NOT close the saved STDOUT/STDERR descriptors until the new
    # ones are open and dup'ed into fileno 1 and 2, and the LOG descriptor
448
    # closed. This was causing SelfLoader to get confused about something!
449
    #
450 451 452 453 454
    if (!$daemon) {
	POSIX::close($libaudit::SAVE_STDOUT)
	    if ($libaudit::SAVE_STDOUT && $libaudit::SAVE_STDOUT != 1);
	POSIX::close($libaudit::SAVE_STDERR)
	    if ($libaudit::SAVE_STDERR && $libaudit::SAVE_STDERR != 2);
455 456
	$libaudit::SAVE_STDOUT = 0;
	$libaudit::SAVE_STDERR = 0;
457 458
    }

459 460 461 462 463 464
    #
    # We have to disconnect STDIN from the caller too.
    #
    open(STDIN, "< /dev/null") or
	die("opening /dev/null for STDIN: $!");

465 466 467 468
    #
    # Create a new session to ensure we are clear of any process group.
    #
    POSIX::setsid();
469 470 471 472

    # For exit handling.
    $SAVEPID = $PID;
    $forked  = 1;
473
    
474
    return 0;
475 476
}

477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
#
# Try to delete the log file. We might have to flip back to root.
#
sub DeleteLogFile()
{
    return
	if (!defined($logfile) || $savelog);

    return
	if (unlink($logfile));

    # Failed, try flipping (which will fail of course if not setuid).
    $EUID = 0;
    unlink($logfile);
}

493 494 495 496 497 498 499 500
#
# Internal function to send the email. First argument is exit status.
#
# Two messages are sent. A topical message is sent to the audit list. This
# is a short message that says what was done and by who. The actual log of
# what happened is sent to the logs list so that we can go back and see the
# details if needed.
# 
501
sub SendFancyMail($);
502 503 504 505 506 507 508 509
sub SendAuditMail($)
{
    my($exitstatus) = @_;
    
    if ($auditing) {
	# Avoid duplicate messages.
	$auditing = 0;

510 511 512 513 514 515
	# Needs to called here before STDOUT and STDERR is
	# redirectected below
	if ($exitstatus && $fancy) {
	    &libtblog::tblog_find_error(); 
	}

516 517 518
	if (!$daemon && $libaudit::SAVE_STDOUT) {
	    POSIX::dup2($libaudit::SAVE_STDOUT, fileno(STDOUT));
	    POSIX::dup2($libaudit::SAVE_STDERR, fileno(STDERR));
519 520
	}

521
	my $subject  = "$SCRIPTNAME @SAVEARGV";
522
	if ($exitstatus) {
523 524 525
	    $subject = "Failed: $subject";
	}

526 527
	my $body     = "$SCRIPTNAME @SAVEARGV\n" .
	               "Invoked by $USERNAME ($GCOS)";
528 529
	if ($exitstatus) {
	    $body   .= "\nExited with status: $exitstatus";
530
	}
531 532 533
	if (defined($AUDIT_INFO{'message'})) {
	    $body   .= "\n" . $AUDIT_INFO{'message'};
	}
534
	my $FROM     = "$GCOS <${USERNAME}\@${OURDOMAIN}>";
535

536
	if (! $logonly) {
537
	    $brand->SendEmail($TBAUDIT, $subject, $body, $FROM, undef, ());
538
	}
539 540

	# Success and no log ...
541
	if ($exitstatus == 0 && !(defined($logfile) && -s $logfile)) {
542
	    # Do not save empty logfile. 
543
	    DeleteLogFile()
544
		if (defined($logfile));
545
	    goto done;
546 547
	}

548 549
	if ($fancy) {
	    SendFancyMail($exitstatus);
550
	    goto done;
551 552
	}

553 554 555 556 557 558 559 560
	#
	# Send logfile to tblogs. Carbon to tbops if it failed. If no logfile
	# then no point in sending to tblogs, obviously.
	#
	my $TO;
	my $HDRS  = "Reply-To: $TBOPS";
	my @FILES = ();
	
561
	if (defined($logfile) && -s $logfile) {
562
	    @FILES = ($logfile);
563 564

	    if ($logonly) {
565 566 567 568
		if (defined($AUDIT_INFO{'to'})) {
		    $TO    = join(', ', @{ $AUDIT_INFO{'to'} });
		}
		elsif ($logtbops) {
569 570
		    $TO    = $TBOPS;
		}
571 572 573 574
		elsif ($logtblogs) {
		    $TO    = $TBLOGS;
		    $HDRS .= "\nCC: $TBOPS" if ($exitstatus);
		}
575 576 577 578
		else {
		    $TO    = $FROM;
		    $HDRS .= "\nCC: ". ($exitstatus ? $TBOPS : $TBLOGS);
		}
579 580 581 582 583
	    }
	    else {
		$TO    = $TBLOGS;
		$HDRS .= "\nCC: $TBOPS" if ($exitstatus);
	    }
584
	}
585 586 587 588
	elsif ($logtblogs) {
	    $TO    = $TBLOGS;
	    $HDRS .= "\nCC: $TBOPS" if ($exitstatus);
	}
589 590 591
	else {
	    $TO    = $TBOPS;
	}
592 593 594 595
	if (defined($AUDIT_INFO{'cc'})) {
	    $HDRS .= "\n";
	    $HDRS .= "CC: " . join(', ', @{ $AUDIT_INFO{'cc'} });
	}
596

597 598
	# This always succeeds, stop leaving file in /tmp
	$brand->SendEmail($TO, $subject, $body, $FROM, $HDRS, @FILES);
599
	DeleteLogFile()
600 601
	    if (defined($logfile) && !$savelog);

602 603 604
      done:
	system("/usr/bin/touch $prefork")
	    if (defined($prefork));
605 606 607
    }
}

608 609 610 611
sub SendFancyMail($)
{
    import libdb;
    import libtblog;
612
    import User;
613 614 615 616 617

    my ($exitstatus) = @_;
    
    my ($TO, $FROM);
    my ($name, $email);
618 619 620 621 622
    my $this_user = User->ThisUser();
    if (defined($this_user)) {
	$name  = $this_user->name();
	$email = $this_user->email();
	$TO    = "$name <$email>";
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
    } else {
	$TO = "$GCOS <${USERNAME}\@${OURDOMAIN}>";
    }
    $FROM = $TO;

    my @FILES;
    
    if (defined($logfile) && -s $logfile) {
	@FILES = ($logfile);
    }

    # Avoid sending a person the same email twice
    my $extra_cc;
    if (defined ($AUDIT_INFO{cc})) {
	my @cc;
	my @prev_emails = ($email);
	OUTER: foreach (@{$AUDIT_INFO{cc}}) {
	    ($email) = /([^<> \t@]+@[^<> \t@]+)/;
	    foreach my $e (@prev_emails) {
		next OUTER if $email eq $e;
		push @prev_email, $e;
	    }
	    push @cc, $_;
	}
	if (@cc) {
	    $extra_cc = "Cc: ";
	    $extra_cc .= join(', ', @cc);
	}
    }

    my $sendmail_res;
    if ($exitstatus) {
	my $d = tblog_lookup_error();
	my $prefix;
	$prefix .= "$SCRIPTNAME @SAVEARGV\n";
	$prefix .= "Exited with status: $exitstatus";
	my $what = "Failed: $SCRIPTNAME";
	$what = $AUDIT_INFO{failure_frag} if defined $AUDIT_INFO{failure_frag};
	$which = $AUDIT_INFO{which};
662 663 664 665

	# Ick.
	local $libtestbed::MAILTAG = $brand->EmailTag();
    
666 667 668
	$sendmail_res 
	    = tblog_email_error($d, $TO, $what,	$which, 
				$FROM, $extra_cc, "Cc: $TBOPS",
669
				$prefix, @FILES);
670 671 672 673 674 675 676 677 678 679 680 681 682
    } else {

	my $subject  = "$SCRIPTNAME succeeded";
	$subject = $AUDIT_INFO{success_frag} if defined $AUDIT_INFO{success_frag};
	$subject .= ": $AUDIT_INFO{which}" if defined $AUDIT_INFO{which};
	my $body     = "$SCRIPTNAME @SAVEARGV\n";

	my $HDRS;
	$HDRS .= "$extra_cc\n" if defined $extra_cc;
	$HDRS .= "Reply-To: $TBOPS\n";
	$HDRS .= "Bcc: $TBLOGS";
	
	$sendmail_res 
683
	    = $brand->SendEmail($TO, $subject, $body, $FROM, $HDRS, @FILES);
684 685 686
    }
    
    if ($sendmail_res) {
687
	DeleteLogFile()
688
	    if (defined($logfile) && !$savelog);
689 690 691 692 693 694 695 696 697 698
    }
}


# Info on possibe values for AUDIT_INFO
# [KEY => string|list]
my %AUDIT_METAINFO = 
    ( which => 'string',        # ex "PROJ/EXP"
      success_frag => 'string', # ex "T. Swapped In"
      failure_frag => 'string', # ie "Bla Failure"
699
      message      => 'string',
700
      to           => 'list',   # Send audit mail to these people
701 702
      cc           => 'list',   # Cc audit mail to these people
      brand        => 'brand'); # Brand object for sendmail
703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723

#
# AddAuditInfo($key, $value)
#   add additional information for libaudit to use in SendAuditMail
#   when AUDIT_FANCY is set
#
# TODO: Eventually child scripts should be able to use AddAuditInfo, not 
#   just the script in which AuditStart(...) was called.  This will probably
#   involve storing the values in the database somehow.
#
sub AddAuditInfo ($$) {
    my ($key, $value) = @_;

    if (!$auditing) {

	carp "AddAuditInfo($key, ...) ignored since the script isn't being audited.";
	return 0;

    }

    if ($AUDIT_METAINFO{$key} eq 'string') {
724

725 726 727 728 729 730 731 732
	$AUDIT_INFO{$key} = $value;
	return 1;

    } elsif ($AUDIT_METAINFO{$key} eq 'list') {

	push @{$AUDIT_INFO{$key}}, $value;
	return 1;

733 734
    } elsif ($AUDIT_METAINFO{$key} eq 'brand') {
	$brand = $value;
735 736 737 738 739 740 741 742
    } else {

	carp "Unknown key, \"$key\" in AddAuditInfo";
	return 0;

    }
}

743 744 745 746
#
# When the script ends, if the audit has not been sent, send it. 
# 
END {
747
    return
748
	if (($forked || $nochild) && $PID != $SAVEPID);
749
    
750 751 752 753 754 755 756 757 758
    # Save, since shell commands will alter it.
    my $exitstatus = $?;
    
    SendAuditMail($exitstatus);

    $? = $exitstatus;
}

1;