File Coverage

lib/Audit/Log.pm
Criterion Covered Total %
statement 79 95 83.1
branch 24 36 66.6
condition 13 18 72.2
subroutine 9 10 90.0
pod 3 3 100.0
total 128 162 79.0


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