2025 twenty-four merry days of Perl Feed

Stopping the Evil Grinch: A Holiday Defense Guide

System Security Auditing - 2025-12-04

Stopping the Evil Grinch

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!

Gravatar Image This article contributed by: Dragos Trif <drd.trif@gmail.com>