Stopping the Evil Grinch: A Holiday Defense Guide
During a December evening in Santa's workshop, the security team received an urgent alert: a malicious actor, known as the "Evil Grinch," intended to compromise the systems running the toy production environment. Fortunately, the team already relied on several essential security tools:
Lynis for system auditing
ClamAV for malware scanning
Perl to orchestrate everything
Installing Lynis Without Sudo
Because the environment restricted sudo usage, Lynis was installed in the user's home directory using the following commands:
wget https://downloads.cisofy.com/lynis/lynis-3.1.6.tar.gz
tar -xvf lynis-3.1.6.tar.gz
mkdir -p /home/user/bin
chmod 775 /home/user/bin
With this wrapper script, Lynis was executable without requiring sudo privileges.
cat << 'EOF' > /home/user/bin/lynis
#!/bin/bash
LYNIS_DIR="/home/user/lynis-3.1.6"
export LYNIS_LOG_FILE="/home/user/lynis-logs/lynis.log"
export LYNIS_REPORT_FILE="/home/user/lynis-logs/lynis-report.dat"
cd "$LYNIS_DIR" || exit 1
./lynis "$@"
EOF
chmod 775 /home/user/bin/lynis
Installing ClamAV
ClamAV was installed using the system package manager:
sudo apt update
sudo apt install clamav clamav-daemon -y
Building the Perl Cron Script
A Perl script was created to automate security reporting and deliver the results via email.
First, the necessary Perl dependencies were installed:
cpanm Moo \
Email::Sender::Simple \
Email::Sender::Transport::SMTP \
Email::MIME \
Try::Tiny \
Types::Standard \
IO::All \
DateTime \
Readonly \
Log::Log4perl
The script is meant to run daily and exits early if it is not the last day of the month, determined via:
DateTime->now()->is_last_day_of_month()
Logging is handled by the Log::Log4perl module. The core of the script is the run_report function, which deletes any previous report file and executes each command. Because ClamAV errors are expected when scanning protected or locked files, the ClamAV call is allowed to return a non-zero exit code.
Below is the Perl script:
#!/usr/bin/env perl
use strict;
use warnings;
use DateTime;
use Cwd;
use Readonly;
use Log::Log4perl qw(:easy);
use Dotenv;
Readonly::Scalar my $lynis_file => '/home/user/lynis-report.dat';
Readonly::Scalar my $clamav_file => '/home/user/clamav.log';
if ( DateTime->now()->is_last_day_of_month() ) {
my $logger = Log::Log4perl->easy_init(
{
level => $DEBUG,
file => ">>test.log"
}
);
$logger->debug('Last day of month detected. Preparing reports.');
my $env = Dotenv->load('.mail_env');
$logger->debug('Running Lynis report');
run_report(
[
'/home/user/bin/lynis', 'audit', 'system',
'--profile', '/home/user/custom.prf'
],
$lynis_file
);
$logger->debug('Running ClamAV report');
run_report(
[
'clamscan', '-r', '-i',
'--exclude-dir=^/proc',
'--exclude-dir=^/sys',
'--exclude-dir=^/dev',
'/home/user',
"--log=$clamav_file",
'-v'
],
$clamav_file
);
}
sub _execute_cmd {
my ( $cmd, $file ) = @_;
my $cmd_str = join ' ', @{$cmd};
if ( $file eq '/home/user/clamav.log' ) {
# Accept non-zero exit code for ClamAV
system( @{$cmd} );
}
else {
system( @{$cmd} ) == 0
or die "Cannot execute $cmd_str: $?";
}
return;
}
sub run_report {
my ( $cmd, $file ) = @_;
if ( -f $file ) {
unlink $file;
}
_execute_cmd( $cmd, $file );
return;
}
To send reports securely, a Gmail App Password was stored in a protected file:
chmod 600 .mail_env
Then the SendMail class was implemented inside lib/SendMail.pm. The class defines the following attributes:
has sasl_username => ( is => 'ro', required => 1, isa => Str );
has sasl_password => ( is => 'ro', required => 1, isa => Str );
has from => ( is => 'ro', required => 1, isa => Str );
has to => ( is => 'ro', required => 1, isa => ArrayRef );
has email_body => ( is => 'ro', required => 1, isa => Str );
has attachments => ( is => 'ro', required => 1, isa => ArrayRef );
has subject => ( is => 'ro', required => 1, isa => Str );
which are used internally to create an Email::MIME object and an Email::Sender::Transport::SMTP object, stored in:
has message => ( is => 'ro', lazy => 1, isa => Object, builder => '_build_message' );
has transport => ( is => 'ro', lazy => 1, isa => Object, builder => '_build_transport' );
attributes. Finally, the class implements the send_email method, a simple wrapper over Email::Sender::Simple::sendmail.
The full body of the class is below:
package SendMail;
use Moo;
use Email::Sender::Simple qw(sendmail);
use Email::Sender::Transport::SMTP;
use Email::MIME;
use Try::Tiny;
use Types::Standard qw(Str ArrayRef Object);
use IO::All;
has sasl_username => ( is => 'ro', required => 1, isa => Str );
has sasl_password => ( is => 'ro', required => 1, isa => Str );
has from => ( is => 'ro', required => 1, isa => Str );
has to => ( is => 'ro', required => 1, isa => ArrayRef );
has email_body => ( is => 'ro', required => 1, isa => Str );
has attachments => ( is => 'ro', required => 1, isa => ArrayRef );
has subject => ( is => 'ro', required => 1, isa => Str );
has message => (
is => 'ro',
lazy => 1,
isa => Object,
builder => '_build_message'
);
has transport => (
is => 'ro',
lazy => 1,
isa => Object,
builder => '_build_transport'
);
sub _build_message {
my $self = shift;
my @file_list;
foreach my $file ( @{ $self->attachments } ) {
my @file_parts = split '/', $file;
my $mime = Email::MIME->create(
attributes => {
filename => $file_parts[-1],
content_type => "text/plain",
encoding => "quoted-printable",
name => $file_parts[-1],
},
body => io($file)->binary->all,
);
push @file_list, $mime;
}
my @parts = (
@file_list,
Email::MIME->create(
attributes => {
content_type => "text/plain",
disposition => "attachment",
encoding => "quoted-printable",
charset => "US-ASCII",
},
body_str => $self->email_body,
),
);
return Email::MIME->create(
header_str => [
From => $self->from,
To => join( ',', @{ $self->to } ),
Subject => $self->subject,
],
parts => \@parts,
);
}
sub _build_transport {
my $self = shift;
return Email::Sender::Transport::SMTP->new(
{
host => 'smtp.gmail.com',
port => 465,
ssl => 1,
sasl_username => $self->sasl_username,
sasl_password => $self->sasl_password,
}
);
}
sub send_email {
my $self = shift;
try {
sendmail( $self->message, { transport => $self->transport } );
}
catch {
print "|$_|";
};
return 1;
}
The package was integrated into the main script as follows:
use lib 'lib';
use SendMail;
my $mailer = SendMail->new(
{
sasl_username => $env->{cron_mail},
sasl_password => $env->{cron_password},
from => $env->{cron_mail},
to => $to,
email_body => 'Security report attached.',
attachments => [ $lynis_file, $clamav_file ],
subject => 'Security Report',
}
);
Adding to Crontab
The monthly automation was scheduled as follows:
crontab -e
0 10 * * * cd /home/user/scripts && /home/user/perl5/perlbrew/bin/perlbrew exec --with perl-5.42.0 perl security_report.pl >/dev/null 2>&1
Happy auditing and secure coding!
- Previous
- Next