diff --git a/clamd/clamd.c b/clamd/clamd.c index 4e173c2c51..866746a40d 100644 --- a/clamd/clamd.c +++ b/clamd/clamd.c @@ -106,6 +106,7 @@ static void help(void) printf(" --debug Enable debug mode\n"); printf(" --log=FILE -l FILE Log into FILE\n"); printf(" --config-file=FILE -c FILE Read configuration from FILE\n"); + printf(" --fail-if-cvd-older-than=days Return with a nonzero error code if virus database outdated.\n"); printf("\n"); printf("Pass in - as the filename for stdin.\n"); printf("\n"); @@ -651,6 +652,12 @@ int main(int argc, char **argv) svc_register("clamd"); } #endif + if (optget(opts, "fail-if-cvd-older-than")->enabled) { + if (check_if_cvd_outdated(dbdir, optget(opts, "fail-if-cvd-older-than")->numarg) != CL_SUCCESS) { + ret = 1; + break; + } + } if ((ret = cl_load(dbdir, engine, &sigs, dboptions))) { logg(LOGG_ERROR, "%s\n", cl_strerror(ret)); diff --git a/clamscan/clamscan.c b/clamscan/clamscan.c index 504c0c9d84..b9004a873a 100644 --- a/clamscan/clamscan.c +++ b/clamscan/clamscan.c @@ -255,6 +255,7 @@ void help(void) mprintf(LOGG_INFO, " A JSON file will dropped to the temp directory if --leave-temps is enabled.\n"); mprintf(LOGG_INFO, " --database=FILE/DIR -d FILE/DIR Load virus database from FILE or load all supported db files from DIR\n"); mprintf(LOGG_INFO, " --official-db-only[=yes/no(*)] Only load official signatures\n"); + mprintf(LOGG_INFO, " --fail-if-cvd-older-than=days Return with a nonzero error code if virus database outdated.\n"); mprintf(LOGG_INFO, " --log=FILE -l FILE Save scan report to FILE\n"); mprintf(LOGG_INFO, " --recursive[=yes/no(*)] -r Scan subdirectories recursively\n"); mprintf(LOGG_INFO, " --allmatch[=yes/no(*)] -z Continue scanning within file after finding a match\n"); diff --git a/clamscan/manager.c b/clamscan/manager.c index 8c1c734042..dc6c4e413c 100644 --- a/clamscan/manager.c +++ b/clamscan/manager.c @@ -1258,6 +1258,13 @@ int scanmanager(const struct optstruct *opts) if ((opt = optget(opts, "database"))->active) { while (opt) { + if (optget(opts, "fail-if-cvd-older-than")->enabled) { + if (check_if_cvd_outdated(opt->strarg, optget(opts, "fail-if-cvd-older-than")->numarg) != CL_SUCCESS) { + ret = 2; + goto done; + } + } + if ((ret = cl_load(opt->strarg, engine, &info.sigs, dboptions))) { logg(LOGG_ERROR, "%s\n", cl_strerror(ret)); @@ -1270,6 +1277,13 @@ int scanmanager(const struct optstruct *opts) } else { char *dbdir = freshdbdir(); + if (optget(opts, "fail-if-cvd-older-than")->enabled) { + if (check_if_cvd_outdated(dbdir, optget(opts, "fail-if-cvd-older-than")->numarg) != CL_SUCCESS) { + ret = 2; + goto done; + } + } + if ((ret = cl_load(dbdir, engine, &info.sigs, dboptions))) { logg(LOGG_ERROR, "%s\n", cl_strerror(ret)); diff --git a/common/misc.c b/common/misc.c index 5ed6d33d5a..541728cf4f 100644 --- a/common/misc.c +++ b/common/misc.c @@ -44,7 +44,6 @@ #include // libclamav -#include "clamav.h" #include "cvd.h" #include "others.h" /* for cli_rmdirs() */ #include "regex/regex.h" @@ -487,3 +486,21 @@ unsigned int countlines(const char *filename) fclose(fh); return lines; } + +cl_error_t check_if_cvd_outdated(const char *path, long long days) +{ + cl_error_t status; + time_t cvd_age; + + if ((status = cl_cvdgetage(path, &cvd_age)) != CL_SUCCESS) { + logg(LOGG_ERROR, "%s\n", cl_strerror(status)); + return status; + } + + if (days * 86400 < cvd_age) { + logg(LOGG_ERROR, "Virus database is older than %lld days!\n", days); + return CL_ECVD; + } + + return CL_SUCCESS; +} diff --git a/common/misc.h b/common/misc.h index 1a21fff618..9c89f0c9d0 100644 --- a/common/misc.h +++ b/common/misc.h @@ -28,6 +28,7 @@ #endif #include +#include "clamav.h" #include "platform.h" #include "optparser.h" /* Maximum filenames under various systems - njh */ @@ -105,4 +106,7 @@ int match_regex(const char *filename, const char *pattern); int cli_is_abspath(const char *path); unsigned int countlines(const char *filename); +/* Checks if a virus database file or directory is older than 'days'. */ +cl_error_t check_if_cvd_outdated(const char *path, long long days); + #endif diff --git a/common/optparser.c b/common/optparser.c index 6fd639c5bb..10398cb0f5 100644 --- a/common/optparser.c +++ b/common/optparser.c @@ -280,6 +280,8 @@ const struct clam_option __clam_options[] = { {"OfficialDatabaseOnly", "official-db-only", 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 0, NULL, 0, OPT_CLAMD | OPT_CLAMSCAN, "Only load the official signatures published by the ClamAV project.", "no"}, + {"FailIfCvdOlderThan", "fail-if-cvd-older-than", 0, CLOPT_TYPE_NUMBER, MATCH_NUMBER, -1, NULL, 0, OPT_CLAMD | OPT_CLAMSCAN, "Return with a nonzero error code if the virus database is older than the specified number of days.", "-1"}, + {"YaraRules", "yara-rules", 0, CLOPT_TYPE_STRING, NULL, 0, NULL, 0, OPT_CLAMSCAN, "By default, yara rules will be loaded. This option allows you to exclude yara rules when scanning and also to scan only using yara rules. Valid options are yes|no|only", "yes"}, {"LocalSocket", NULL, 0, CLOPT_TYPE_STRING, NULL, -1, NULL, 0, OPT_CLAMD, "Path to a local socket file the daemon will listen on.", "/tmp/clamd.socket"}, diff --git a/docs/man/clamd.8.in b/docs/man/clamd.8.in index 4e08d56b49..08d941f9d8 100644 --- a/docs/man/clamd.8.in +++ b/docs/man/clamd.8.in @@ -112,6 +112,9 @@ Enable debug mode. .TP \fB\-c FILE, \-\-config\-file=FILE\fR Read configuration from FILE. +.TP +\fB\-\-fail\-if\-cvd\-older\-than=days\fR +Return with a nonzero error code if the virus database is older than the specified number of days. .SH "ENVIRONMENT VARIABLES" .LP diff --git a/docs/man/clamd.conf.5.in b/docs/man/clamd.conf.5.in index 2d9748a39e..818f6b3797 100644 --- a/docs/man/clamd.conf.5.in +++ b/docs/man/clamd.conf.5.in @@ -102,6 +102,11 @@ Only load the official signatures published by the ClamAV project. .br Default: no .TP +\fBFailIfCvdOlderThan NUMBER\fR +Return with a nonzero error code if the virus database is older than the specified number of days. +.br +Default: -1 +.TP \fBLocalSocket STRING\fR Path to a local (Unix) socket the daemon will listen on. .br diff --git a/docs/man/clamscan.1.in b/docs/man/clamscan.1.in index beb2ed7b31..5b8f467019 100644 --- a/docs/man/clamscan.1.in +++ b/docs/man/clamscan.1.in @@ -60,6 +60,9 @@ Load virus database from FILE or load all virus database files from DIR. \fB\-\-official\-db\-only=[yes/no(*)]\fR Only load the official signatures published by the ClamAV project. .TP +\fB\-\-fail\-if\-cvd\-older\-than=days\fR +Return with a nonzero error code if the virus database is older than the specified number of days. +.TP \fB\-l FILE, \-\-log=FILE\fR Save scan report to FILE. .TP diff --git a/etc/clamd.conf.sample b/etc/clamd.conf.sample index 37fb03bf20..8cf370c600 100644 --- a/etc/clamd.conf.sample +++ b/etc/clamd.conf.sample @@ -88,6 +88,11 @@ Example # Default: no #OfficialDatabaseOnly no +# Return with a nonzero error code if the virus database is older than +# the specified number of days. +# Default: -1 +#FailIfCvdOlderThan 7 + # The daemon can work in local mode, network mode or both. # Due to security reasons we recommend the local mode. diff --git a/libclamav/clamav.h b/libclamav/clamav.h index de1c9f2e4d..d0e707ed9f 100644 --- a/libclamav/clamav.h +++ b/libclamav/clamav.h @@ -1132,6 +1132,18 @@ extern void cl_cvdfree(struct cl_cvd *cvd); */ extern cl_error_t cl_cvdunpack(const char *file, const char *dir, bool dont_verify); +/** + * @brief Retrieve the age of CVD disk data. + * + * Will retrieve the age of the youngest file in a database directory, + * or the age of a single CVD (or CLD) file. + * + * @param path Filepath of CVD directory or file. + * @param age_seconds Age of the directory or file. + * @return cl_error_t CL_SUCCESS if success, else a CL_E* error code. + */ +extern cl_error_t cl_cvdgetage(const char *path, time_t *age_seconds); + /* ---------------------------------------------------------------------------- * DB directory stat functions. * Use these functions to watch for database changes. diff --git a/libclamav/cvd.c b/libclamav/cvd.c index 8366a45787..71decca453 100644 --- a/libclamav/cvd.c +++ b/libclamav/cvd.c @@ -34,6 +34,7 @@ #include #include #include +#include #ifdef HAVE_UNISTD_H #include #endif @@ -774,3 +775,106 @@ cl_error_t cl_cvdunpack(const char *file, const char *dir, bool dont_verify) return status; } + +static cl_error_t cvdgetfileage(const char *path, time_t *age_seconds) +{ + struct cl_cvd cvd; + time_t s_time; + cl_error_t status = CL_SUCCESS; + FILE *fs = NULL; + + if ((fs = fopen(path, "rb")) == NULL) { + cli_errmsg("cvdgetfileage: Can't open file %s\n", path); + return CL_EOPEN; + } + + if ((status = cli_cvdverify(fs, &cvd, 1)) != CL_SUCCESS) + goto done; + + time(&s_time); + + if (cvd.stime > s_time) + *age_seconds = 0; + else + *age_seconds = s_time - cvd.stime; + +done: + if (fs) + fclose(fs); + + return status; +} + +cl_error_t cl_cvdgetage(const char *path, time_t *age_seconds) +{ + STATBUF statbuf; + struct dirent *dent; + size_t path_len; + bool ends_with_sep = false; + DIR *dd = NULL; + bool first_age_set = true; + cl_error_t status = CL_SUCCESS; + + if (CLAMSTAT(path, &statbuf) == -1) { + cli_errmsg("cl_cvdgetage: Can't get status of: %s\n", path); + status = CL_ESTAT; + goto done; + } + + if (!S_ISDIR(statbuf.st_mode)) { + status = cvdgetfileage(path, age_seconds); + goto done; + } + + if ((dd = opendir(path)) == NULL) { + cli_errmsg("cl_cvdgetage: Can't open directory %s\n", path); + status = CL_EOPEN; + goto done; + } + + path_len = strlen(path); + + if (path_len >= strlen(PATHSEP)) { + if (strcmp(path + path_len - strlen(PATHSEP), PATHSEP) == 0) { + cli_dbgmsg("cl_cvdgetage: path ends with separator\n"); + ends_with_sep = true; + } + } + + while ((dent = readdir(dd))) { + char fname[1024] = {0}; + time_t file_age; + + if (!dent->d_ino) + continue; + + if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, "..")) + continue; + + if (!CLI_DBEXT(dent->d_name)) + continue; + + if (ends_with_sep) + snprintf(fname, sizeof(fname) - 1, "%s%s", path, dent->d_name); + else + snprintf(fname, sizeof(fname) - 1, "%s" PATHSEP "%s", path, dent->d_name); + + if ((status = cvdgetfileage(fname, &file_age)) != CL_SUCCESS) { + cli_errmsg("cl_cvdgetage: cvdgetfileage() failed for %s\n", fname); + goto done; + } + + if (first_age_set) { + first_age_set = false; + *age_seconds = file_age; + } else { + *age_seconds = MIN(file_age, *age_seconds); + } + } + +done: + if (dd) + closedir(dd); + + return status; +} diff --git a/libclamav/libclamav.map b/libclamav/libclamav.map index 2a36220cca..e6d5b07d22 100644 --- a/libclamav/libclamav.map +++ b/libclamav/libclamav.map @@ -71,6 +71,10 @@ CLAMAV_1.0.0 { cl_cvdunpack; cl_engine_set_clcb_file_inspection; } CLAMAV_0.104.0; +CLAMAV_1.1.0 { + global: + cl_cvdgetage; +} CLAMAV_1.0.0; CLAMAV_PRIVATE { global: cli_sigperf_print; diff --git a/win32/conf_examples/clamd.conf.sample b/win32/conf_examples/clamd.conf.sample index 5a8a9cfeae..272a661740 100644 --- a/win32/conf_examples/clamd.conf.sample +++ b/win32/conf_examples/clamd.conf.sample @@ -76,6 +76,11 @@ Example # Default: no #OfficialDatabaseOnly no +# Return with a nonzero error code if the virus database is older than +# the specified number of days. +# Default: -1 +#FailIfCvdOlderThan 7 + # The daemon on Windows only supports unsecured TCP sockets. # Due to security reasons make sure that your IP & port is not # exposed to the open internet.