/* 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.
*
* $Id: cyr_virusscan.c,v 1.9 2010/01/06 17:01:31 murch Exp $
*/
#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 "sysexits.h"
#include "exitcodes.h"
#include "append.h"
#include "imap_err.h"
#include "index.h"
#include "mailbox.h"
#include "xmalloc.h"
#include "mboxlist.h"
#include "prot.h"
#include "util.h"
#include "sync_log.h"
#include "rfc822date.h"
/* config.c stuff */
const int config_need_data = CONFIG_NEED_PARTITION_DATA;
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;
uint32_t recno; /* running count of which message we're scanning */
struct infected_msg *msgs;
struct infected_mbox *next;
};
/* globals for getopt routines */
extern char *optarg;
extern int optind;
extern int opterr;
extern int optopt;
/* globals for callback functions */
int disinfect = 0;
int notify = 0;
struct infected_mbox *public = NULL;
struct infected_mbox *user = NULL;
/* current namespace */
static struct namespace scan_namespace;
int verbose = 1;
/* 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 */
};
#define HAVE_CLAMAV
#ifdef HAVE_CLAMAV
/* ClamAV implementation */
#include <clamav.h>
struct clamav_state {
struct cl_engine *av_engine;
struct cl_limits av_limits;
};
void *clamav_init()
{
unsigned int sigs = 0;
int r;
struct clamav_state *st = xzmalloc(sizeof(struct clamav_state));
/* load all available databases from default directory */
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);
}
if (verbose) printf("Loaded %d virus signatures.\n", sigs);
/* build av_engine */
if((r = cl_build(st->av_engine))) {
syslog(LOG_ERR,
"Database initialization error: %s", cl_strerror(r));
cl_free(st->av_engine);
fatal(cl_strerror(r), EC_SOFTWARE);
}
/* set up archive av_limits */
st->av_limits.maxfiles = 10000; /* max files */
st->av_limits.maxscansize = 100 * 1048576; /* during the scanning of
* archives
* this size (100 MB) will never
* be exceeded
*/
st->av_limits.maxfilesize = 10 * 1048576; /* compressed files will only be
* decompressed and scanned up to
* this size (10 MB)
*/
st->av_limits.maxreclevel = 16; /* maximum recursion level for archives */
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 */
r = cl_scanfile(fname, virname, NULL, st->av_engine, &st->av_limits,
CL_SCAN_STDOPT);
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_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 = { NULL, NULL, NULL, NULL, NULL };
#endif
/* forward declarations */
int usage(char *name);
int scan_me(char *, int, int, void *);
unsigned virus_check(struct mailbox *mailbox,
struct index_record *record,
void *rock);
void append_notifications();
int main (int argc, char *argv[]) {
int option; /* getopt() returns an int */
char buf[MAX_MAILBOX_PATH+1];
char *alt_config = NULL;
int r;
if ((geteuid()) == 0 && (become_cyrus() != 0)) {
fatal("must run as the Cyrus user", EC_USAGE);
}
while ((option = getopt(argc, argv, "C:rn")) != EOF) {
switch (option) {
case 'C': /* alt config file */
alt_config = optarg;
break;
case 'r':
disinfect = 1;
break;
case 'n':
notify = 1;
break;
case 'h':
default: usage(argv[0]);
}
}
cyrus_init(alt_config, "cyr_virusscan", 0);
if (!engine.name) {
fatal("no virus scanner configured", EC_SOFTWARE);
} else {
if (verbose) printf("Using %s virus scanner\n", engine.name);
}
engine.state = engine.init();
/* 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);
}
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();
if (optind == argc) { /* do the whole partition */
strcpy(buf, "*");
(*scan_namespace.mboxlist_findall)(&scan_namespace, buf, 1, 0, 0,
scan_me, NULL);
} else {
for (; optind < argc; optind++) {
strncpy(buf, argv[optind], MAX_MAILBOX_BUFFER);
/* Translate any separators in mailboxname */
mboxname_hiersep_tointernal(&scan_namespace, buf,
config_virtdomains ?
strcspn(buf, "@") : 0);
(*scan_namespace.mboxlist_findall)(&scan_namespace, buf, 1, 0, 0,
scan_me, NULL);
}
}
if (notify) append_notifications();
sync_log_done();
quotadb_close();
quotadb_done();
mboxlist_close();
mboxlist_done();
engine.destroy(engine.state);
cyrus_done();
return 0;
}
int usage(char *name)
{
printf("usage: %s [-C <alt_config>] [ -r [-n] ]\n"
"\t[mboxpattern1 ... [mboxpatternN]]\n", name);
printf("\tif no mboxpattern is given %s works on all mailboxes\n", name);
printf("\t -r remove infected messages\n");
printf("\t -n notify mailbox owner of deleted messages via email\n");
exit(0);
}
/* we don't check what comes in on matchlen and maycreate, should we? */
int scan_me(char *name,
int matchlen __attribute__((unused)),
int maycreate __attribute__((unused)),
void *rock __attribute__((unused)))
{
struct mailbox *mailbox;
int r;
struct infected_mbox *i_mbox = NULL;
if (verbose) {
char mboxname[MAX_MAILBOX_BUFFER];
/* Convert internal name to external */
(*scan_namespace.mboxname_toexternal)(&scan_namespace, name,
"cyrus", mboxname);
printf("Working on %s...\n", mboxname);
}
r = mailbox_open_iwl(name, 0, &mailbox);
if (r) { /* did we find it? */
syslog(LOG_ERR, "Couldn't find %s, check spelling", name);
return 0;
}
if (notify) {
/* XXX Need to handle virtdomains */
if (!strncmp(name, "user.", 5)) {
size_t ownerlen;
if (user && (ownerlen = strlen(user->owner)) &&
!strncmp(name, user->owner, ownerlen) &&
(name[ownerlen] == '.' || name[ownerlen] =='\0')) {
/* mailbox belongs to current owner */
i_mbox = user;
} else {
/* new owner (Inbox) */
struct infected_mbox *new = xzmalloc(sizeof(struct infected_mbox));
new->owner = xstrdup(name);
new->next = user;
i_mbox = user = new;
}
}
#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
if (i_mbox) i_mbox->recno = 1;
}
mailbox_expunge(mailbox, virus_check, i_mbox, 0, NULL);
mailbox_commit(mailbox, 0);
mailbox_close(&mailbox);
return 0;
}
void create_digest(struct infected_mbox *i_mbox, struct mailbox *mbox,
uint32_t recno, unsigned long uid, const char *virname)
{
struct infected_msg *i_msg = xmalloc(sizeof(struct infected_msg));
struct nntp_overview *over;
i_msg->mboxname = xstrdup(mbox->name);
i_msg->virname = xstrdup(virname);
i_msg->uid= uid;
index_operatemailbox(mbox);
over = index_overview(mbox, recno);
i_msg->msgid = strdup(over->msgid);
i_msg->date = strdup(over->date);
i_msg->from = strdup(over->from);
i_msg->subj = strdup(over->subj);
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,
struct index_record *record,
void *deciderock)
{
struct infected_mbox *i_mbox = (struct infected_mbox *) deciderock;
char *fname;
const char *virname;
int r = 0;
fname = mailbox_message_fname(mailbox, record->uid);
if ((r = engine.scanfile(engine.state, fname, &virname))) {
if (verbose) {
printf("Virus detected in message %lu: %s\n", record->uid, virname);
}
if (disinfect) {
if (notify && i_mbox) {
create_digest(i_mbox, mailbox, i_mbox->recno, record->uid, virname);
}
}
}
if (i_mbox) i_mbox->recno++;
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+");
size_t ownerlen;
struct infected_msg *msg;
char buf[8192], datestr[80];
time_t t;
struct protstream *pout;
struct appendstate as;
struct body *body = NULL;
long msgsize;
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);
rfc822date_gen(datestr, sizeof(datestr), t);
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", i_mbox->owner+5);
fprintf(f, "MIME-Version: 1.0\r\n");
fprintf(f, "Subject: Automatically deleted mail\r\n");
ownerlen = strlen(i_mbox->owner);
while ((msg = i_mbox->msgs)) {
fprintf(f, "\r\n\r\nThe following message was deleted from mailbox "
"'Inbox%s'\r\n", msg->mboxname+ownerlen);
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);
append_setup(&as, i_mbox->owner, NULL, NULL, 0, -1);
pout = prot_new(fd, 0);
prot_rewind(pout);
append_fromstream(&as, &body, pout, msgsize, t, NULL, 0);
append_commit(&as, -1, NULL, NULL, NULL);
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);
}
}