0

The maldet / Rfxn Linux MalDetect docs give this for getting an email report even when nothing was found:

-e, --report SCANID email
   View scan report of most recent scan or of a specific SCANID and optionally
   e-mail the report to a supplied e-mail address
   e.g: maldet --report
   e.g: maldet --report list
   e.g: maldet --report 050910-1534.21135
   e.g: maldet --report SCANID user@domain.com

All very straightforward, but I'm not sure how can I pass an email address as the second argument here while allowing the first argument (the scan ID) to fall back to its default, so that maldet emails whatever the latest report is to this custom email address. I'd like to be able to use this (for example in cron) to periodically check that Maldet is scanning and able to send email reports as expected.

I've tried maldet --report "" user@domain.com based on the standard ways of passing an empty variable in bash, but it ignores it and outputs what looks like an empty report into the console.

I've also tried things like maldet --report 0 user@domain.com and maldet --report " " user@domain.com but it responds {report} no report found, aborting.

Environment is Centos in case that's relevant.

5 Answers5

2

Sorry to resurrect an old question. I had the same issue trying to get maldet to email a report after every scan. I dug into the source code as suggested by @Tilman. The function responsible for sending report emails, view_report(), can be found in /usr/local/maldetect/internals/functions lines 645-706 in v1.6.4. Looking specifically at the code responsible (lines 681-696) we see mail is only sent if a SCANID, stored as $rid, is the suffix, ie 190429-0343.31494, corresponds to the filename of a report that is present in /usr/local/maldetect/sess/ such as session.190429-0343.31494

if [ -f "$sessdir/session.$rid" ] && [ ! -z "$(echo $2 | grep '\@')" ]; then
    if [ -f "$mail" ]; then
        cat $sessdir/session.$rid | $mail -s "$email_subj" "$2"
    elif [ -f "$sendmail" ]; then
        if ! grep -q "SUBJECT: " "$sessdir/session.$rid"; then
            echo -e "SUBJECT: $email_subj\n$(cat $sessdir/session.$rid)" > $sessdir/session.$rid
        fi
        cat $sessdir/session.$rid | $sendmail -t "$2"
    else
        eout "{scan} no \$mail or \$sendmail binaries found, e-mail alerts disabled."
        exit
    fi

    eout "{report} report ID $rid sent to $2" 1
    exit
fi

The code that handles an empty SCANID immediatly follows this (lines 697-705):

if [ "$rid" == "" ] && [ -f "$sessdir/session.last" ]; then
    rid=`cat $sessdir/session.last`
    $EDITOR $sessdir/session.$rid
elif [ -f "$sessdir/session.$rid" ]; then
    $EDITOR $sessdir/session.$rid
else
    echo "{report} no report found, aborting."
    exit
fi

I would have thought the code handling an empty SCANID would simply grab the most recent one and email it. What it actually does is look at /usr/local/maldetect/sess/session.last where maldet stores the most recent SCANID. And for some reason, it then opens the corresponding report in the terminal editor rather than just printing it out. Note that there actually isn't any working code to email the most recent report.

-- Updated Fix - May 5th, 2019--

Since preventing LMD from performing integrity checks, as required in my original fix, is a potential security risk, I created an alternative solution using LMD's custom.cron. The benefit is integrity checks remain in place and the email script should persist through updates. You don't need to touch LMD internal files or maldet daily cron.

Ensure $email_alert="1" and $email_addr= is set to at least one proper email address in /usr/local/maldetect/conf.maldet. Then add the following to /usr/local/maldetect/cron/custom.cron and it will run automatically at the end of the maldet daily cron:

##
# Please use this file for preservation of custom LMD execution code for the daily cronjob.
# NOTE: scripts in this file are called at the end of maldet daily cron as $custom_cron_exec
##

# log_cron="1" enable logging, log_cron="0" disable logging 
# applies only to the code in this file
log_cron="1"

# logging function borrowed from /maldetect/internals/functions
eout() {
    if [ "$log_cron" == "1" ]; then
        msg="$1"
        stdout="$2"
        appn=maldet
        if [ ! -d "$logdir" ]; then
            mkdir -p $logdir ; chmod 700 $logdir
        fi
        if [ ! -f "$maldet_log" ]; then
            touch $maldet_log
        fi
        log_size=`$wc -l $maldet_log | awk '{print$1}'`
        if [ "$log_size" -ge "20000" ]; then
            trim=1000
            printf "%s\n" "$trim,${log_size}d" w | ed -s $maldet_log 2> /dev/null
        fi
        if [ ! "$msg" == "" ]; then
            echo "$(date +"%b %d %H:%M:%S") $(hostname -s) $appn($$): $msg" >> $maldet_log
            if [ ! -z "$stdout" ]; then
                echo "$appn($$): $msg"
            fi
        fi
    fi
}

eout "{cron} running $cron_custom_exec"

##
# LMD Daily Email v1.0.0
# Author: kdub Email: kdubdev@gmail.com Date: May 5th, 2019
# https://github.com/kdubdev/linux-malware-detect/blob/master/files/cron/custom.cron
# Script to send email of newest report after daily scan. More info:
# https://serverfault.com/questions/805158/how-to-get-an-email-report-of-whatever-the-most-recent-maldet-scan-is
# #
de_version='v1.0.0'
eout "{cron} starting LMD Cron Email $de_version"
eout "{cron} $intcnf shows email_alert=$email_alert email_addr=$email_addr"

# Default email subject defined in /usr/local/maldetect/internals/internals.conf
# is email_subj="maldet alert from $(hostname)"
# comment this line to use the default email_subj or change to what you want
printf -v email_subj '[%s] %s: Scan Report' "$(hostname)" "$appn($$)"

# uncomment email_addr below to override recipients. Separate multiple emails with ,
# use $email_addr to include recipient defined in /usr/local/maldetect/conf.maldet
# email_addr="$email_addr,second@domain.tld,third@domain.tld"

# this is the email text inserted before the report
body_intro="Here are the results of the latest LMD scan:"
# this is the email text inserted after the report
printf -v body_footer "Email provided by LMD Cron Email %s\nCron file: %s\nLog file: %s" "$de_version" "$cron_custom_exec" "$maldet_log"

# this is a very weak email validation, just looking for @
if [ "$email_alert" == "1" ] && [ ! -z "$(echo $email_addr | grep '\@')" ]; then
# email_alert is true and email provided, send newest report
    if [ -f "$sessdir/session.last" ]; then    
        # Get most recent scan id
        rid=$(cat "$sessdir/session.last")
        if [ ! -z "$rid" ]; then
            # session.list contains something
            if [ -f "$sessdir/session.$rid" ]; then
                # report exists, get contents
                body=$(cat "$sessdir/session.$rid")
                eout "{cron} reading report $sessdir/session.$rid"
            else
                # report doesn't exist   
                body="{cron} unable to find report $sessdir/session.$rid."
            fi
            if [ -z "$body" ]; then
                # report file exists but is empty
                body="{cron} report $sessdir/session.$rid is empty."
            fi          
        else
            # session.last is empty  
            body="{cron} $sessdir/session.last is empty."
        fi
    else    
        # session.last doesn't exist
        body="{cron} unable to find $sessdir/session.last."
    fi
    # log if body starts with {cron} ie there's a problem reading report
    if [[ $body == '{cron}'* ]]; then
        eout "$body"
    fi

    # add intro and footer to body
    body=$(printf "%s\n\n%s\n\n%s\n\n" "$body_intro" "$body" "$body_footer")

    if [ -f "$mail" ]; then
        printf "%s" "$body" | $mail -s "$email_subj" "$email_addr"
        eout "{cron} mail sent using $mail to $email_addr, subject: $email_subj."
    elif [ -f "$sendmail" ]; then
        printf "%s\n%s" "$email_subj" "$body" | $sendmail -t "$email_addr"
        eout "{cron} mail sent using $sendmail to $email_addr, subject: $email_subj."
    fi
fi
eout "{cron} mail latest report finished."
eout "{cron} done running $cron_custom_exec"

You can check for updates here as well https://github.com/kdubdev/linux-malware-detect/blob/master/files/cron/custom.cron

In the script you can disable logging, override the email subject and/or recipients, and customize the email body intro and footer. The script is heavily commented so you can follow along or make changes.

I welcome any feedback or suggestions for improvement.

-- Original Fix Below --

To fix this and add other improvements, I modified view_report() with following changes:

  • added option 'newest' as alias to --report and --report "" to allow $ maldet --report newest user@domain.com
  • properly email most recent report when using $ maldet --report newest user@domain.com or $ maldet --report "" user@domain.com
  • change from unnecessarily using editor to view reports, instead simply print to terminal
  • improved logging

FIRST: You need to set autoupdate_version_hashed="0" in /usr/local/maldetect/conf.maldet to prevent LMD from automatically overwriting any changes you make when it runs the update check. Be aware that this is a potential security issue:

# This controls validating the LMD executable MD5 hash with known
# good upstream hash value. This allows LMD to replace the the
# executable / force a reinstallation in the event the LMD executable
# is tampered with or corrupted. If you intend to make customizations
# to the LMD executable, you should disable this feature.
# [0 = disabled, 1 = enabled]
autoupdate_version_hashed="0"

SECOND: Replace your current view_report() /usr/local/maldetect/internals/functions (lines 645-706) with this:

view_report() {
    # $1 is first arg passed from command line ex. $ maldet --report $1 $2
    rid="$1"
    # $ maldet --report list
    if [ "$rid" == "list" ]; then
        tmpf="$tmpdir/.areps$$"
        for file in `ls $sessdir/session.[0-9]* 2> /dev/null`; do
            SCANID=`cat $file | grep "SCAN ID" | sed 's/SCAN ID/SCANID/'`
            FILES=`cat $file | grep "TOTAL FILES" | sed 's/TOTAL //'`
            HITS=`cat $file | grep "TOTAL HITS" | sed 's/TOTAL //'`
            CLEAN=`cat $file | grep "TOTAL CLEANED" | sed 's/TOTAL //'`
            TIME=`cat $file | grep -E "^TIME|^STARTED" | sed -e 's/TIME: //' -e 's/STARTED: //' | awk '{print$1,$2,$3,$4}'`
            TIME_U=`date -d "$TIME" "+%s" 2> /dev/null`
                        ETIME=`cat $file | grep "ELAPSED" | awk '{print$1,$2}' | sed 's/ELAPSED/RUNTIME/'`
            if [ -z "$ETIME" ]; then
                ETIME="RUNTIME: unknown"
            fi
            if [ ! -z "$SCANID" ] && [ ! -z "$TIME" ]; then
                clean_zero=`echo $CLEAN | awk '{print$2}'`
                if [ -z "$clean_zero" ]; then
                    CLEAN="CLEANED:  0"
                fi
                echo "$TIME_U | $TIME | $SCANID | $ETIME | $FILES | $HITS | $CLEAN" >> $tmpf
            fi
        done
        if [ -f "$tmpf" ]; then
            if [ "$OSTYPE" == "FreeBSD" ]; then
                cat $tmpf | sort -k1 -n | cut -d'|' -f2-7 | column -t | more
            else
                cat $tmpf | sort -k1 -n | tac | cut -d'|' -f2-7 | column -t | more
            fi
            rm -f $tmpf 2> /dev/null
            exit 0
        else
            eout  "{list} unable to find report data for list, check \$sessdir"
            exit 1
        fi
    fi
    # If no SCANID is provided or "recent" then set $rid to most recent. 
    # $ maldet --report "" or $maldet --report newest
    if { [ "$rid" == "" ] || [ "$rid" == "newest" ]; } && [ -f "$sessdir/session.last" ]; then
        rid=`cat $sessdir/session.last`
    fi
    # make sure report exists
    if [ -f "$sessdir/session.$rid" ]; then
        # if email is provided, then send the report and exit
        if [ ! -z "$(echo $2 | grep '\@')" ]; then
            if [ -f "$mail" ]; then
                cat $sessdir/session.$rid | $mail -s "$email_subj" "$2"
            elif [ -f "$sendmail" ]; then
                if ! grep -q "SUBJECT: " "$sessdir/session.$rid"; then
                    echo -e "SUBJECT: $email_subj\n$(cat $sessdir/session.$rid)" > $sessdir/session.$rid
                fi
                cat $sessdir/session.$rid | $sendmail -t "$2"
            else
                # eout is an internal function to log to maldet_log and echo
                eout "{scan} no \$mail or \$sendmail binaries found, e-mail alerts disabled."
                exit
            fi
            eout "{report} report ID $rid sent to $2" 1
            exit        
        # no email is provided so show report and exit
        else
            printf '%b\n' "$(cat $sessdir/session.$rid)"
            exit
        fi
    # can't find requested report so log & echo error
    else
        eout "{report} unable to find report session.\$rid, aborting."
        exit
    fi
}

You can find the entire updated /usr/local/maldetect/internals/functions file in a pull request here as well: https://github.com/kdubdev/linux-malware-detect/blob/patch-1/files/internals/functions

LAST: Add the following line to the end of /etc/cron.daily/maldet if you want to receive an email after each daily scan: $inspath/maldet --report newest user@domain.com

Note: if it wasn't clear you can use -e or --report interchangeably.

kdub
  • 21
1

The author of maldet either didn't provide that possibility or neglected to document it. It's impossible to guess from the outside. The best way forward is UTSL: look up in the program source code how it handles the -e option and whether there's a way to embark the "most recent scan" branch and activate the E-mail option at the same time.

1

You should edit your /usr/local/maldetect/conf.maldet and on line 22. Replace email_addr="your@domain.com" to a valid address.

edit:

I read the original post wrong, but this setting could help someone else.

0

My solution is based around finding the most recent scan report ID in the file /usr/local/maldetect/sess/session.last

A wrapper script like this is called from Cron

#!/bin/bash

recipient=${recipient:-helpdesk@mydomain.co.za}

Upgrade Maldet and Update the signatures

maldet -d maldet -u

Perform a scan on files created in the last week

maldet -r / 7 logger -t "system_scan" "Maldet Task ended with RC=$?"

Send the last scan via email

maldet -e $(cat /usr/local/maldetect/sess/session.last) "$recipient"

Note the "recipient" will provide a default but the user can override it, eg:
recipient=me@mydomain.com ./scan.sh

Some Gotchas:

  1. Check that the location of the session.last file is valid!! I'm not sure that this is a documented feature and as such it could change
  2. If more than one scan runs the results may be unpredictable. Parsing the results from maldet -e list to get a list of reports could be used in stead.
  3. I did not try to handle errors on any steps, nor to capture error output, etc.
0

Here's the correct way to do this: maldet -e reportID email

You do not need the --report switch