File Coverage

lib/Audit/Log.pm
Criterion Covered Total %
statement 81 99 81.8
branch 24 42 57.1
condition 16 21 76.1
subroutine 9 10 90.0
pod 3 3 100.0
total 133 175 76.0


line stmt bran cond sub pod time code
1             package Audit::Log 0.005;
2              
3 1     1   84534 use strict;
  1         2  
  1         28  
4 1     1   4 use warnings;
  1         11  
  1         35  
5              
6 1     1   20 use 5.006;
  1         3  
7 1     1   6 use v5.12.0; # Before 5.006, v5.10.0 would not be understood.
  1         3  
8              
9 1     1   589 use File::Which();
  1         1122  
  1         20  
10 1     1   557 use UUID::Tiny();
  1         23866  
  1         41  
11 1     1   7 use List::Util qw{uniq};
  1         2  
  1         1164  
12              
13             # ABSTRACT: auditd log parser with minimal dependencies, using no perl features past 5.12
14              
15             sub new {
16 1     1 1 104 my ( $class, $path, @returning ) = @_;
17 1 50       5 $path = '/var/log/audit/audit.log' unless $path;
18 1         7 my $fullpath = File::Which::which('ausearch');
19              
20 1 50       288 if ( $path eq 'ausearch' ) {
21 0 0       0 die "Cannot find ausearch" unless -f $fullpath;
22             }
23             else {
24 1 50       16 die "Cannot access $path" unless -f $path;
25             }
26             return
27 1         10 bless( { path => $path, ausearch => $fullpath, returning => \@returning },
28             $class );
29             }
30              
31             sub search {
32 1     1 1 19 my ( $self, %options ) = @_;
33              
34 1         2 my $ret = [];
35 1         2 my $in_block = 1;
36 1         1 my $line = -1;
37 1         4 my ( $cwd, $exe, $comm, $res ) = ( '', '', '', '' );
38 1         1 my $fh;
39 1 50       7 if ( $self->{path} eq 'ausearch' ) {
40 0         0 my @args = qw{--input-logs --raw};
41 0         0 push( @args, ( '-k', $self->{key} ) );
42             push( @args, ( '-sv', $options{res} ? 'yes' : 'no' ) )
43 0 0       0 if defined $options{success};
    0          
44 0 0       0 push( @args, ( '-comm', $options{comm} ) ) if defined $options{comm};
45 0 0       0 open( $fh, '|', qq|$self->{fullpath} @args| )
46             or die "Could not run $self->{fullpath}!";
47             }
48             else {
49 1 50       39 open( $fh, '<', $self->{path} ) or die "Could not open $self->{path}!";
50             }
51 1         47 LINE: while (<$fh>) {
52 327 100 100     1026 next if index( $_, 'SYSCALL' ) < 0 && !$in_block;
53              
54             # I am trying to cheat here to snag the timestamp.
55 105         131 my $msg_start = index( $_, 'msg=audit(' ) + 10;
56 105         106 my $msg_end = index( $_, ':' );
57 105         144 my $timestamp = substr( $_, $msg_start, $msg_end - $msg_start );
58 105 100 66     363 next if $options{older} && $timestamp > $options{older};
59 84 100 66     279 next if $options{newer} && $timestamp < $options{newer};
60              
61             # Snag CWDs
62 43 100       84 if ( index( $_, 'type=CWD' ) == 0 ) {
63 3         5 my $cwd_start = index( $_, 'cwd="' ) + 5;
64 3         4 my $cwd_end = index( $_, "\n" ) - 1;
65 3         5 $cwd = substr( $_, $cwd_start, $cwd_end - $cwd_start );
66 3         3 $line++;
67 3         15 next;
68             }
69              
70             # Replace GROUP SEPARATOR usage with simple spaces
71 40         131 s/[\x1D]/ /g;
72              
73             my %parsed = map {
74 1052         1421 my @out = split( /=/, $_ );
75 1052         1820 shift @out, join( '=', @out )
76 1052         1105 } grep { $_ } map {
77 40         233 my $subj = $_;
  1052         990  
78 1052         1225 $subj =~ s/"//g;
79 1052         1025 chomp $subj;
80 1052         1192 $subj
81             } split( / /, $_ );
82              
83 40         192 $line++;
84 40         59 $parsed{line} = $line;
85 40         51 $parsed{timestamp} = $timestamp;
86 40         47 $parsed{cwd} = $cwd;
87 40   66     81 $parsed{exe} //= $exe;
88 40   66     83 $parsed{comm} //= $comm;
89 40   100     96 $parsed{res} //= $res;
90              
91 40 100 66     130 if ( exists $options{key} && $parsed{type} eq 'SYSCALL' ) {
92 19         54 $in_block = $parsed{key} =~ $options{key};
93 19         23 $exe = $parsed{exe};
94 19         20 $comm = $parsed{comm};
95 19         25 $res = lc( $parsed{success} ) eq 'yes';
96 19         22 $cwd = '';
97 19 100       133 next unless $in_block;
98             }
99              
100             # Check constraints BEFORE filtering returned values, this is a WHERE clause
101 24         57 CONSTRAINT: foreach my $constraint ( keys(%options) ) {
102 37 100       51 next CONSTRAINT if !exists $parsed{$constraint};
103 31 100       287 next LINE if $parsed{$constraint} !~ $options{$constraint};
104             }
105              
106             # Filter fields for RETURNING clause
107 2 50       4 if ( @{ $self->{returning} } ) {
  2         6  
108 2         11 foreach my $field ( keys(%parsed) ) {
109             delete $parsed{$field}
110 48 100       45 unless grep { $field eq $_ } @{ $self->{returning} };
  432         526  
  48         59  
111             }
112             }
113 2         11 push( @$ret, \%parsed );
114             }
115 1         11 close($fh);
116 1         9 return $ret;
117             }
118              
119             sub file_changes(&@) {
120 0     0 1   my ( $block, @dirs ) = @_;
121 0           my %rules;
122              
123             # Instruct auditctl to add UUID based rules
124 0           foreach my $dir (@dirs) {
125 0           $rules{$dir} = UUID::Tiny::create_uuid_as_string( UUID::Tiny::UUID_V1,
126             UUID::Tiny::UUID_NS_DNS );
127              
128             #TODO handle errors, etc
129 0           system( qw[auditctl -w], $dir, qw[-p rw -k], $rules{$dir} );
130             }
131              
132 0           $block->();
133              
134             # Unload the rule, flush the log
135 0           foreach my $dir (@dirs) {
136              
137             #TODO errors, flush
138 0           system( qw[auditctl -W], $dir );
139             }
140              
141             # Grab events
142 0           my $parser = Audit::Log->new( 'ausearch', qw{name cwd} );
143              
144             # TODO support arrayref
145 0           my $entries = $parser->search( 'key' => [ values(%rules) ] );
146             return uniq
147 0 0         map { $_->{name} =~ m/^\// ? $_->{name} : "$_->{cwd}/$_->{name}" }
  0            
148             @$entries;
149             }
150              
151             1;
152              
153             __END__