summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog1
-rw-r--r--configure.in4
-rw-r--r--doc/clamsmtpd.conf4
-rw-r--r--doc/clamsmtpd.conf.538
-rw-r--r--src/clamsmtpd.c192
5 files changed, 183 insertions, 56 deletions
diff --git a/ChangeLog b/ChangeLog
index 6fabba4..dd44ce0 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -6,6 +6,7 @@
- Compiles and runs on systems without error checking mutexes
- Ability to run as a different user [Rubio Vaughan]
- Fixed config file bugs
+ - Virus Actions. Run a script every time a virus is found.
0.8
- clamsmtpd now uses a configuration file
diff --git a/configure.in b/configure.in
index 7283d90..b57dd73 100644
--- a/configure.in
+++ b/configure.in
@@ -36,8 +36,8 @@ dnl Nate Nielsen <nielsen@memberwebs.com>
dnl
dnl Process this file with autoconf to produce a configure script.
-AC_INIT(clamsmtp, 0.8.92, nielsen@memberwebs.com)
-AM_INIT_AUTOMAKE(clamsmtp, 0.8.92)
+AC_INIT(clamsmtp, 0.8.93, nielsen@memberwebs.com)
+AM_INIT_AUTOMAKE(clamsmtp, 0.8.93)
LDFLAGS="$LDFLAGS -L/usr/local/lib"
CFLAGS="$CFLAGS -I/usr/local/include"
diff --git a/doc/clamsmtpd.conf b/doc/clamsmtpd.conf
index 44fd01e..24afb19 100644
--- a/doc/clamsmtpd.conf
+++ b/doc/clamsmtpd.conf
@@ -42,3 +42,7 @@ OutAddress: 10026
# User to switch to
#User: clamav
+
+# Virus actions. There's an option to run a script every time a
+# virus is found. Read the man page for clamsmtpd.conf for details.
+
diff --git a/doc/clamsmtpd.conf.5 b/doc/clamsmtpd.conf.5
index 9505629..f79be11 100644
--- a/doc/clamsmtpd.conf.5
+++ b/doc/clamsmtpd.conf.5
@@ -128,6 +128,9 @@ The user to run as. If this option is specified then
.Xr clamsmtpd 8
must be started as root. It will then drop root privileges and run as the
specified user. The user can either be a name or a numerical user id.
+.It Ar VirusAction
+This is a command to run when a virus is found. See the VIRUS ACTION section
+below for a discussion of this option.
.El
.Sh ADDRESSES
Addresses can be specified in multiple formats:
@@ -141,6 +144,41 @@ the port number (ie: '127.0.0.1:3310').
.It
IPv6 addresses are implemented but disabled. The code needs testing.
.El
+.Sh VIRUS ACTIONS
+Using the
+.Ar VirusAction
+option you can run a script or program whenever a virus is found. This may
+be handy in certain circumstances but it has several drawbacks. For one, the
+performance of the virus filtering will take a hit, perhaps DOS'ing your
+machine under heavy load. Secondly as with running any program there are
+security implications to be considered.
+.Pp
+.Please consider the above carefully before implementing a virus action.
+.Pp
+The script is run without its output being logged, or return value being
+checked. Because of this you should test it thoroughly. Make sure it runs
+without problems under the user that
+.Xr clamsmtpd 8
+is being run as.
+.Pp
+Various environment variables will be present when your script is run:
+.Bl -tag -width Fl
+.It Ar EMAIL
+When the
+.Ar Quarantine
+option is enabled, this specifies the file that the virus was saved to.
+.It Ar RECIPIENTS
+The email addresses of the email recipients. These are specified one per
+line, in standard address format.
+.It Ar SENDER
+The email address for the sender of the email.
+.It Ar TMP
+The path to the temp directory in use. This is the same as the
+.Ar TempDirectory
+option.
+.It Ar VIRUS
+The name of the virus found.
+.El
.Sh SEE ALSO
.Xr clamsmtpd 8
.Sh AUTHOR
diff --git a/src/clamsmtpd.c b/src/clamsmtpd.c
index 1d10adf..591db3a 100644
--- a/src/clamsmtpd.c
+++ b/src/clamsmtpd.c
@@ -66,6 +66,7 @@ typedef struct clstate
const char* clamname;
const char* header; /* The header to add to email */
const char* directory; /* The directory for temp files */
+ const char* virusaction; /* Program to run when event occurs */
int bounce; /* Send back a reject line */
int quarantine; /* Leave virus files in temp dir */
int debug_files; /* Leave all files in temp dir */
@@ -123,6 +124,7 @@ clctx_t;
#define CFG_BOUNCE "Bounce"
#define CFG_QUARANTINE "Quarantine"
#define CFG_DEBUGFILES "DebugFiles"
+#define CFG_VIRUSACTION "VirusAction"
/* -----------------------------------------------------------------------
* GLOBALS
@@ -137,8 +139,8 @@ clstate_t g_clstate;
static void usage();
static int connect_clam(clctx_t* ctx);
static int disconnect_clam(clctx_t* ctx);
-static int quarantine_virus(clctx_t* ctx);
-static int clam_scan_file(clctx_t* ctx);
+static int virus_action(clctx_t* ctx, const char* virus);
+static int clam_scan_file(clctx_t* ctx, const char** virus);
/* -----------------------------------------------------------------------
* SIMPLE MACROS
@@ -322,6 +324,7 @@ static void usage()
int cb_check_data(spctx_t* sp)
{
int r = 0;
+ const char* virus;
clctx_t* ctx = (clctx_t*)sp;
/* Connect to clamav */
@@ -331,7 +334,7 @@ int cb_check_data(spctx_t* sp)
if(r != -1 && (r = sp_cache_data(sp)) > 0)
/* ClamAV doesn't like empty files */
- r = clam_scan_file(ctx);
+ r = clam_scan_file(ctx, &virus);
switch(r)
{
@@ -362,7 +365,7 @@ int cb_check_data(spctx_t* sp)
*/
case 1:
/* Any special post operation actions on the virus */
- quarantine_virus(ctx);
+ virus_action(ctx, virus);
if(sp_fail_data(sp, g_clstate.bounce ?
SMTP_DATAVIRUS : SMTP_DATAVIRUSOK) == -1)
@@ -424,6 +427,12 @@ int cb_parse_option(const char* name, const char* value)
return 1;
}
+ else if(strcasecmp(CFG_VIRUSACTION, name) == 0)
+ {
+ g_clstate.virusaction = value;
+ return 1;
+ }
+
return 0;
}
@@ -508,19 +517,25 @@ static int disconnect_clam(clctx_t* ctx)
return 0;
}
-static int clam_scan_file(clctx_t* ctx)
+static int clam_scan_file(clctx_t* ctx, const char** virus)
{
- int len;
+ int len, x;
+ char* line;
spctx_t* sp = &(ctx->sp);
+ ASSERT(ctx && virus);
+
+ *virus = NULL;
+
/* Needs to be long enough to hold path names */
ASSERT(SP_LINE_LENGTH > MAXPATHLEN + 32);
- strcpy(sp->line, CLAM_SCAN);
- strcat(sp->line, sp->cachename);
- strcat(sp->line, "\n");
+ line = ctx->clam.line;
+ strcpy(line, CLAM_SCAN);
+ strcat(line, sp->cachename);
+ strcat(line, "\n");
- if(spio_write_data(sp, &(ctx->clam), sp->line) == -1)
+ if(spio_write_data(sp, &(ctx->clam), line) == -1)
return -1;
len = spio_read_line(sp, &(ctx->clam), SPIO_DISCARD | SPIO_TRIM);
@@ -530,35 +545,58 @@ static int clam_scan_file(clctx_t* ctx)
return -1;
}
- if(is_last_word(sp->line, CLAM_OK, KL(CLAM_OK)))
+ if(is_last_word(line, CLAM_OK, KL(CLAM_OK)))
{
sp_add_log(sp, "status=", "CLEAN");
sp_messagex(sp, LOG_DEBUG, "no virus");
return 0;
}
- if(is_last_word(sp->line, CLAM_FOUND, KL(CLAM_FOUND)))
+ /*
+ * When a virus is found the returned line from
+ * clamd looks something like this:
+ *
+ * /path/to/virus: Virus.XXXX FOUND
+ */
+ if(is_last_word(line, CLAM_FOUND, KL(CLAM_FOUND)))
{
- len = strlen(sp->cachename);
+ x = strlen(sp->cachename);
+
+ /* A little sanity check ... */
+ if(len > x + KL(CLAM_FOUND))
+ {
+ /* Remove the "FOUND" from the end */
+ line[len - KL(CLAM_FOUND)] = 0;
+
+ /* Skip the filename returned, and colon */
+ line += x + 1;
+
+ line = trim_space(line);
+
+ sp_messagex(sp, LOG_DEBUG, "found virus: %s", line);
+ sp_add_log(sp, "status=VIRUS:", line);
+ *virus = line;
+ }
- if(sp->linelen > len)
- sp_add_log(sp, "status=VIRUS:", sp->line + len + 1);
else
+ {
+ sp_messagex(sp, LOG_WARNING, "couldn't parse virus name from clamd response: %s", line);
sp_add_log(sp, "status=", "VIRUS");
+ *virus = "Unparsable.Virus.Name";
+ }
- sp_messagex(sp, LOG_DEBUG, "found virus");
return 1;
}
- if(is_last_word(sp->line, CLAM_ERROR, KL(CLAM_ERROR)))
+ if(is_last_word(line, CLAM_ERROR, KL(CLAM_ERROR)))
{
- sp_messagex(sp, LOG_ERR, "clamav error: %s", sp->line);
+ sp_messagex(sp, LOG_ERR, "clamav error: %s", line);
sp_add_log(sp, "status=", "CLAMAV-ERROR");
return -1;
}
sp_add_log(sp, "status=", "CLAMAV-ERROR");
- sp_messagex(sp, LOG_ERR, "unexepected response from clamd: %s", sp->line);
+ sp_messagex(sp, LOG_ERR, "unexepected response from clamd: %s", line);
return -1;
}
@@ -566,56 +604,102 @@ static int clam_scan_file(clctx_t* ctx)
* TEMP FILE HANDLING
*/
-static int quarantine_virus(clctx_t* ctx)
+static int virus_action(clctx_t* ctx, const char* virus)
{
- char buf[MAXPATHLEN];
+ char qfilename[MAXPATHLEN];
spctx_t* sp = &(ctx->sp);
char* t;
+ int i;
+ pid_t pid;
- if(!g_clstate.quarantine)
- return 0;
+ if(g_clstate.quarantine)
+ {
+ strlcpy(qfilename, g_clstate.directory, MAXPATHLEN);
+ strlcat(qfilename, "/virus.", MAXPATHLEN);
+
+ /* Points to null terminator */
+ t = qfilename + strlen(qfilename);
+
+ /*
+ * Yes, I know we're using mktemp. And yet we're doing it in
+ * a safe manner due to the link command below not overwriting
+ * existing files.
+ */
+ for(;;)
+ {
+ /* Null terminate off the ending, and replace with X's for mktemp */
+ *t = 0;
+ strlcat(qfilename, "XXXXXX", MAXPATHLEN);
+
+ if(!mktemp(qfilename))
+ {
+ sp_message(sp, LOG_ERR, "couldn't create quarantine file name");
+ return -1;
+ }
- strlcpy(buf, g_clstate.directory, MAXPATHLEN);
- strlcat(buf, "/virus.", MAXPATHLEN);
+ /* Try to link the file over to the temp */
+ if(link(sp->cachename, qfilename) == -1)
+ {
+ /* We don't want to allow race conditions */
+ if(errno == EEXIST)
+ {
+ sp_message(sp, LOG_WARNING, "race condition when quarantining virus file: %s", qfilename);
+ continue;
+ }
+
+ sp_message(sp, LOG_ERR, "couldn't quarantine virus file");
+ return -1;
+ }
- /* Points to null terminator */
- t = buf + strlen(buf);
+ break;
+ }
- /*
- * Yes, I know we're using mktemp. And yet we're doing it in
- * a safe manner due to the link command below not overwriting
- * existing files.
- */
- for(;;)
- {
- /* Null terminate off the ending, and replace with X's for mktemp */
- *t = 0;
- strlcat(buf, "XXXXXX", MAXPATHLEN);
+ sp_messagex(sp, LOG_INFO, "quarantined virus file as: %s", qfilename);
+ }
- if(!mktemp(buf))
+ if(g_clstate.virusaction != NULL)
+ {
+ switch(pid = fork())
{
- sp_message(sp, LOG_ERR, "couldn't create quarantine file name");
+ case -1:
+ sp_message(sp, LOG_ERR, "couldn't fork for virus action");
return -1;
- }
- /* Try to link the file over to the temp */
- if(link(sp->cachename, buf) == -1)
- {
- /* We don't want to allow race conditions */
- if(errno == EEXIST)
- {
- sp_message(sp, LOG_WARNING, "race condition when quarantining virus file: %s", buf);
- continue;
- }
+ /* The child */
+ case 0:
+ /*
+ * New process group because we don't care about this child,
+ * it's return value, waiting for it to exit or any of that.
+ * Apparently this can't fail when done like this.
+ */
+ setsid();
- sp_message(sp, LOG_ERR, "couldn't quarantine virus file");
- return -1;
- }
+ /* Close all descriptors */
+ for(i = 0; i <= 2; i++)
+ close(i);
- break;
+ /* Set the environment variables */
+ sp_setup_env(sp, 0);
+
+ /* When quarantining we can hand the file name off */
+ if(g_clstate.quarantine)
+ setenv("EMAIL", qfilename, 1);
+
+ if(virus)
+ setenv("VIRUS", virus, 1);
+
+ sp_messagex(sp, LOG_DEBUG, "executing virus action: %s", g_clstate.virusaction);
+
+ /* And execute the program */
+ execl("/bin/sh", "sh", "-c", g_clstate.virusaction, NULL);
+
+ /* If that returned then there was an error */
+ sp_message(sp, LOG_ERR, "error executing the shell for virus action");
+ exit(1);
+ break;
+ };
}
- sp_messagex(sp, LOG_INFO, "quarantined virus file as: %s", buf);
return 0;
}