/* cyr_virusscan.c - scan mailboxes for infected messages and remove them
*
* Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <config.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdlib.h>
#include <stdio.h>
#include <syslog.h>
#include <string.h>
#include <netinet/in.h>
/* cyrus includes */
#include "global.h"
#include "exitcodes.h"
#include "append.h"
#include "index.h"
#include "mailbox.h"
#include "message.h"
#include "xmalloc.h"
#include "mboxlist.h"
#include "prot.h"
#include "util.h"
#include "sync_log.h"
#include "times.h"
#include "xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include "imap/imap_err.h"
struct infected_msg {
char *mboxname;
char *virname;
char *msgid;
char *date;
char *from;
char *subj;
unsigned long uid;
struct infected_msg *next;
};
struct infected_mbox {
char *owner;
struct infected_msg *msgs;
struct infected_mbox *next;
};
struct scan_rock {
struct infected_mbox *i_mbox;
struct searchargs *searchargs;
struct index_state *idx_state;
uint32_t msgno;
char userid[MAX_MAILBOX_NAME];
int user_infected;
int total_infected;
int mailboxes_scanned;
};
/* globals for getopt routines */
extern char *optarg;
extern int optind;
extern int opterr;
extern int optopt;
/* globals for callback functions */
int disinfect = 0;
int email_notification = 0;
struct infected_mbox *public = NULL;
struct infected_mbox *user = NULL;
int verbose = 0;
/* abstract definition of a virus scan engine */
struct scan_engine {
const char *name;
void *state;
void *(*init)(void); /* initialize state */
int (*scanfile)(void *state, /* scan fname & return non-zero if infected */
const char *fname, const char **virname);
void (*destroy)(void *state); /* destroy state */
};
#ifdef HAVE_CLAMAV
/* ClamAV implementation */
#include <clamav.h>
struct clamav_state {
struct cl_engine *av_engine;
};
void *clamav_init()
{
unsigned int sigs = 0;
int r;
/* initialise ClamAV library */
if ((r = cl_init(0)) != CL_SUCCESS) {
syslog(LOG_ERR, "cl_init: %s", cl_strerror(r));
fatal("Failed to initialise ClamAV library", EC_SOFTWARE);
}
struct clamav_state *st = xzmalloc(sizeof(struct clamav_state));
if (st == NULL) {
fatal("memory allocation failed", EC_SOFTWARE);
}
st->av_engine = cl_engine_new();
if ( ! st->av_engine ) {
fatal("Failed to initialize AV engine", EC_SOFTWARE);
}
/* load all available databases from default directory */
if (verbose) puts("Loading virus signatures...");
if ((r = cl_load(cl_retdbdir(), st->av_engine, &sigs, CL_DB_STDOPT))) {
syslog(LOG_ERR, "cl_load: %s", cl_strerror(r));
fatal(cl_strerror(r), EC_SOFTWARE);
}
printf("Loaded %d virus signatures.\n", sigs);
/* build av_engine */
if ((r = cl_engine_compile(st->av_engine))) {
syslog(LOG_ERR,
"Database initialization error: %s", cl_strerror(r));
cl_engine_free(st->av_engine);
fatal(cl_strerror(r), EC_SOFTWARE);
}
/* set up archive av_limits */
/* max files */
cl_engine_set_num(st->av_engine, CL_ENGINE_MAX_FILES, 10000);
/* during the scanning of archives, this size (100 MB) will
* never be exceeded
*/
cl_engine_set_num(st->av_engine, CL_ENGINE_MAX_SCANSIZE, 100 * 1048576);
/* compressed files will only be decompressed and scanned up to
* this size (10 MB)
*/
cl_engine_set_num(st->av_engine, CL_ENGINE_MAX_FILESIZE, 10 * 1048576);
/* maximum recursion level for archives */
cl_engine_set_num(st->av_engine, CL_ENGINE_MAX_RECURSION, 16);
return (void *) st;
}
int clamav_scanfile(void *state, const char *fname,
const char **virname)
{
struct clamav_state *st = (struct clamav_state *) state;
int r;
/* scan file */
#ifdef CL_SCAN_STDOPT
r = cl_scanfile(fname, virname, NULL, st->av_engine,
CL_SCAN_STDOPT);
#else
static struct cl_scan_options options;
memset(&options, 0, sizeof(struct cl_scan_options));
options.parse |= ~0; /* enable all parsers */
r = cl_scanfile(fname, virname, NULL, st->av_engine, &options);
#endif
switch (r) {
case CL_CLEAN:
/* do nothing */
break;
case CL_VIRUS:
return 1;
break;
default:
printf("cl_scanfile error: %s\n", cl_strerror(r));
syslog(LOG_ERR, "cl_scanfile error: %s\n", cl_strerror(r));
break;
}
return 0;
}
void clamav_destroy(void *state)
{
struct clamav_state *st = (struct clamav_state *) state;
if (st->av_engine) {
/* free memory */
cl_engine_free(st->av_engine);
}
free(st);
}
struct scan_engine engine =
{ "ClamAV", NULL, &clamav_init, &clamav_scanfile, &clamav_destroy };
#elif defined(HAVE_SOME_UNKNOWN_VIRUS_SCANNER)
/* XXX Add other implementations here */
#else
/* NO configured virus scanner */
struct scan_engine engine = { "<None Configured>", NULL, NULL, NULL, NULL };
#endif
/* forward declarations */
int usage(char *name);
int scan_me(struct findall_data *, void *);
unsigned virus_check(struct mailbox *mailbox,
const struct index_record *record,
void *rock);
void append_notifications();
int main (int argc, char *argv[]) {
int option; /* getopt() returns an int */
char *alt_config = NULL;
char *search_str = NULL;
struct scan_rock srock;
while ((option = getopt(argc, argv, "C:s:rnv")) != EOF) {
switch (option) {
case 'C': /* alt config file */
alt_config = optarg;
break;
case 's': /* IMAP SEARCH string */
search_str = optarg;
break;
case 'r':
disinfect = 1;
break;
case 'n':
email_notification = 1;
break;
case 'v':
verbose ++;
break;
case 'h':
default: usage(argv[0]);
}
}
cyrus_init(alt_config, "cyr_virusscan", 0, CONFIG_NEED_PARTITION_DATA);
memset(&srock, 0, sizeof(struct scan_rock));
if (search_str) {
int r, c;
struct namespace scan_namespace;
struct protstream *scan_in = NULL;
struct protstream *scan_out = NULL;
scan_in = prot_readmap(search_str, strlen(search_str)+1); /* inc NUL */
scan_out = prot_new(2, 1);
/* Set namespace -- force standard (internal) */
if ((r = mboxname_init_namespace(&scan_namespace, 1)) != 0) {
syslog(LOG_ERR, "%s", error_message(r));
fatal(error_message(r), EC_CONFIG);
}
search_attr_init();
srock.searchargs = new_searchargs("*", GETSEARCH_CHARSET_KEYWORD,
&scan_namespace, NULL, NULL, 1);
c = get_search_program(scan_in, scan_out, srock.searchargs);
prot_free(scan_in);
prot_flush(scan_out);
prot_free(scan_out);
if (c == EOF) {
syslog(LOG_ERR, "Invalid search string");
fatal("Invalid search string", EC_USAGE);
}
}
else {
printf("Using %s virus scanner\n", engine.name);
if (engine.init) engine.state = engine.init();
}
mboxlist_init(0);
mboxlist_open(NULL);
/* open the quota db, we'll need it for expunge */
quotadb_init(0);
quotadb_open(NULL);
sync_log_init();
/* setup for mailbox event notifications */
mboxevent_init();
if (optind == argc) { /* do the whole partition */
mboxlist_findall(NULL, "*", 1, 0, 0, scan_me, &srock);
} else {
strarray_t *array = strarray_new();
for (; optind < argc; optind++) {
strarray_append(array, argv[optind]);
}
mboxlist_findallmulti(NULL, array, 1, 0, 0, scan_me, &srock);
strarray_free(array);
}
if (email_notification) append_notifications();
sync_log_done();
quotadb_close();
quotadb_done();
mboxlist_close();
mboxlist_done();
printf("\n%d mailboxes scanned, %d infected messages %s\n",
srock.mailboxes_scanned,
srock.total_infected,
disinfect ? "removed" : "found");
if (srock.searchargs) freesearchargs(srock.searchargs);
else if (engine.destroy) engine.destroy(engine.state);
cyrus_done();
return 0;
}
int usage(char *name)
{
printf("usage: %s [-C <alt_config>] [-s <imap-search-string>] [ -r [-n] ] [-v]\n"
"\t[mboxpattern1 ... [mboxpatternN]]\n", name);
printf("\tif no mboxpattern is given %s works on all mailboxes\n", name);
printf("\t -s imap-search-string Rather than scanning for viruses,\n"
"\t messages matching the search criteria will be treated as infected.\n"
"\t Useful for removing messages without a distinct signature, such as Phish.\n");
printf("\t -r remove infected messages\n");
printf("\t -n notify mailbox owner of deleted messages via email\n");
printf("\t -v verbose output\n");
exit(0);
}
static void print_header(void)
{
printf("\n%-40s\t%10s\t%6s\t%s\n",
"Mailbox Name", "Msg UID", "Status", "Virus Name");
printf("----------------------------------------\t"
"----------\t------\t"
"--------------------------------------------------\n");
}
int scan_me(struct findall_data *data, void *rock)
{
if (!data || !data->mbname) return 0;
struct mailbox *mailbox = NULL;
int r;
struct infected_mbox *i_mbox = NULL;
const char *name = mbname_intname(data->mbname);
const char *userid = mbname_userid(data->mbname);
struct scan_rock *srock = (struct scan_rock *) rock;
/* reset infected count when user changes, without choking
* on shared mailboxes, which don't have a user. */
if (userid != NULL && strcmp(srock->userid, userid) != 0) {
strlcpy(srock->userid, userid, sizeof(srock->userid));
srock->user_infected = 0;
}
else if (userid == NULL && *srock->userid != '\0') {
memset(srock->userid, 0, sizeof(srock->userid));
srock->user_infected = 0;
}
r = mailbox_open_iwl(name, &mailbox);
if (r) {
printf("failed to open %s (%s)\n", name, error_message(r));
return 0;
}
if (srock->searchargs) {
r = index_open_mailbox(mailbox, NULL, &srock->idx_state);
if (!r) r = mailbox_lock_index(mailbox, LOCK_EXCLUSIVE);
if (r) {
printf("failed to open index %s (%s)\n", name, error_message(r));
return 0;
}
search_expr_internalise(srock->idx_state, srock->searchargs->root);
srock->msgno = 1;
}
if (email_notification) {
char *owner = mboxname_to_userid(name);
if (owner) {
if (user && !strcmp(owner, user->owner)) {
i_mbox = user;
} else {
/* new owner (Inbox) */
struct infected_mbox *new = xzmalloc(sizeof(struct infected_mbox));
new->owner = xstrdup(owner);
new->next = user;
i_mbox = user = new;
}
free(owner);
}
#if 0 /* XXX what to do with public mailboxes (bboards)? */
else {
if (!public) {
public = xzmalloc(sizeof(struct infected_mbox));
public->owner = xstrdup("");
}
i_mbox = public;
}
#endif
}
srock->i_mbox = i_mbox;
if (verbose) printf("Scanning %s...\n", name);
mailbox_expunge(mailbox, virus_check, srock, NULL, EVENT_MESSAGE_EXPUNGE);
if (srock->idx_state) index_close(&srock->idx_state); /* closes mailbox */
else mailbox_close(&mailbox);
srock->mailboxes_scanned++;
return 0;
}
void create_digest(struct infected_mbox *i_mbox, struct mailbox *mailbox,
const struct index_record *record, const char *virname)
{
struct infected_msg *i_msg = xzmalloc(sizeof(struct infected_msg));
i_msg->mboxname = xstrdup(mailbox->name);
i_msg->virname = xstrdup(virname);
i_msg->uid = record->uid;
i_msg->msgid = mailbox_cache_get_env(mailbox, record, ENV_MSGID);
i_msg->date = mailbox_cache_get_env(mailbox, record, ENV_DATE);
i_msg->from = mailbox_cache_get_env(mailbox, record, ENV_FROM);
i_msg->subj = mailbox_cache_get_env(mailbox, record, ENV_SUBJECT);
i_msg->next = i_mbox->msgs;
i_mbox->msgs = i_msg;
}
/* thumbs up routine, checks for virus and returns yes or no for deletion */
/* 0 = no, 1 = yes */
unsigned virus_check(struct mailbox *mailbox,
const struct index_record *record,
void *deciderock)
{
struct scan_rock *srock = (struct scan_rock *) deciderock;
struct infected_mbox *i_mbox = srock->i_mbox;
const char *virname =
"Cyrus Administrator Targeted Removal (Phish, etc.)";
int r = 0;
if (srock->searchargs) {
/* run the search program against this message */
r = index_search_evaluate(srock->idx_state,
srock->searchargs->root, srock->msgno++);
}
else if (engine.scanfile) {
const char *fname = mailbox_record_fname(mailbox, record);
/* run the virus scanner against this message */
r = engine.scanfile(engine.state, fname, &virname);
}
if (r) {
/* print header if this is the first infection seen for this user */
if (verbose || !srock->user_infected) print_header();
printf("%-40s\t%10u\t%6s\t%s\n", mailbox->name, record->uid,
(record->system_flags & FLAG_SEEN) ? "READ" : "UNREAD",
virname);
srock->user_infected ++;
srock->total_infected ++;
if (disinfect) {
if (email_notification && i_mbox) {
create_digest(i_mbox, mailbox, record, virname);
}
}
else r = 0;
}
return r;
}
void append_notifications()
{
struct infected_mbox *i_mbox;
int outgoing_count = 0;
pid_t p = getpid();;
int fd = create_tempfile(config_getstring(IMAPOPT_TEMP_PATH));
while ((i_mbox = user)) {
if (i_mbox->msgs) {
FILE *f = fdopen(fd, "w+");
struct infected_msg *msg;
char buf[8192], datestr[RFC822_DATETIME_MAX+1];
time_t t;
struct protstream *pout;
struct appendstate as;
struct body *body = NULL;
long msgsize;
mbname_t *mbname = mbname_from_userid(i_mbox->owner);
fprintf(f, "Return-Path: <>\r\n");
t = time(NULL);
snprintf(buf, sizeof(buf), "<cmu-cyrus-%d-%d-%d@%s>",
(int) p, (int) t,
outgoing_count++, config_servername);
fprintf(f, "Message-ID: %s\r\n", buf);
time_to_rfc822(t, datestr, sizeof(datestr));
fprintf(f, "Date: %s\r\n", datestr);
fprintf(f, "From: Mail System Administrator <%s>\r\n",
config_getstring(IMAPOPT_POSTMASTER));
/* XXX Need to handle virtdomains */
fprintf(f, "To: <%s>\r\n", mbname_userid(mbname));
fprintf(f, "MIME-Version: 1.0\r\n");
fprintf(f, "Subject: Automatically deleted mail\r\n");
while ((msg = i_mbox->msgs)) {
fprintf(f, "\r\n\r\nThe following message was deleted from mailbox "
"'Inbox%s'\r\n", msg->mboxname+4); /* skip "user" */
fprintf(f, "because it was infected with virus '%s'\r\n\r\n",
msg->virname);
fprintf(f, "\tMessage-ID: %s\r\n", msg->msgid);
fprintf(f, "\tDate: %s\r\n", msg->date);
fprintf(f, "\tFrom: %s\r\n", msg->from);
fprintf(f, "\tSubject: %s\r\n", msg->subj);
fprintf(f, "\tIMAP UID: %lu\r\n", msg->uid);
i_mbox->msgs = msg->next;
/* free msg digest */
free(msg->mboxname);
free(msg->msgid);
free(msg->date);
free(msg->from);
free(msg->subj);
free(msg->virname);
free(msg);
}
fflush(f);
msgsize = ftell(f);
/* send MessageAppend event notification */
append_setup(&as, mbname_intname(mbname), NULL, NULL, 0, NULL, NULL, 0,
EVENT_MESSAGE_APPEND);
mbname_free(&mbname);
pout = prot_new(fd, 0);
prot_rewind(pout);
append_fromstream(&as, &body, pout, msgsize, t, NULL);
append_commit(&as);
if (body) {
message_free_body(body);
free(body);
}
prot_free(pout);
fclose(f);
}
user = i_mbox->next;
/* free owner info */
free(i_mbox->owner);
free(i_mbox);
}
}