From b6fbf22d63de88b77d79cd0b1d2337e589cac6d7 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Tue, 23 Dec 2014 20:16:36 +0000 Subject: [PATCH] Add support for avast malware scanner. Bug 1033 Originally by Dominic Benson Rebased for current malware.c by JGH. Testing by Heiko Schlittermann --- doc/doc-docbook/spec.xfpt | 33 ++++++++++ doc/doc-txt/NewStuff | 2 + src/src/malware.c | 131 ++++++++++++++++++++++++++++++++++++-- src/src/readconf.c | 4 +- src/src/spam.c | 2 +- 5 files changed, 165 insertions(+), 7 deletions(-) diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index aece633af..460b1bfab 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -30353,6 +30353,39 @@ The usual list-parsing of the content (see &<>&) applies. The following scanner types are supported in this release: .vlist +.vitem &%avast%& +.cindex "virus scanners" "avast" +This is the scanner daemon of Avast. It has been tested with Avast Core +Security (currenty at version 1.1.7). +You can get a trial version at &url(http://www.avast.com) or for Linux +at &url(http://www.avast.com/linux-server-antivirus). +This scanner type takes one option, +which can be either a full path to a UNIX socket, +or host and port specifiers separated by white space. +The host may be a name or an IP address; the port is either a +single number or a pair of numbers with a dash between. +Any further options are given, on separate lines, +to the daemon as options before the main scan command. +For example: +.code +av_scanner = avast:/var/run/avast/scan.sock:FLAGS -fullfiles:SENSITIVITY -pup +av_scanner = avast:192.168.2.22 5036 +.endd +If you omit the argument, the default path +&_/var/run/avast/scan.sock_& +is used. +If you use a remote host, +you need to make Exim's spool directory available to it, +as the scanner is passed a file path, not file contents. +For information about available commands and their options you may use +.code +$ socat UNIX:/var/run/avast/scan.sock STDIO: + FLAGS + SENSITIVITY + PACK +.endd + + .vitem &%aveserver%& .cindex "virus scanners" "Kaspersky" This is the scanner daemon of Kaspersky Version 5. You can get a trial version diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff index 8cb2d0dbd..c371cb2dd 100644 --- a/doc/doc-txt/NewStuff +++ b/doc/doc-txt/NewStuff @@ -14,6 +14,8 @@ Version 4.86 2. New expansion items $config_file, $config_dir, containing the file and directory name of the main configuration file. Also $exim_version. + 3. New "malware=" support for Avast. + Version 4.85 ------------ diff --git a/src/src/malware.c b/src/src/malware.c index 93bcf8667..167f47f2c 100644 --- a/src/src/malware.c +++ b/src/src/malware.c @@ -11,7 +11,7 @@ #ifdef WITH_CONTENT_SCAN typedef enum {M_FPROTD, M_DRWEB, M_AVES, M_FSEC, M_KAVD, M_CMDL, - M_SOPHIE, M_CLAMD, M_SOCK, M_MKSD} scanner_t; + M_SOPHIE, M_CLAMD, M_SOCK, M_MKSD, M_AVAST} scanner_t; typedef enum {MC_NONE, MC_TCP, MC_UNIX, MC_STRM} contype_t; static struct scan { @@ -31,6 +31,7 @@ static struct scan { M_CLAMD, US"clamd", US"/tmp/clamd", MC_NONE }, { M_SOCK, US"sock", US"/tmp/malware.sock", MC_STRM }, { M_MKSD, US"mksd", NULL, MC_NONE }, + { M_AVAST, US"avast", US"/var/run/avast/scan.sock", MC_STRM }, { -1, NULL, NULL, MC_NONE } /* end-marker */ }; @@ -1527,7 +1528,128 @@ malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking) } break; } - } + case M_AVAST: /* "avast" scanner type ----------------------------------- */ + { + int ovector[1*3]; + uschar buf[1024]; + uschar * scanrequest; + const pcre * avast_clean_re, * avast_virus_re; + enum {AVA_HELO, AVA_OPT, AVA_RSP, AVA_DONE} avast_stage; + + /* According to Martin Tuma @avast the protocol uses "escaped + whitespace", that is, every embedded whitespace is backslash + escaped, as well as backslash is protected by backslash. + The returned lines contain the name of the scanned file, a tab + and the [ ] marker. + [+] - not infected + [L] - infected + [E] - some error occured + Such marker follows the first non-escaped TAB. */ + if ( !(avast_clean_re = + m_pcre_compile(US"(?!\\\\)\\t\\[\\+\\]", &errstr)) + || !(avast_virus_re = + m_pcre_compile(US"(?!\\\\)\\t\\[L\\]\\d\\.\\d\\t\\d\\s(.*)", + &errstr)) + ) + return malware_errlog_defer(errstr); + + /* wait for result */ + for (avast_stage = AVA_HELO; recv_line(sock, buf, sizeof(buf)) > 0; ) + { + int slen = Ustrlen(buf); + if (slen >= 1) + { + DEBUG(D_acl) debug_printf("got from avast: %s\n", buf); + switch (avast_stage) + { + case AVA_HELO: + if (Ustrncmp(buf, "220", 3) != 0) + goto endloop; /* require a 220 */ + goto sendreq; + + case AVA_OPT: + if (Ustrncmp(buf, "210", 3) == 0) + break; /* ignore 210 responses */ + if (Ustrncmp(buf, "200", 3) != 0) + goto endloop; /* require a 200 */ + + sendreq: + { + int len; + /* Check for another option to send. Newline-terminate it. */ + if ((scanrequest = string_nextinlist(&av_scanner_work, &sep, + NULL, 0))) + { + scanrequest = string_sprintf("%s\n", scanrequest); + avast_stage = AVA_OPT; /* just sent option */ + } + else + { + scanrequest = string_sprintf("SCAN %s/scan/%s\n", + spool_directory, message_id); + avast_stage = AVA_RSP; /* just sent command */ + } + + /* send config-cmd or scan-request to socket */ + len = Ustrlen(scanrequest); + if (send(sock, scanrequest, len, 0) < 0) + { + scanrequest[len-1] = '\0'; + return m_errlog_defer_3(scanent, string_sprintf( + "unable to send request '%s' to socket (%s): %s", + scanrequest, scanner_options, strerror(errno)), sock); + } + break; + } + + case AVA_RSP: + if (Ustrncmp(buf, "210", 3) == 0) + break; /* ignore the "210 SCAN DATA" message */ + + if (pcre_exec(avast_clean_re, NULL, CS buf, slen, + 0, 0, ovector, nelements(ovector)) > 0) + break; + + if ((malware_name = m_pcre_exec(avast_virus_re, buf))) + { /* remove backslash in front of [whitespace|backslash] */ + uschar * p, * p0; + for (p = malware_name; *p; ++p) + if (*p == '\\' && (isspace(p[1]) || p[1] == '\\')) + for (p0 = p; *p0; ++p0) *p0 = p0[1]; + + avast_stage = AVA_DONE; + goto endloop; + } + + if (Ustrncmp(buf, "200 SCAN OK", 11) == 0) + { /* we're done finally */ + if (send(sock, "QUIT\n", 5, 0) < 0) /* courtesy */ + return m_errlog_defer_3(scanent, string_sprintf( + "unable to send quit request to socket (%s): %s", + scanner_options, strerror(errno)), + sock); + malware_name = NULL; + avast_stage = AVA_DONE; + goto endloop; + } + + /* here for any unexpected response from the scanner */ + goto endloop; + } + } + } + endloop: + + switch(avast_stage) + { + case AVA_HELO: + case AVA_OPT: + case AVA_RSP: return m_errlog_defer_3(scanent, string_sprintf( + "invalid response from scanner: %s\n", buf), sock); + default: break; + } + } + } /* scanner type switch */ if (sock >= 0) (void) close (sock); @@ -1535,10 +1657,11 @@ malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking) } /* match virus name against pattern (caseless ------->----------v) */ - if ( malware_name && (regex_match_and_setup(re, malware_name, 0, -1)) ) { + if (malware_name && regex_match_and_setup(re, malware_name, 0, -1)) + { DEBUG(D_acl) debug_printf("Matched regex to malware [%s] [%s]\n", malware_regex, malware_name); return OK; - } + } else return FAIL; } diff --git a/src/src/readconf.c b/src/src/readconf.c index 0b3778054..687b35223 100644 --- a/src/src/readconf.c +++ b/src/src/readconf.c @@ -3009,9 +3009,9 @@ if (config_file != NULL) uschar *p; config_filename = config_main_filename = string_copy(filename); - p = strrchr(filename, '/'); + p = Ustrrchr(filename, '/'); config_main_directory = p ? string_copyn(filename, p - filename) - : string_copy("."); + : string_copy(US"."); } else { diff --git a/src/src/spam.c b/src/src/spam.c index 45a06931c..c0c3fb373 100644 --- a/src/src/spam.c +++ b/src/src/spam.c @@ -48,7 +48,7 @@ spam(uschar **listptr) fd_set select_fd; #endif uschar *spamd_address_work; - static const char * loglabel = US"spam acl condition:"; + static const uschar * loglabel = US"spam acl condition:"; /* stop compiler warning */ result = 0; -- 2.25.1