You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
5.0 KiB
165 lines
5.0 KiB
#!/usr/bin/perl -w
|
|
|
|
use IO::Socket;
|
|
use Getopt::Long;
|
|
use File::Basename;
|
|
use File::Path qw(make_path);
|
|
use IO::Handle;
|
|
|
|
my $maxlen = 16384;
|
|
my $port = 514;
|
|
my $maxlines = 10000;
|
|
my $logdir = '/run/cs-gelf-server/';
|
|
|
|
GetOptions(
|
|
"port=i" => \$port,
|
|
"maxlines=i" => \$maxlines,
|
|
"logdir=s" => \$logdir
|
|
);
|
|
|
|
if ($port !~ /^\d+$/ or $port < 1 or $port > 65535){
|
|
die "Invalid port $port\n";
|
|
}
|
|
if ($maxlines !~ /^\d+/ or $maxlines < 10){
|
|
die "Invalid max line specified\n";
|
|
}
|
|
if (not -d $logdir){
|
|
die "$logdir doesn't exists or is not a directory\n";
|
|
}
|
|
|
|
# Remove trailing / of the logdir, it's not nice in the logs when you have double /
|
|
$logdir =~ s/\/$//;
|
|
|
|
# List of syslog_identifier we're not intersted in
|
|
my @ignored_syslog_id = qw(
|
|
c-icap
|
|
charon
|
|
unbound
|
|
sudo
|
|
zed
|
|
zimbramon
|
|
);
|
|
# List of log files we're not interested in
|
|
my @ignored_log_files = qw(
|
|
/var/log/audit/audit.log
|
|
/var/log/squid/cache.log
|
|
/var/log/ufdbGuard/ufdbguardd.log
|
|
/opt/zimbra/log/gc.log
|
|
);
|
|
|
|
print "Start listening on UDP port $port\n";
|
|
$sock = IO::Socket::INET->new(
|
|
LocalPort => $port,
|
|
Proto => 'udp'
|
|
) or die("Socket: $@");
|
|
|
|
my $buf;
|
|
my $cnt = {};
|
|
my $loghandles = {};
|
|
|
|
while (1) {
|
|
$sock->recv($buf, $maxlen);
|
|
my ($port, $ipaddr) = sockaddr_in($sock->peername);
|
|
my $fields = {};
|
|
|
|
# We're not really interested in CEF headers. So let's extract
|
|
# the various fields
|
|
$buf =~ m/(?:(?:CEF:\d+\|)(?:[^=\\]+\|)+)(.*)/;
|
|
my $ext = $1;
|
|
|
|
# Taken from https://github.com/DavidJBianco/pycef
|
|
while ($ext =~ m/([^=\s]+)=((?:[\\]=|[^=])+)(?:\s|$)/g) {
|
|
$fields->{$1} = $2;
|
|
# Unescape value string
|
|
$fields->{$1} =~ s/\\=/=/g;
|
|
}
|
|
|
|
# Skip lines we're not interested in early.
|
|
# So crowdsec will eat less CPU parsing useless stuff
|
|
if (
|
|
defined $fields->{syslog_identifier} and grep { $_ eq $fields->{syslog_identifier} } @ignored_syslog_id or
|
|
defined $fields->{log_file_path} and grep { $_ eq $fields->{log_file_path} } @ignored_log_files
|
|
) {
|
|
next;
|
|
}
|
|
|
|
# We need a timestamp, a source and a msg at least
|
|
if (not defined $fields->{timestamp} or not defined $fields->{source} or not defined $fields->{msg}){
|
|
next;
|
|
}
|
|
|
|
my $msg;
|
|
# Default log will be syslog
|
|
my $logfile = $logdir . '/syslog.log';
|
|
|
|
# But for some services, we need special handling. Eg for web access logs
|
|
if (defined $fields->{event_dataset}){
|
|
if ($fields->{event_dataset} =~ m/^nginx\.(access|ingress_controller)/){
|
|
$logfile = $logdir . '/nginx/access.log';
|
|
$msg = $fields->{msg};
|
|
} elsif ($fields->{event_dataset} =~ m/^nginx\.error/){
|
|
$logfile = $logdir . '/nginx/error.log';
|
|
$msg = $fields->{msg};
|
|
} elsif ($fields->{event_dataset} =~ m/^apache\.access/){
|
|
$logfile = $logdir . '/httpd/access.log';
|
|
$msg = $fields->{msg};
|
|
} elsif ($fields->{event_dataset} =~ m/^apache\.error/){
|
|
$logfile = $logdir . '/httpd/access.log';
|
|
$msg = $fields->{msg};
|
|
}
|
|
} elsif (defined $fields->{log_file_path}){
|
|
if ($fields->{log_file_path} eq '/var/log/pveproxy/access.log'){
|
|
$logfile = $logdir . '/pveproxy/access.log';
|
|
$msg = $fields->{msg};
|
|
} elsif ($fields->{log_file_path} eq '/var/log/squid/access.log'){
|
|
$logfile = $logdir . '/squid/access.log';
|
|
$msg = $fields->{msg};
|
|
} elsif ($fields->{log_file_path} eq '/opt/zimbra/log/nginx.access.log'){
|
|
$logfile = $logdir . '/nginx/access.log';
|
|
$msg = $fields->{msg};
|
|
}
|
|
} elsif (defined $fields->{application_name}){
|
|
if ($fields->{application_name} eq 'nginx'){
|
|
$logfile = $logdir . '/nginx/access.log';
|
|
$msg = $fields->{msg};
|
|
}
|
|
}
|
|
|
|
# OK, no special handling (else $msg would be defined), so let's
|
|
# provide a syslog format
|
|
if (not defined $msg){
|
|
$msg .= $fields->{timestamp} . ' ' . $fields->{source} . ' ';
|
|
my $id = $fields->{syslog_identifier} || $fields->{program} || $fields->{application_name} || $fields->{process_name} || 'unknown';
|
|
# For older PfSense, which sent invalid syslog messages, we might extract
|
|
# the syslog identifier from the begining of the message
|
|
if ($id eq 'unknown' and $fields->{msg} =~ m/^(\w+(\[\d+\])?):\s(.*)/){
|
|
$id = $1;
|
|
$fields->{msg} = $3;
|
|
}
|
|
$msg .= $id;
|
|
# Try to append the pid of the process
|
|
if ($id ne 'kernel' and $id ne 'filterlog' and $id !~ m/\[\d+\]$/){
|
|
$msg .= '[';
|
|
$msg .= $fields->{process_pid} || $fields->{process_id} || $fields->{pid} || '0';
|
|
$msg .= ']';
|
|
}
|
|
$msg .= ': ' . $fields->{msg};
|
|
}
|
|
|
|
# Create the log sub dir if needed
|
|
my $dir = dirname($logfile);
|
|
if (not -d $dir){
|
|
make_path($dir);
|
|
}
|
|
|
|
defined $loghandles->{$logfile} or open($loghandles->{$logfile}, ">>", $logfile);
|
|
# Truncate the file so it's not growing too large
|
|
# Crowdsec will read it in nearly real time anyway
|
|
if ($cnt->{$logfile}++ > $maxlines){
|
|
print "Truncating $logfile\n";
|
|
truncate $loghandles->{$logfile}, 0;
|
|
$cnt->{$logfile} = 0;
|
|
}
|
|
print { $loghandles->{$logfile} } $msg . "\n";
|
|
$loghandles->{$logfile}->flush;
|
|
};
|
|
|