diff options
author | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2019-01-17 15:03:38 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-01-17 15:03:38 +0000 |
commit | c20a13ccab59e433e82744fe958c5746203e9ab2 (patch) | |
tree | e2f584727df5366a4eed772f777b746a823c597d | |
parent | 5b577d4da98d9790ae6b055e1368e63c68a62349 (diff) | |
parent | 44de7f58793a846a36b9eaf4c459c035e7d9cfb2 (diff) | |
download | rspamd-c20a13ccab59e433e82744fe958c5746203e9ab2.tar.gz rspamd-c20a13ccab59e433e82744fe958c5746203e9ab2.zip |
Merge pull request #2711 from HeinleinSupport/master
Oletools,ICAP support / lua_scanners enhancements
-rw-r--r-- | conf/groups.conf | 12 | ||||
-rw-r--r-- | conf/modules.d/external_services.conf | 91 | ||||
-rw-r--r-- | lualib/lua_scanners/clamav.lua | 47 | ||||
-rw-r--r-- | lualib/lua_scanners/common.lua | 193 | ||||
-rw-r--r-- | lualib/lua_scanners/dcc.lua | 85 | ||||
-rw-r--r-- | lualib/lua_scanners/fprot.lua | 37 | ||||
-rw-r--r-- | lualib/lua_scanners/icap.lua | 301 | ||||
-rw-r--r-- | lualib/lua_scanners/init.lua | 4 | ||||
-rw-r--r-- | lualib/lua_scanners/kaspersky_av.lua | 48 | ||||
-rw-r--r-- | lualib/lua_scanners/oletools.lua | 302 | ||||
-rw-r--r-- | lualib/lua_scanners/savapi.lua | 45 | ||||
-rw-r--r-- | lualib/lua_scanners/sophos.lua | 53 | ||||
-rw-r--r-- | src/plugins/lua/antivirus.lua | 58 | ||||
-rw-r--r-- | src/plugins/lua/external_services.lua | 128 |
14 files changed, 1164 insertions, 240 deletions
diff --git a/conf/groups.conf b/conf/groups.conf index 02e714174..51e2a6522 100644 --- a/conf/groups.conf +++ b/conf/groups.conf @@ -107,5 +107,15 @@ group "neural" { .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/neural_group.conf" } +group "antivirus" { + .include(try=true; priority=1; duplicate=merge) "$LOCAL_CONFDIR/local.d/antivirus_group.conf" + .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/antivirus_group.conf" +} + +group "external_services" { + .include(try=true; priority=1; duplicate=merge) "$LOCAL_CONFDIR/local.d/external_services_group.conf" + .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/external_services_group.conf" +} + .include(try=true; priority=1; duplicate=merge) "$LOCAL_CONFDIR/local.d/groups.conf" -.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/groups.conf"
\ No newline at end of file +.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/groups.conf" diff --git a/conf/modules.d/external_services.conf b/conf/modules.d/external_services.conf new file mode 100644 index 000000000..3995a7c70 --- /dev/null +++ b/conf/modules.d/external_services.conf @@ -0,0 +1,91 @@ +# Please don't modify this file as your changes might be overwritten with +# the next update. +# +# You can modify '$LOCAL_CONFDIR/rspamd.conf.local.override' to redefine +# parameters defined on the top level +# +# You can modify '$LOCAL_CONFDIR/rspamd.conf.local' to add +# parameters defined on the top level +# +# For specific modules or configuration you can also modify +# '$LOCAL_CONFDIR/local.d/file.conf' - to add your options or rewrite defaults +# '$LOCAL_CONFDIR/override.d/file.conf' - to override the defaults +# +# See https://rspamd.com/doc/tutorials/writing_rules.html for details + +external_services { + oletools { + # If set force this action if any virus is found (default unset: no action is forced) + # action = "reject"; + # If set, then rejection message is set to this value (mention single quotes) + # If `max_size` is set, messages > n bytes in size are not scanned + # max_size = 20000000; + # log_clean = true; + # servers = "127.0.0.1:10050"; + # cache_expire = 86400; + # scan_mime_parts = true; + # extended = false; + # if `patterns` is specified virus name will be matched against provided regexes and the related + # symbol will be yielded if a match is found. If no match is found, default symbol is yielded. + patterns { + # symbol_name = "pattern"; + JUST_EICAR = "^Eicar-Test-Signature$"; + } + # mime-part regex matching in content-type or filename + mime_parts_filter_regex { + #GEN1 = "application\/octet-stream"; + DOC2 = "application\/msword"; + DOC3 = "application\/vnd\.ms-word.*"; + XLS = "application\/vnd\.ms-excel.*"; + PPT = "application\/vnd\.ms-powerpoint.*"; + GEN2 = "application\/vnd\.openxmlformats-officedocument.*"; + } + # Mime-Part filename extension matching (no regex) + mime_parts_filter_ext { + doc = "doc"; + dot = "dot"; + docx = "docx"; + dotx = "dotx"; + docm = "docm"; + dotm = "dotm"; + xls = "xls"; + xlt = "xlt"; + xla = "xla"; + xlsx = "xlsx"; + xltx = "xltx"; + xlsm = "xlsm"; + xltm = "xltm"; + xlam = "xlam"; + xlsb = "xlsb"; + ppt = "ppt"; + pot = "pot"; + pps = "pps"; + ppa = "ppa"; + pptx = "pptx"; + potx = "potx"; + ppsx = "ppsx"; + ppam = "ppam"; + pptm = "pptm"; + potm = "potm"; + ppsm = "ppsm"; + } + # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned. + whitelist = "/etc/rspamd/antivirus.wl"; + } + dcc { + # If set force this action if any virus is found (default unset: no action is forced) + # action = "reject"; + # If set, then rejection message is set to this value (mention single quotes) + # If `max_size` is set, messages > n bytes in size are not scanned + max_size = 20000000; + #servers = "127.0.0.1:10045"; + # if `patterns` is specified virus name will be matched against provided regexes and the related + # symbol will be yielded if a match is found. If no match is found, default symbol is yielded. + patterns { + # symbol_name = "pattern"; + JUST_EICAR = "^Eicar-Test-Signature$"; + } + # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned. + whitelist = "/etc/rspamd/antivirus.wl"; + } +} diff --git a/lualib/lua_scanners/clamav.lua b/lualib/lua_scanners/clamav.lua index b7de739cd..4ca3e8a8b 100644 --- a/lualib/lua_scanners/clamav.lua +++ b/lualib/lua_scanners/clamav.lua @@ -32,9 +32,10 @@ local default_message = '${SCANNER}: virus found: "${VIRUS}"' local function clamav_config(opts) local clamav_conf = { - scan_mime_parts = true; - scan_text_mime = false; - scan_image_mime = false; + N = N, + scan_mime_parts = true, + scan_text_mime = false, + scan_image_mime = false, default_port = 3310, log_clean = false, timeout = 5.0, -- FIXME: this will break task_timeout! @@ -44,12 +45,18 @@ local function clamav_config(opts) message = default_message, } - for k,v in pairs(opts) do - clamav_conf[k] = v - end + clamav_conf = lua_util.override_defaults(clamav_conf, opts) if not clamav_conf.prefix then - clamav_conf.prefix = 'rs_cl' + clamav_conf.prefix = 'rs_' .. clamav_conf.name .. '_' + end + + if not clamav_conf.log_prefix then + if clamav_conf.name:lower() == clamav_conf.type:lower() then + clamav_conf.log_prefix = clamav_conf.name + else + clamav_conf.log_prefix = clamav_conf.name .. ' (' .. clamav_conf.type .. ')' + end end if not clamav_conf['servers'] then @@ -63,7 +70,7 @@ local function clamav_config(opts) clamav_conf.default_port) if clamav_conf['upstreams'] then - lua_util.add_debug_alias('antivirus', N) + lua_util.add_debug_alias('antivirus', clamav_conf.N) return clamav_conf end @@ -96,7 +103,7 @@ local function clamav_check(task, content, digest, rule) upstream = rule.upstreams:get_upstream_round_robin() addr = upstream:get_addr() - lua_util.debugm(N, task, '%s [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) + lua_util.debugm(rule.N, task, '%s: retry IP: %s', rule.log_prefix, addr) tcp.request({ task = task, @@ -108,7 +115,7 @@ local function clamav_check(task, content, digest, rule) stop_pattern = '\0' }) else - rspamd_logger.errx(task, '%s [%s]: failed to scan, maximum retransmits exceed', rule['symbol'], rule['type']) + rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits exceed', rule.log_prefix) task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan and retransmits exceed') end @@ -116,26 +123,26 @@ local function clamav_check(task, content, digest, rule) upstream:ok() data = tostring(data) local cached - lua_util.debugm(N, task, '%s [%s]: got reply: %s', rule['symbol'], rule['type'], data) + lua_util.debugm(rule.N, task, '%s: got reply: %s', rule.log_prefix, data) if data == 'stream: OK' then cached = 'OK' if rule['log_clean'] then - rspamd_logger.infox(task, '%s [%s]: message or mime_part is clean', rule['symbol'], rule['type']) + rspamd_logger.infox(task, '%s: message or mime_part is clean', rule.log_prefix) else - lua_util.debugm(N, task, '%s [%s]: message or mime_part is clean', rule['symbol'], rule['type']) + lua_util.debugm(rule.N, task, '%s: message or mime_part is clean', rule.log_prefix) end else local vname = string.match(data, 'stream: (.+) FOUND') if vname then - common.yield_result(task, rule, vname, N) + common.yield_result(task, rule, vname) cached = vname else - rspamd_logger.errx(task, 'unhandled response: %s', data) + rspamd_logger.errx(task, '%s: unhandled response: %s', rule.log_prefix, data) task:insert_result(rule['symbol_fail'], 0.0, 'unhandled response') end end if cached then - common.save_av_cache(task, digest, rule, cached, N) + common.save_av_cache(task, digest, rule, cached) end end end @@ -151,8 +158,8 @@ local function clamav_check(task, content, digest, rule) }) end - if common.need_av_check(task, content, rule, N) then - if common.check_av_cache(task, digest, rule, clamav_check_uncached, N) then + if common.need_av_check(task, content, rule) then + if common.check_av_cache(task, digest, rule, clamav_check_uncached) then return else clamav_check_uncached() @@ -165,5 +172,5 @@ return { description = 'clamav antivirus', configure = clamav_config, check = clamav_check, - name = 'clamav' -}
\ No newline at end of file + name = N +} diff --git a/lualib/lua_scanners/common.lua b/lualib/lua_scanners/common.lua index da1a4dd49..0c76004eb 100644 --- a/lualib/lua_scanners/common.lua +++ b/lualib/lua_scanners/common.lua @@ -1,5 +1,6 @@ --[[ Copyright (c) 2018, Vsevolod Stakhov <vsevolod@highsecure.ru> +Copyright (c) 2019, Carsten Rosenberg <c.rosenberg@heinlein-support.de> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,30 +21,43 @@ limitations under the License. --]] local rspamd_logger = require "rspamd_logger" +local rspamd_regexp = require "rspamd_regexp" local lua_util = require "lua_util" local lua_redis = require "lua_redis" local fun = require "fun" local exports = {} -local function match_patterns(default_sym, found, patterns) - if type(patterns) ~= 'table' then return default_sym end +local function log_clean(task, rule, msg) + + msg = msg or 'message or mime_part is clean' + + if rule.log_clean then + rspamd_logger.infox(task, '%s: %s', rule.log_prefix, msg) + else + lua_util.debugm(rule.module_name, task, '%s: %s', rule.log_prefix, msg) + end + +end + +local function match_patterns(default_sym, found, patterns, dyn_weight) + if type(patterns) ~= 'table' then return default_sym, dyn_weight end if not patterns[1] then for sym, pat in pairs(patterns) do if pat:match(found) then - return sym + return sym, '1' end end - return default_sym + return default_sym, dyn_weight else for _, p in ipairs(patterns) do for sym, pat in pairs(p) do if pat:match(found) then - return sym + return sym, '1' end end end - return default_sym + return default_sym, dyn_weight end end @@ -51,23 +65,23 @@ local function yield_result(task, rule, vname, N, dyn_weight) local all_whitelisted = true if not dyn_weight then dyn_weight = 1.0 end if type(vname) == 'string' then - local symname = match_patterns(rule.symbol, vname, rule.patterns) + local symname, symscore = match_patterns(rule.symbol, vname, rule.patterns, dyn_weight) if rule.whitelist and rule.whitelist:get_key(vname) then - rspamd_logger.infox(task, '%s: "%s" is in whitelist', N, vname) + rspamd_logger.infox(task, '%s: "%s" is in whitelist', rule.log_prefix, vname) return end - task:insert_result(symname, 1.0, vname) - rspamd_logger.infox(task, '%s: %s found: "%s"', N, rule.detection_category, vname) + task:insert_result(symname, symscore, vname) + rspamd_logger.infox(task, '%s: %s found: "%s"', rule.log_prefix, rule.detection_category, vname) elseif type(vname) == 'table' then for _, vn in ipairs(vname) do - local symname = match_patterns(rule.symbol, vn, rule.patterns) + local symname, symscore = match_patterns(rule.symbol, vn, rule.patterns, dyn_weight) if rule.whitelist and rule.whitelist:get_key(vn) then - rspamd_logger.infox(task, '%s: "%s" is in whitelist', N, vn) + rspamd_logger.infox(task, '%s: "%s" is in whitelist', rule.log_prefix, vn) else all_whitelisted = false - task:insert_result(symname, dyn_weight, vn) + task:insert_result(symname, symscore, vn) rspamd_logger.infox(task, '%s: %s found: "%s"', - N, rule.detection_category, vn) + rule.log_prefix, rule.detection_category, vn) end end end @@ -76,43 +90,45 @@ local function yield_result(task, rule, vname, N, dyn_weight) if all_whitelisted then return end vname = table.concat(vname, '; ') end - task:set_pre_result(rule['action'], + task:set_pre_result(rule.action, lua_util.template(rule.message or 'Rejected', { - SCANNER = N, + SCANNER = rule.name, VIRUS = vname, }), N) end end -local function message_not_too_large(task, content, rule, N) +local function message_not_too_large(task, content, rule) local max_size = tonumber(rule.max_size) if not max_size then return true end if #content > max_size then rspamd_logger.infox(task, "skip %s check as it is too large: %s (%s is allowed)", - N, #content, max_size) + rule.log_prefix, #content, max_size) return false end return true end -local function need_av_check(task, content, rule, N) - return message_not_too_large(task, content, rule, N) +local function need_av_check(task, content, rule) + return message_not_too_large(task, content, rule) end -local function check_av_cache(task, digest, rule, fn, N) +local function check_av_cache(task, digest, rule, fn) local key = digest local function redis_av_cb(err, data) if data and type(data) == 'string' then -- Cached - if data ~= 'OK' then - lua_util.debugm(N, task, 'got cached result for %s: %s', - key, data) - data = lua_util.str_split(data, '\v') - yield_result(task, rule, data, N) + data = rspamd_str_split(data, '\t') + local threat_string = rspamd_str_split(data[1], '\v') + local score = data[2] or rule.default_score + if threat_string[1] ~= 'OK' then + lua_util.debugm(rule.module_name, task, '%s: got cached threat result for %s: %s', + rule.log_prefix, key, threat_string[1]) + yield_result(task, rule, threat_string, score) else - lua_util.debugm(N, task, 'got cached result for %s: %s', - key, data) + lua_util.debugm(rule.module_name, task, '%s: got cached negative result for %s: %s', + rule.log_prefix, key, threat_string[1]) end else if err then @@ -124,7 +140,7 @@ local function check_av_cache(task, digest, rule, fn, N) if rule.redis_params then - key = rule['prefix'] .. key + key = rule.prefix .. key if lua_redis.redis_make_request(task, rule.redis_params, -- connect params @@ -141,7 +157,7 @@ local function check_av_cache(task, digest, rule, fn, N) return false end -local function save_av_cache(task, digest, rule, to_save, N) +local function save_av_cache(task, digest, rule, to_save, dyn_weight) local key = digest local function redis_set_cb(err) @@ -150,8 +166,7 @@ local function save_av_cache(task, digest, rule, to_save, N) rspamd_logger.errx(task, 'failed to save %s cache for %s -> "%s": %s', rule.detection_category, to_save, key, err) else - lua_util.debugm(N, task, 'saved cached result for %s: %s', - key, to_save) + lua_util.debugm(rule.module_name, task, '%s: saved cached result for %s: %s', rule.log_prefix, key, to_save) end end @@ -159,6 +174,8 @@ local function save_av_cache(task, digest, rule, to_save, N) to_save = table.concat(to_save, '\v') end + local value = table.concat({to_save, dyn_weight}, '\t') + if rule.redis_params and rule.prefix then key = rule.prefix .. key @@ -168,29 +185,127 @@ local function save_av_cache(task, digest, rule, to_save, N) true, -- is write redis_set_cb, --callback 'SETEX', -- command - { key, rule.cache_expire or 0, to_save } + { key, rule.cache_expire or 0, value } ) end return false end -local function text_parts_min_words(task, min_words) - local filter_func = function(p) - return p:get_words_count() >= min_words +local function create_regex_table(patterns) + local regex_table = {} + if patterns[1] then + for i, p in ipairs(patterns) do + if type(p) == 'table' then + local new_set = {} + for k, v in pairs(p) do + new_set[k] = rspamd_regexp.create_cached(v) + end + regex_table[i] = new_set + else + regex_table[i] = {} + end + end + else + for k, v in pairs(patterns) do + regex_table[k] = rspamd_regexp.create_cached(v) + end + end + return regex_table +end + +local function match_filter(task, found, patterns) + if type(patterns) ~= 'table' then return false end + if not patterns[1] then + for _, pat in pairs(patterns) do + if pat:match(found) then + return true + end + end + return false + else + for _, p in ipairs(patterns) do + for _, pat in ipairs(p) do + if pat:match(found) then + return true + end + end + end + return false end +end - return fun.any(filter_func, task:get_text_parts()) +-- borrowed from mime_types.lua +-- ext is the last extension, LOWERCASED +-- ext2 is the one before last extension LOWERCASED +local function gen_extension(fname) + local filename_parts = rspamd_str_split(fname, '.') + local ext = {} + for n = 1, 2 do + ext[n] = #filename_parts > n and string.lower(filename_parts[#filename_parts + 1 - n]) or nil + end + return ext[1],ext[2],filename_parts end +local function check_parts_match(task, rule) + + local filter_func = function(p) + local content_type,content_subtype = p:get_type() + local fname = p:get_filename() + local ext, ext2, part_table + local extension_check = false + local content_type_check = false + local text_part_min_words_check = true + + if rule.scan_all_mime_parts == false then + -- check file extension and filename regex matching + if fname ~= nil then + ext,ext2,part_table = gen_extension(fname) + lua_util.debugm(rule.module_name, task, '%s: extension found: %s - 2.ext: %s - parts: %s', + rule.log_prefix, ext, ext2, part_table) + if match_filter(task, ext, rule.mime_parts_filter_ext) + or match_filter(task, ext2, rule.mime_parts_filter_ext) then + lua_util.debugm(rule.module_name, task, '%s: extension matched: %s', rule.log_prefix, ext) + extension_check = true + end + if match_filter(task, fname, rule.mime_parts_filter_regex) then + content_type_check = true + end + end + -- check content type regex matching + if content_type ~= nil and content_subtype ~= nil then + if match_filter(task, content_type..'/'..content_subtype, rule.mime_parts_filter_regex) then + lua_util.debugm(rule.module_name, task, '%s: regex ct: %s', rule.log_prefix, + content_type..'/'..content_subtype) + content_type_check = true + end + end + end + + -- check text_part has more words than text_part_min_words_check + if rule.text_part_min_words and p:is_text() then + text_part_min_words_check = p:get_words_count() >= tonumber(rule.text_part_min_words) + end + + return (rule.scan_image_mime and p:is_image()) + or (rule.scan_text_mime and text_part_min_words_check) + or (p:is_attachment() and rule.scan_all_mime_parts ~= false) + or extension_check + or content_type_check + end + + return fun.filter(filter_func, task:get_parts()) +end +exports.log_clean = log_clean exports.yield_result = yield_result exports.match_patterns = match_patterns exports.need_av_check = need_av_check exports.check_av_cache = check_av_cache exports.save_av_cache = save_av_cache -exports.text_parts_min_words = text_parts_min_words +exports.create_regex_table = create_regex_table +exports.check_parts_match = check_parts_match setmetatable(exports, { __call = function(t, override) @@ -210,4 +325,4 @@ setmetatable(exports, { end, }) -return exports
\ No newline at end of file +return exports diff --git a/lualib/lua_scanners/dcc.lua b/lualib/lua_scanners/dcc.lua index b6c3d552f..e5c0a1964 100644 --- a/lualib/lua_scanners/dcc.lua +++ b/lualib/lua_scanners/dcc.lua @@ -29,7 +29,7 @@ local fun = require "fun" local N = 'dcc' -local function dcc_check(task, content, _, rule) +local function dcc_check(task, content, digest, rule) local function dcc_check_uncached () local upstream = rule.upstreams:get_upstream_round_robin() local addr = upstream:get_addr() @@ -81,8 +81,7 @@ local function dcc_check(task, content, _, rule) local function dcc_callback(err, data, conn) - if err then - + local function dcc_requery() -- set current upstream to fail because an error occurred upstream:fail() @@ -91,11 +90,15 @@ local function dcc_check(task, content, _, rule) retransmits = retransmits - 1 + lua_util.debugm(rule.N, task, '%s: Request Error: %s - retries left: %s', + rule.log_prefix, err, retransmits) + -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() addr = upstream:get_addr() - lua_util.debugm(N, task, '%s: retry IP: %s', rule.log_prefix, addr) + lua_util.debugm(rule.N, task, '%s: retry IP: %s:%s', + rule.log_prefix, addr, addr:get_port()) tcp.request({ task = task, @@ -110,35 +113,35 @@ local function dcc_check(task, content, _, rule) fuz2_max = 999999, }) else - rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits exceed', rule['symbol'], rule['type']) - task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan and retransmits exceed') + rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits '.. + 'exceed', rule.log_prefix) + task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan and '.. + 'retransmits exceed') end + end + + if err then + + dcc_requery() + else -- Parse the response if upstream then upstream:ok() end local _,_,result,disposition,header = tostring(data):find("(.-)\n(.-)\n(.-)\n") - lua_util.debugm(N, task, 'DCC result=%1 disposition=%2 header="%3"', + lua_util.debugm(rule.N, task, 'DCC result=%1 disposition=%2 header="%3"', result, disposition, header) if header then local _,_,info = header:find("; (.-)$") - if (result == 'R') then -- Reject common.yield_result(task, rule, info, rule.default_score) + common.save_av_cache(task, digest, rule, info, rule.default_score) elseif (result == 'T') then -- Temporary failure rspamd_logger.warnx(task, 'DCC returned a temporary failure result: %s', result) - task:insert_result(rule.symbol_fail, - 0.0, - 'tempfail:' .. result) + dcc_requery() elseif result == 'A' then - if rule.log_clean then - rspamd_logger.infox(task, '%s: clean, returned result A - info: %s', - rule.log_prefix, info) - else - lua_util.debugm(N, task, '%s: returned result A - info: %s', - rule.log_prefix, info) local opts = {} local score = 0.0 @@ -188,25 +191,36 @@ local function dcc_check(task, content, _, rule) task:insert_result(rule.symbol_bulk, score, opts) + common.save_av_cache(task, digest, rule, opts, score) + else + common.save_av_cache(task, digest, rule, 'OK') + if rule.log_clean then + rspamd_logger.infox(task, '%s: clean, returned result A - info: %s', + rule.log_prefix, info) + else + lua_util.debugm(rule.N, task, '%s: returned result A - info: %s', + rule.log_prefix, info) end end elseif result == 'G' then -- do nothing + common.save_av_cache(task, digest, rule, 'OK') if rule.log_clean then rspamd_logger.infox(task, '%s: clean, returned result G - info: %s', rule.log_prefix, info) else - lua_util.debugm(N, task, '%s: returned result G - info: %s', rule.log_prefix, info) + lua_util.debugm(rule.N, task, '%s: returned result G - info: %s', rule.log_prefix, info) end elseif result == 'S' then -- do nothing + common.save_av_cache(task, digest, rule, 'OK') if rule.log_clean then rspamd_logger.infox(task, '%s: clean, returned result S - info: %s', rule.log_prefix, info) else - lua_util.debugm(N, task, '%s: returned result S - info: %s', rule.log_prefix, info) + lua_util.debugm(rule.N, task, '%s: returned result S - info: %s', rule.log_prefix, info) end else -- Unknown result - rspamd_logger.warnx(task, 'DCC result error: %1', result); + rspamd_logger.warnx(task, '%s: result error: %1', rule.log_prefix, result); task:insert_result(rule.symbol_fail, 0.0, 'error: ' .. result) @@ -222,21 +236,30 @@ local function dcc_check(task, content, _, rule) timeout = rule.timeout or 2.0, shutdown = true, data = request_data, - callback = dcc_callback + callback = dcc_callback, + body_max = 999999, + fuz1_max = 999999, + fuz2_max = 999999, }) end - if common.need_av_check(task, content, rule, N) then - dcc_check_uncached() + if common.need_av_check(task, content, rule) then + if common.check_av_cache(task, digest, rule, dcc_check_uncached) then + return + else + dcc_check_uncached() + end end end local function dcc_config(opts) local dcc_conf = { + N = N, default_port = 10045, timeout = 5.0, log_clean = false, retransmits = 2, + cache_expire = 7200, -- expire redis in 2h message = '${SCANNER}: bulk message found: "${VIRUS}"', detection_category = "hash", default_score = 1, @@ -252,8 +275,16 @@ local function dcc_config(opts) dcc_conf = lua_util.override_defaults(dcc_conf, opts) + if not dcc_conf.prefix then + dcc_conf.prefix = 'rs_' .. dcc_conf.name .. '_' + end + if not dcc_conf.log_prefix then - dcc_conf.log_prefix = N + if dcc_conf.name:lower() == dcc_conf.type:lower() then + dcc_conf.log_prefix = dcc_conf.name + else + dcc_conf.log_prefix = dcc_conf.name .. ' (' .. dcc_conf.type .. ')' + end end if not dcc_conf.servers and dcc_conf.socket then @@ -271,7 +302,7 @@ local function dcc_config(opts) dcc_conf.default_port) if dcc_conf.upstreams then - lua_util.add_debug_alias('external_services', N) + lua_util.add_debug_alias('external_services', dcc_conf.N) return dcc_conf end @@ -285,5 +316,5 @@ return { description = 'dcc bulk scanner', configure = dcc_config, check = dcc_check, - name = 'dcc' -}
\ No newline at end of file + name = N +} diff --git a/lualib/lua_scanners/fprot.lua b/lualib/lua_scanners/fprot.lua index bfa0b4d33..2004d8aa0 100644 --- a/lualib/lua_scanners/fprot.lua +++ b/lualib/lua_scanners/fprot.lua @@ -31,9 +31,10 @@ local default_message = '${SCANNER}: virus found: "${VIRUS}"' local function fprot_config(opts) local fprot_conf = { - scan_mime_parts = true; - scan_text_mime = false; - scan_image_mime = false; + N = N, + scan_mime_parts = true, + scan_text_mime = false, + scan_image_mime = false, default_port = 10200, timeout = 5.0, -- FIXME: this will break task_timeout! log_clean = false, @@ -43,12 +44,18 @@ local function fprot_config(opts) message = default_message, } - for k,v in pairs(opts) do - fprot_conf[k] = v - end + fprot_conf = lua_util.override_defaults(fprot_conf, opts) if not fprot_conf.prefix then - fprot_conf.prefix = 'rs_fp' + fprot_conf.prefix = 'rs_' .. fprot_conf.name .. '_' + end + + if not fprot_conf.log_prefix then + if fprot_conf.name:lower() == fprot_conf.type:lower() then + fprot_conf.log_prefix = fprot_conf.name + else + fprot_conf.log_prefix = fprot_conf.name .. ' (' .. fprot_conf.type .. ')' + end end if not fprot_conf['servers'] then @@ -62,7 +69,7 @@ local function fprot_config(opts) fprot_conf.default_port) if fprot_conf['upstreams'] then - lua_util.add_debug_alias('antivirus', N) + lua_util.add_debug_alias('antivirus', fprot_conf.N) return fprot_conf end @@ -96,7 +103,7 @@ local function fprot_check(task, content, digest, rule) upstream = rule.upstreams:get_upstream_round_robin() addr = upstream:get_addr() - lua_util.debugm(N, task, '%s [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) + lua_util.debugm(rule.N, task, '%s [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) tcp.request({ task = task, @@ -133,12 +140,12 @@ local function fprot_check(task, content, digest, rule) if not vname then rspamd_logger.errx(task, 'Unhandled response: %s', data) else - common.yield_result(task, rule, vname, N) + common.yield_result(task, rule, vname) cached = vname end end if cached then - common.save_av_cache(task, digest, rule, cached, N) + common.save_av_cache(task, digest, rule, cached) end end end @@ -154,8 +161,8 @@ local function fprot_check(task, content, digest, rule) }) end - if common.need_av_check(task, content, rule, N) then - if common.check_av_cache(task, digest, rule, fprot_check_uncached, N) then + if common.need_av_check(task, content, rule) then + if common.check_av_cache(task, digest, rule, fprot_check_uncached) then return else fprot_check_uncached() @@ -168,5 +175,5 @@ return { description = 'fprot antivirus', configure = fprot_config, check = fprot_check, - name = 'fprot' -}
\ No newline at end of file + name = N +} diff --git a/lualib/lua_scanners/icap.lua b/lualib/lua_scanners/icap.lua new file mode 100644 index 000000000..8810681f9 --- /dev/null +++ b/lualib/lua_scanners/icap.lua @@ -0,0 +1,301 @@ +--[[ +Copyright (c) 2018, Vsevolod Stakhov <vsevolod@highsecure.ru> +Copyright (c) 2019, Carsten Rosenberg <c.rosenberg@heinlein-support.de> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + +--[[[ +-- @module icap +-- This module contains icap access functions. +-- Currently tested with Symantec, Sophos Savdi, ClamAV/c-icap +--]] + +local lua_util = require "lua_util" +local tcp = require "rspamd_tcp" +local upstream_list = require "rspamd_upstream_list" +local rspamd_logger = require "rspamd_logger" +local common = require "lua_scanners/common" + +local N = 'icap' + +local function icap_check(task, content, digest, rule) + local function icap_check_uncached () + local upstream = rule.upstreams:get_upstream_round_robin() + local addr = upstream:get_addr() + local retransmits = rule.retransmits + local respond_headers = {} + + -- Build the icap queries + local options_request = { + "OPTIONS icap://" .. addr:to_string() .. ":" .. addr:get_port() .. "/" .. rule.scheme .. " ICAP/1.0\r\n", + "Host:" .. addr:to_string() .. "\r\n", + "User-Agent: Rspamd\r\n", + "Encapsulated: null-body=0\r\n\r\n", + } + local size = string.format("%x", tonumber(#content)) + lua_util.debugm(rule.N, task, '%s: size: %s', rule.log_prefix, size) + + local function get_respond_query() + table.insert(respond_headers, 1, 'RESPMOD icap://' .. addr:to_string() .. ':' .. addr:get_port() .. '/' + .. rule.scheme .. ' ICAP/1.0\r\n') + table.insert(respond_headers, 'Encapsulated: res-body=0\r\n') + table.insert(respond_headers, '\r\n') + table.insert(respond_headers, size .. '\r\n') + table.insert(respond_headers, content) + table.insert(respond_headers, '\r\n0\r\n\r\n') + return respond_headers + end + + local function add_respond_header(name, value) + table.insert(respond_headers, name .. ': ' .. value .. '\r\n' ) + end + + local function icap_result_header_table(result) + local icap_headers = {} + for s in result:gmatch("[^\r\n]+") do + if string.find(s, '^ICAP') then + icap_headers['icap'] = s + end + if string.find(s, '[%a%d-+]-: ') then + local _,_,key,value = tostring(s):find("([%a%d-+]-):%s(.+)") + icap_headers[key] = value + end + end + lua_util.debugm(rule.N, task, '%s: icap_headers: %s', rule.log_prefix, icap_headers) + return icap_headers + end + + local function icap_parse_result(icap_headers) + + local threat_string = {} + + --[[ + @ToDo: handle type in response + + Generic Strings: + X-Infection-Found: Type=0; Resolution=2; Threat=Troj/DocDl-OYC; + X-Infection-Found: Type=0; Resolution=2; Threat=W97M.Downloader; + Symantec String: + X-Infection-Found: Type=2; Resolution=2; Threat=Container size violation + X-Infection-Found: Type=2; Resolution=2; Threat=Encrypted container violation; + Sophos Strings: + X-Virus-ID: Troj/DocDl-OYC + ]] -- + + local pattern_symbols + local match + + if icap_headers['X-Infection-Found'] ~= nil then + pattern_symbols = "(Type%=%d; .* Threat%=)(.*)([;]+)" + match = string.gsub(icap_headers['X-Infection-Found'], pattern_symbols, "%2") + lua_util.debugm(rule.N, task, '%s: icap X-Infection-Found: %s', rule.log_prefix, match) + table.insert(threat_string, match) + elseif icap_headers['X-Virus-ID'] ~= nil then + lua_util.debugm(rule.N, task, '%s: icap X-Virus-ID: %s', rule.log_prefix, icap_headers['X-Virus-ID']) + table.insert(threat_string, icap_headers['X-Virus-ID']) + end + + if #threat_string > 0 then + common.yield_result(task, rule, threat_string, rule.default_score) + common.save_av_cache(task, digest, rule, threat_string, rule.default_score) + else + common.save_av_cache(task, digest, rule, 'OK', 0) + common.log_clean(task, rule) + end + end + + local function icap_r_respond_cb(err, data, conn) + local result = tostring(data) + conn:close() + + local icap_headers = icap_result_header_table(result) + -- Find ICAP/1.x 2xx response + if string.find(icap_headers.icap, 'ICAP%/1%.. 2%d%d') then + icap_parse_result(icap_headers) + elseif string.find(icap_headers.icap, 'ICAP%/1%.. [45]%d%d') then + -- Find ICAP/1.x 5/4xx response + --[[ + Symantec String: + ICAP/1.0 539 Aborted - No AV scanning license + SquidClamAV/C-ICAP: + ICAP/1.0 500 Server error + ]]-- + rspamd_logger.errx(task, '%s: ICAP ERROR: %s', rule.log_prefix, icap_headers.icap) + task:insert_result(rule.symbol_fail, 0.0, icap_headers.icap) + return false + else + rspamd_logger.errx(task, '%s: unhandled response |%s|', + rule.log_prefix, string.gsub(result, "\r\n", ", ")) + task:insert_result(rule.symbol_fail, 0.0, 'unhandled icap response: ' .. icap_headers.icap) + end + end + + local function icap_w_respond_cb(err, conn) + conn:add_read(icap_r_respond_cb, '\r\n\r\n') + end + + local function icap_r_options_cb(err, data, conn) + local icap_headers = icap_result_header_table(tostring(data)) + + if string.find(icap_headers.icap, 'ICAP%/1%.. 2%d%d') then + if icap_headers['Methods'] ~= nil and string.find(icap_headers['Methods'], 'RESPMOD') then + if icap_headers['Allow'] ~= nil and string.find(icap_headers['Allow'], '204') then + add_respond_header('Allow', '204') + end + conn:add_write(icap_w_respond_cb, get_respond_query()) + else + rspamd_logger.errx(task, '%s: RESPMOD method not advertised: Methods: %s', + rule.log_prefix, icap_headers['Methods']) + task:insert_result(rule.symbol_fail, 0.0, 'NO RESPMOD') + end + else + rspamd_logger.errx(task, '%s: OPTIONS query failed: %s', + rule.log_prefix, icap_headers.icap) + task:insert_result(rule.symbol_fail, 0.0, 'OPTIONS query failed') + end + end + + local function icap_callback(err, conn) + + local function icap_requery(error) + -- set current upstream to fail because an error occurred + upstream:fail() + + -- retry with another upstream until retransmits exceeds + if retransmits > 0 then + + retransmits = retransmits - 1 + + lua_util.debugm(rule.N, task, '%s: Request Error: %s - retries left: %s', + rule.log_prefix, error, retransmits) + + -- Select a different upstream! + upstream = rule.upstreams:get_upstream_round_robin() + addr = upstream:get_addr() + + lua_util.debugm(rule.N, task, '%s: retry IP: %s:%s', + rule.log_prefix, addr, addr:get_port()) + + tcp.request({ + task = task, + host = addr:to_string(), + port = addr:get_port(), + timeout = rule.timeout, + stop_pattern = '\r\n', + data = options_request, + read = false, + callback = icap_callback, + }) + else + rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits '.. + 'exceed - err: %s', rule.log_prefix, error) + task:insert_result(rule.symbol_fail, 0.0, 'failed - err: ' .. error) + end + end + + if err then + icap_requery(err) + else + -- set upstream ok + if upstream then upstream:ok() end + conn:add_read(icap_r_options_cb, '\r\n\r\n') + end + end + + tcp.request({ + task = task, + host = addr:to_string(), + port = addr:get_port(), + timeout = rule.timeout, + stop_pattern = '\r\n', + data = options_request, + read = false, + callback = icap_callback, + }) + end + if common.need_av_check(task, content, rule) then + if common.check_av_cache(task, digest, rule, icap_check_uncached) then + return + else + icap_check_uncached() + end + end +end + + +local function icap_config(opts) + + local icap_conf = { + N = N, + scan_mime_parts = true, + scan_all_mime_parts = true, + scan_text_mime = false, + scan_image_mime = false, + scheme = "scan", + default_port = 4020, + timeout = 10.0, + log_clean = false, + retransmits = 2, + cache_expire = 7200, -- expire redis in one hour + message = '${SCANNER}: threat found with icap scanner: "${VIRUS}"', + detection_category = "virus", + default_score = 1, + action = false, + } + + icap_conf = lua_util.override_defaults(icap_conf, opts) + + if not icap_conf.prefix then + icap_conf.prefix = 'rs_' .. icap_conf.name .. '_' + end + + if not icap_conf.log_prefix then + icap_conf.log_prefix = icap_conf.name .. ' (' .. icap_conf.type .. ')' + end + + if not icap_conf.log_prefix then + if icap_conf.name:lower() == icap_conf.type:lower() then + icap_conf.log_prefix = icap_conf.name + else + icap_conf.log_prefix = icap_conf.name .. ' (' .. icap_conf.type .. ')' + end + end + + if not icap_conf.servers then + rspamd_logger.errx(rspamd_config, 'no servers defined') + + return nil + end + + icap_conf.upstreams = upstream_list.create(rspamd_config, + icap_conf.servers, + icap_conf.default_port) + + if icap_conf.upstreams then + lua_util.add_debug_alias('external_services', icap_conf.N) + return icap_conf + end + + rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', + icap_conf.servers) + return nil +end + +return { + type = {N,'virus', 'virus', 'scanner'}, + description = 'generic icap antivirus', + configure = icap_config, + check = icap_check, + name = N +} diff --git a/lualib/lua_scanners/init.lua b/lualib/lua_scanners/init.lua index f769eb5a5..0c2857e01 100644 --- a/lualib/lua_scanners/init.lua +++ b/lualib/lua_scanners/init.lua @@ -39,6 +39,8 @@ require_scanner('sophos') -- Other scanners require_scanner('dcc') +require_scanner('oletools') +require_scanner('icap') exports.add_scanner = function(name, t, conf_func, check_func) assert(type(conf_func) == 'function' and type(check_func) == 'function', @@ -59,4 +61,4 @@ exports.filter = function(t) end, exports)) end -return exports
\ No newline at end of file +return exports diff --git a/lualib/lua_scanners/kaspersky_av.lua b/lualib/lua_scanners/kaspersky_av.lua index 40cb6eb76..f06e59cd7 100644 --- a/lualib/lua_scanners/kaspersky_av.lua +++ b/lualib/lua_scanners/kaspersky_av.lua @@ -32,9 +32,10 @@ local default_message = '${SCANNER}: virus found: "${VIRUS}"' local function kaspersky_config(opts) local kaspersky_conf = { - scan_mime_parts = true; - scan_text_mime = false; - scan_image_mime = false; + N = N, + scan_mime_parts = true, + scan_text_mime = false, + scan_image_mime = false, product_id = 0, log_clean = false, timeout = 5.0, @@ -43,11 +44,22 @@ local function kaspersky_config(opts) message = default_message, detection_category = "virus", tmpdir = '/tmp', - prefix = 'rs_ak', } kaspersky_conf = lua_util.override_defaults(kaspersky_conf, opts) + if not kaspersky_conf.prefix then + kaspersky_conf.prefix = 'rs_' .. kaspersky_conf.name .. '_' + end + + if not kaspersky_conf.log_prefix then + if kaspersky_conf.name:lower() == kaspersky_conf.type:lower() then + kaspersky_conf.log_prefix = kaspersky_conf.name + else + kaspersky_conf.log_prefix = kaspersky_conf.name .. ' (' .. kaspersky_conf.type .. ')' + end + end + if not kaspersky_conf['servers'] then rspamd_logger.errx(rspamd_config, 'no servers defined') @@ -58,7 +70,7 @@ local function kaspersky_config(opts) kaspersky_conf['servers'], 0) if kaspersky_conf['upstreams'] then - lua_util.add_debug_alias('antivirus', N) + lua_util.add_debug_alias('antivirus', kaspersky_conf.N) return kaspersky_conf end @@ -110,7 +122,7 @@ local function kaspersky_check(task, content, digest, rule) upstream = rule.upstreams:get_upstream_round_robin() addr = upstream:get_addr() - lua_util.debugm(N, task, + lua_util.debugm(rule.N, task, '%s [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) tcp.request({ @@ -134,21 +146,15 @@ local function kaspersky_check(task, content, digest, rule) upstream:ok() data = tostring(data) local cached - lua_util.debugm(N, task, '%s [%s]: got reply: %s', + lua_util.debugm(rule.N, task, '%s [%s]: got reply: %s', rule['symbol'], rule['type'], data) - if data == 'stream: OK' then + if data == 'stream: OK' or data == fname .. ': OK' then cached = 'OK' - if rule['log_clean'] then - rspamd_logger.infox(task, '%s [%s]: message or mime_part is clean', - rule['symbol'], rule['type']) - else - lua_util.debugm(N, task, '%s [%s]: message or mime_part is clean', - rule['symbol'], rule['type']) - end + common.log_clean(task, rule) else local vname = string.match(data, ': (.+) FOUND') if vname then - common.yield_result(task, rule, vname, N) + common.yield_result(task, rule, vname) cached = vname else rspamd_logger.errx(task, 'unhandled response: %s', data) @@ -156,7 +162,7 @@ local function kaspersky_check(task, content, digest, rule) end end if cached then - common.save_av_cache(task, digest, rule, cached, N) + common.save_av_cache(task, digest, rule, cached) end end end @@ -172,8 +178,8 @@ local function kaspersky_check(task, content, digest, rule) }) end - if common.need_av_check(task, content, rule, N) then - if common.check_av_cache(task, digest, rule, kaspersky_check_uncached, N) then + if common.need_av_check(task, content, rule) then + if common.check_av_cache(task, digest, rule, kaspersky_check_uncached) then return else kaspersky_check_uncached() @@ -186,5 +192,5 @@ return { description = 'kaspersky antivirus', configure = kaspersky_config, check = kaspersky_check, - name = 'kaspersky' -}
\ No newline at end of file + name = N +} diff --git a/lualib/lua_scanners/oletools.lua b/lualib/lua_scanners/oletools.lua new file mode 100644 index 000000000..4ee5f040b --- /dev/null +++ b/lualib/lua_scanners/oletools.lua @@ -0,0 +1,302 @@ +--[[ +Copyright (c) 2018, Vsevolod Stakhov <vsevolod@highsecure.ru> +Copyright (c) 2018, Carsten Rosenberg <c.rosenberg@heinlein-support.de> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + +--[[[ +-- @module oletools +-- This module contains oletools access functions. +-- Olefy is needed: https://github.com/HeinleinSupport/olefy +--]] + +local lua_util = require "lua_util" +local tcp = require "rspamd_tcp" +local upstream_list = require "rspamd_upstream_list" +local rspamd_logger = require "rspamd_logger" +local ucl = require "ucl" +local common = require "lua_scanners/common" + +local N = 'oletools' + +local function oletools_check(task, content, digest, rule) + local function oletools_check_uncached () + local upstream = rule.upstreams:get_upstream_round_robin() + local addr = upstream:get_addr() + local retransmits = rule.retransmits + local protocol = 'OLEFY/1.0\nMethod: oletools\nRspamd-ID: ' .. task:get_uid() .. '\n\n' + + local function oletools_callback(err, data) + + local function oletools_requery(error) + -- set current upstream to fail because an error occurred + upstream:fail() + + -- retry with another upstream until retransmits exceeds + if retransmits > 0 then + + retransmits = retransmits - 1 + + lua_util.debugm(rule.N, task, '%s: Request Error: %s - retries left: %s', + rule.log_prefix, error, retransmits) + + -- Select a different upstream! + upstream = rule.upstreams:get_upstream_round_robin() + addr = upstream:get_addr() + + lua_util.debugm(rule.N, task, '%s: retry IP: %s:%s', + rule.log_prefix, addr, addr:get_port()) + + tcp.request({ + task = task, + host = addr:to_string(), + port = addr:get_port(), + timeout = rule.timeout, + shutdown = true, + data = { protocol, content }, + callback = oletools_callback, + }) + else + rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits '.. + 'exceed - err: %s', rule.log_prefix, error) + task:insert_result(rule.symbol_fail, 0.0, 'failed - err: ' .. error) + end + end + + if err then + + oletools_requery(err) + + else + -- Parse the response + if upstream then upstream:ok() end + + data = tostring(data) + + local ucl_parser = ucl.parser() + local ok, ucl_err = ucl_parser:parse_string(tostring(data)) + if not ok then + rspamd_logger.errx(task, "%s: error parsing json response: %s", + rule.log_prefix, ucl_err) + return + end + + local result = ucl_parser:get_object() + + local oletools_rc = { + [0] = 'RETURN_OK', + [1] = 'RETURN_WARNINGS', + [2] = 'RETURN_WRONG_ARGS', + [3] = 'RETURN_FILE_NOT_FOUND', + [4] = 'RETURN_XGLOB_ERR', + [5] = 'RETURN_OPEN_ERROR', + [6] = 'RETURN_PARSE_ERROR', + [7] = 'RETURN_SEVERAL_ERRS', + [8] = 'RETURN_UNEXPECTED', + [9] = 'RETURN_ENCRYPTED', + } + + if result[1].error ~= nil then + rspamd_logger.errx(task, '%s: ERROR found: %s', rule.log_prefix, + result[1].error) + if result[1].error == 'File too small' then + common.save_av_cache(task, digest, rule, 'OK') + common.log_clean(task, rule, 'File too small to be scanned for macros') + else + oletools_requery(result[1].error) + end + elseif result[3]['return_code'] == 9 then + rspamd_logger.warnx(task, '%s: File is encrypted.', rule.log_prefix) + elseif result[3]['return_code'] > 6 then + rspamd_logger.errx(task, '%s: Error Returned: %s', + rule.log_prefix, oletools_rc[result[3]['return_code']]) + rspamd_logger.errx(task, '%s: Error message: %s', + rule.log_prefix, result[2]['message']) + task:insert_result(rule.symbol_fail, 0.0, 'failed - err: ' .. oletools_rc[result[3]['return_code']]) + elseif result[3]['return_code'] > 1 then + rspamd_logger.errx(task, '%s: Error message: %s', + rule.log_prefix, result[2]['message']) + oletools_requery(oletools_rc[result[3]['return_code']]) + elseif #result[2]['analysis'] == 0 and #result[2]['macros'] == 0 then + rspamd_logger.warnx(task, '%s: maybe unhandled python or oletools error', rule.log_prefix) + task:insert_result(rule.symbol_fail, 0.0, 'oletools unhandled error') + elseif result[2]['analysis'] == 'null' and #result[2]['macros'] == 0 then + common.save_av_cache(task, digest, rule, 'OK') + common.log_clean(task, rule, 'No macro found') + elseif #result[2]['macros'] > 0 then + -- M=Macros, A=Auto-executable, S=Suspicious keywords, I=IOCs, + -- H=Hex strings, B=Base64 strings, D=Dridex strings, V=VBA strings + local m_exist = 'M' + local m_autoexec = '-' + local m_suspicious = '-' + local m_iocs = '-' + local m_hex = '-' + local m_base64 = '-' + local m_dridex = '-' + local m_vba = '-' + + lua_util.debugm(rule.N, task, '%s: filename: %s', rule.log_prefix, result[2]['file']) + lua_util.debugm(rule.N, task, '%s: type: %s', rule.log_prefix, result[2]['type']) + + for _,m in ipairs(result[2]['macros']) do + lua_util.debugm(rule.N, task, '%s: macros found - code: %s, ole_stream: %s, '.. + 'vba_filename: %s', rule.log_prefix, m.code, m.ole_stream, m.vba_filename) + end + + local analysis_keyword_table = {} + + for _,a in ipairs(result[2]['analysis']) do + lua_util.debugm(rule.N, task, '%s: threat found - type: %s, keyword: %s, '.. + 'description: %s', rule.log_prefix, a.type, a.keyword, a.description) + if a.type == 'AutoExec' then + m_autoexec = 'A' + table.insert(analysis_keyword_table, a.keyword) + elseif a.type == 'Suspicious' then + m_suspicious = 'S' + if a.keyword ~= 'Base64 Strings' and a.keyword ~= 'Hex Strings' + then + table.insert(analysis_keyword_table, a.keyword) + end + elseif a.type == 'IOCs' then + m_iocs = 'I' + elseif a.type == 'Hex strings' then + m_hex = 'H' + elseif a.type == 'Base64 strings' then + m_base64 = 'B' + elseif a.type == 'Dridex strings' then + m_dridex = 'D' + elseif a.type == 'VBA strings' then + m_vba = 'V' + end + end + + --lua_util.debugm(N, task, '%s: analysis_keyword_table: %s', rule.log_prefix, analysis_keyword_table) + + if rule.extended == false and m_autoexec == 'A' and m_suspicious == 'S' then + -- use single string as virus name + local threat = 'AutoExec + Suspicious (' .. table.concat(analysis_keyword_table, ',') .. ')' + lua_util.debugm(rule.N, task, '%s: threat result: %s', rule.log_prefix, threat) + common.yield_result(task, rule, threat, rule.default_score) + common.save_av_cache(task, digest, rule, threat, rule.default_score) + + elseif rule.extended == true and #analysis_keyword_table > 0 then + -- report any flags (types) and any most keywords as individual virus name + + local flags = m_exist .. + m_autoexec .. + m_suspicious .. + m_iocs .. + m_hex .. + m_base64 .. + m_dridex .. + m_vba + table.insert(analysis_keyword_table, 1, flags) + + lua_util.debugm(rule.N, task, '%s: extended threat result: %s', + rule.log_prefix, table.concat(analysis_keyword_table, ',')) + + common.yield_result(task, rule, analysis_keyword_table, rule.default_score) + common.save_av_cache(task, digest, rule, analysis_keyword_table, rule.default_score) + else + common.save_av_cache(task, digest, rule, 'OK') + common.log_clean(task, rule, 'Scanned Macro is OK') + end + + else + rspamd_logger.warnx(task, '%s: unhandled response', rule.log_prefix) + task:insert_result(rule.symbol_fail, 0.0, 'unhandled response') + end + end + end + + tcp.request({ + task = task, + host = addr:to_string(), + port = addr:get_port(), + timeout = rule.timeout, + shutdown = true, + data = { protocol, content }, + callback = oletools_callback, + }) + + end + if common.need_av_check(task, content, rule) then + if common.check_av_cache(task, digest, rule, oletools_check_uncached) then + return + else + oletools_check_uncached() + end + end +end + +local function oletools_config(opts) + + local oletools_conf = { + N = N, + scan_mime_parts = false, + scan_text_mime = false, + scan_image_mime = false, + default_port = 5954, + timeout = 15.0, + log_clean = false, + retransmits = 2, + cache_expire = 86400, -- expire redis in 1d + message = '${SCANNER}: Oletools threat message found: "${VIRUS}"', + detection_category = "office macro", + default_score = 1, + action = false, + extended = false, + } + + oletools_conf = lua_util.override_defaults(oletools_conf, opts) + + if not oletools_conf.prefix then + oletools_conf.prefix = 'rs_' .. oletools_conf.name .. '_' + end + + if not oletools_conf.log_prefix then + if oletools_conf.name:lower() == oletools_conf.type:lower() then + oletools_conf.log_prefix = oletools_conf.name + else + oletools_conf.log_prefix = oletools_conf.name .. ' (' .. oletools_conf.type .. ')' + end + end + + if not oletools_conf.servers then + rspamd_logger.errx(rspamd_config, 'no servers defined') + + return nil + end + + oletools_conf.upstreams = upstream_list.create(rspamd_config, + oletools_conf.servers, + oletools_conf.default_port) + + if oletools_conf.upstreams then + lua_util.add_debug_alias('external_services', oletools_conf.N) + return oletools_conf + end + + rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', + oletools_conf.servers) + return nil +end + +return { + type = {N,'attachment scanner', 'hash', 'scanner'}, + description = 'oletools office macro scanner', + configure = oletools_config, + check = oletools_check, + name = N +} diff --git a/lualib/lua_scanners/savapi.lua b/lualib/lua_scanners/savapi.lua index fab9de31b..1393cd027 100644 --- a/lualib/lua_scanners/savapi.lua +++ b/lualib/lua_scanners/savapi.lua @@ -32,9 +32,10 @@ local default_message = '${SCANNER}: virus found: "${VIRUS}"' local function savapi_config(opts) local savapi_conf = { - scan_mime_parts = true; - scan_text_mime = false; - scan_image_mime = false; + N = N, + scan_mime_parts = true, + scan_text_mime = false, + scan_image_mime = false, default_port = 4444, -- note: You must set ListenAddress in savapi.conf product_id = 0, log_clean = false, @@ -46,12 +47,18 @@ local function savapi_config(opts) tmpdir = '/tmp', } - for k,v in pairs(opts) do - savapi_conf[k] = v - end + savapi_conf = lua_util.override_defaults(savapi_conf, opts) if not savapi_conf.prefix then - savapi_conf.prefix = 'rs_ap' + savapi_conf.prefix = 'rs_' .. savapi_conf.name .. '_' + end + + if not savapi_conf.log_prefix then + if savapi_conf.name:lower() == savapi_conf.type:lower() then + savapi_conf.log_prefix = savapi_conf.name + else + savapi_conf.log_prefix = savapi_conf.name .. ' (' .. savapi_conf.type .. ')' + end end if not savapi_conf['servers'] then @@ -65,7 +72,7 @@ local function savapi_config(opts) savapi_conf.default_port) if savapi_conf['upstreams'] then - lua_util.add_debug_alias('antivirus', N) + lua_util.add_debug_alias('antivirus', savapi_conf.N) return savapi_conf end @@ -112,15 +119,15 @@ local function savapi_check(task, content, digest, rule) for virus,_ in pairs(vnames) do table.insert(vnames_reordered, virus) end - lua_util.debugm(N, task, "%s: number of virus names found %s", rule['type'], #vnames_reordered) + lua_util.debugm(rule.N, task, "%s: number of virus names found %s", rule['type'], #vnames_reordered) if #vnames_reordered > 0 then local vname = {} for _,virus in ipairs(vnames_reordered) do table.insert(vname, virus) end - common.yield_result(task, rule, vname, N) - common.save_av_cache(task, digest, rule, vname, N) + common.yield_result(task, rule, vname) + common.save_av_cache(task, digest, rule, vname) end if conn then conn:close() @@ -129,7 +136,7 @@ local function savapi_check(task, content, digest, rule) local function savapi_scan2_cb(err, data, conn) local result = tostring(data) - lua_util.debugm(N, task, "%s: got reply: %s", + lua_util.debugm(rule.N, task, "%s: got reply: %s", rule['type'], result) -- Terminal response - clean @@ -137,7 +144,7 @@ local function savapi_check(task, content, digest, rule) if rule['log_clean'] then rspamd_logger.infox(task, '%s: message or mime_part is clean', rule['type']) end - common.save_av_cache(task, digest, rule, 'OK', N) + common.save_av_cache(task, digest, rule, 'OK') conn:add_write(savapi_fin_cb, 'QUIT\n') -- Terminal response - infected @@ -171,7 +178,7 @@ local function savapi_check(task, content, digest, rule) local function savapi_greet2_cb(err, data, conn) local result = tostring(data) if string.find(result, '100 PRODUCT') then - lua_util.debugm(N, task, "%s: scanning file: %s", + lua_util.debugm(rule.N, task, "%s: scanning file: %s", rule['type'], fname) conn:add_write(savapi_scan1_cb, {string.format('SCAN %s\n', fname)}) @@ -201,7 +208,7 @@ local function savapi_check(task, content, digest, rule) upstream = rule.upstreams:get_upstream_round_robin() addr = upstream:get_addr() - lua_util.debugm(N, task, '%s [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) + lua_util.debugm(rule.N, task, '%s [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) tcp.request({ task = task, @@ -236,8 +243,8 @@ local function savapi_check(task, content, digest, rule) }) end - if common.need_av_check(task, content, rule, N) then - if common.check_av_cache(task, digest, rule, savapi_check_uncached, N) then + if common.need_av_check(task, content, rule) then + if common.check_av_cache(task, digest, rule, savapi_check_uncached) then return else savapi_check_uncached() @@ -250,5 +257,5 @@ return { description = 'savapi avira antivirus', configure = savapi_config, check = savapi_check, - name = 'savapi' -}
\ No newline at end of file + name = N +} diff --git a/lualib/lua_scanners/sophos.lua b/lualib/lua_scanners/sophos.lua index b3eafc837..3919d9449 100644 --- a/lualib/lua_scanners/sophos.lua +++ b/lualib/lua_scanners/sophos.lua @@ -31,9 +31,10 @@ local default_message = '${SCANNER}: virus found: "${VIRUS}"' local function sophos_config(opts) local sophos_conf = { - scan_mime_parts = true; - scan_text_mime = false; - scan_image_mime = false; + N = N, + scan_mime_parts = true, + scan_text_mime = false, + scan_image_mime = false, default_port = 4010, timeout = 15.0, log_clean = false, @@ -45,12 +46,18 @@ local function sophos_config(opts) savdi_report_oversize = false, } - for k,v in pairs(opts) do - sophos_conf[k] = v - end + sophos_conf = lua_util.override_defaults(sophos_conf, opts) if not sophos_conf.prefix then - sophos_conf.prefix = 'rs_sp' + sophos_conf.prefix = 'rs_' .. sophos_conf.name .. '_' + end + + if not sophos_conf.log_prefix then + if sophos_conf.name:lower() == sophos_conf.type:lower() then + sophos_conf.log_prefix = sophos_conf.name + else + sophos_conf.log_prefix = sophos_conf.name .. ' (' .. sophos_conf.type .. ')' + end end if not sophos_conf['servers'] then @@ -64,7 +71,7 @@ local function sophos_config(opts) sophos_conf.default_port) if sophos_conf['upstreams'] then - lua_util.add_debug_alias('antivirus', N) + lua_util.add_debug_alias('antivirus', sophos_conf.N) return sophos_conf end @@ -97,7 +104,7 @@ local function sophos_check(task, content, digest, rule) upstream = rule.upstreams:get_upstream_round_robin() addr = upstream:get_addr() - lua_util.debugm(N, task, '%s [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) + lua_util.debugm(rule.N, task, '%s [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) tcp.request({ task = task, @@ -114,19 +121,19 @@ local function sophos_check(task, content, digest, rule) else upstream:ok() data = tostring(data) - lua_util.debugm(N, task, '%s [%s]: got reply: %s', rule['symbol'], rule['type'], data) + lua_util.debugm(rule.N, task, '%s [%s]: got reply: %s', rule['symbol'], rule['type'], data) local vname = string.match(data, 'VIRUS (%S+) ') if vname then - common.yield_result(task, rule, vname, N) - common.save_av_cache(task, digest, rule, vname, N) + common.yield_result(task, rule, vname) + common.save_av_cache(task, digest, rule, vname) else if string.find(data, 'DONE OK') then if rule['log_clean'] then - rspamd_logger.infox(task, '%s [%s]: message or mime_part is clean', rule['symbol'], rule['type']) + rspamd_logger.infox(task, '%s: message or mime_part is clean', rule.log_prefix) else - lua_util.debugm(N, task, '%s [%s]: message or mime_part is clean', rule['symbol'], rule['type']) + lua_util.debugm(rule.N, task, '%s: message or mime_part is clean', rule.log_prefix) end - common.save_av_cache(task, digest, rule, 'OK', N) + common.save_av_cache(task, digest, rule, 'OK') -- not finished - continue elseif string.find(data, 'ACC') or string.find(data, 'OK SSSP') then conn:add_read(sophos_callback) @@ -134,15 +141,15 @@ local function sophos_check(task, content, digest, rule) elseif string.find(data, 'FAIL 0212') then rspamd_logger.infox(task, 'Message is ENCRYPTED (0212 SOPHOS_SAVI_ERROR_FILE_ENCRYPTED): %s', data) if rule['savdi_report_encrypted'] then - common.yield_result(task, rule, "SAVDI_FILE_ENCRYPTED", N) - common.save_av_cache(task, digest, rule, "SAVDI_FILE_ENCRYPTED", N) + common.yield_result(task, rule, "SAVDI_FILE_ENCRYPTED") + common.save_av_cache(task, digest, rule, "SAVDI_FILE_ENCRYPTED") end -- set pseudo virus if configured, else set fail since part was not scanned elseif string.find(data, 'REJ 4') then if rule['savdi_report_oversize'] then rspamd_logger.infox(task, 'SAVDI: Message is OVERSIZED (SSSP reject code 4): %s', data) - common.yield_result(task, rule, "SAVDI_FILE_OVERSIZED", N) - common.save_av_cache(task, digest, rule, "SAVDI_FILE_OVERSIZED", N) + common.yield_result(task, rule, "SAVDI_FILE_OVERSIZED") + common.save_av_cache(task, digest, rule, "SAVDI_FILE_OVERSIZED") else rspamd_logger.errx(task, 'SAVDI: Message is OVERSIZED (SSSP reject code 4): %s', data) task:insert_result(rule['symbol_fail'], 0.0, 'Message is OVERSIZED (SSSP reject code 4):' .. data) @@ -170,8 +177,8 @@ local function sophos_check(task, content, digest, rule) }) end - if common.need_av_check(task, content, rule, N) then - if common.check_av_cache(task, digest, rule, sophos_check_uncached, N) then + if common.need_av_check(task, content, rule) then + if common.check_av_cache(task, digest, rule, sophos_check_uncached) then return else sophos_check_uncached() @@ -184,5 +191,5 @@ return { description = 'sophos antivirus', configure = sophos_config, check = sophos_check, - name = 'sophos' -}
\ No newline at end of file + name = N +} diff --git a/src/plugins/lua/antivirus.lua b/src/plugins/lua/antivirus.lua index 2f8f948ad..0dde3e217 100644 --- a/src/plugins/lua/antivirus.lua +++ b/src/plugins/lua/antivirus.lua @@ -15,10 +15,10 @@ limitations under the License. ]] -- local rspamd_logger = require "rspamd_logger" -local rspamd_regexp = require "rspamd_regexp" local lua_util = require "lua_util" local fun = require "fun" local lua_antivirus = require("lua_scanners").filter('antivirus') +local common = require "lua_scanners/common" local redis_params local N = "antivirus" @@ -70,29 +70,29 @@ end local function add_antivirus_rule(sym, opts) - if not opts['type'] then + if not opts.type then rspamd_logger.errx(rspamd_config, 'unknown type for AV rule %s', sym) return nil end - if not opts['symbol'] then opts['symbol'] = sym:upper() end - local cfg = lua_antivirus[opts['type']] + if not opts.symbol then opts.symbol = sym:upper() end + local cfg = lua_antivirus[opts.type] if not cfg then rspamd_logger.errx(rspamd_config, 'unknown antivirus type: %s', - opts['type']) + opts.type) return nil end - if not opts['symbol_fail'] then - opts['symbol_fail'] = string.upper(opts['type']) .. '_FAIL' + if not opts.symbol_fail then + opts.symbol_fail = opts.symbol .. '_FAIL' end -- WORKAROUND for deprecated attachments_only - if opts['attachments_only'] ~= nil then - opts['scan_mime_parts'] = opts['attachments_only'] + if opts.attachments_only ~= nil then + opts.scan_mime_parts = opts.attachments_only rspamd_logger.warnx(rspamd_config, '%s [%s]: Using attachments_only is deprecated. '.. - 'Please use scan_mime_parts = %s instead', opts['symbol'], opts['type'], opts['attachments_only']) + 'Please use scan_mime_parts = %s instead', opts.symbol, opts.type, opts.attachments_only) end -- WORKAROUND for deprecated attachments_only @@ -103,52 +103,25 @@ local function add_antivirus_rule(sym, opts) if not rule then rspamd_logger.errx(rspamd_config, 'cannot configure %s for %s', - opts['type'], opts['symbol']) + opts.type, opts.symbol) return nil end - if type(opts['patterns']) == 'table' then - rule['patterns'] = {} - if opts['patterns'][1] then - for i, p in ipairs(opts['patterns']) do - if type(p) == 'table' then - local new_set = {} - for k, v in pairs(p) do - new_set[k] = rspamd_regexp.create_cached(v) - end - rule['patterns'][i] = new_set - else - rule['patterns'][i] = {} - end - end - else - for k, v in pairs(opts['patterns']) do - rule['patterns'][k] = rspamd_regexp.create_cached(v) - end - end - end + rule.patterns = common.create_regex_table(opts.patterns or {}) - if opts['whitelist'] then - rule['whitelist'] = rspamd_config:add_hash_map(opts['whitelist']) + if opts.whitelist then + rule.whitelist = rspamd_config:add_hash_map(opts.whitelist) end return function(task) if rule.scan_mime_parts then - local parts = task:get_parts() or {} - - local filter_func = function(p) - return (rule.scan_image_mime and p:is_image()) - or (rule.scan_text_mime and p:is_text()) - or (p:is_attachment()) - end fun.each(function(p) local content = p:get_content() - if content and #content > 0 then cfg.check(task, content, p:get_digest(), rule) end - end, fun.filter(filter_func, parts)) + end, common.check_parts_match(task, rule)) else cfg.check(task, task:get_content(), task:get_digest(), rule) @@ -164,6 +137,7 @@ if opts and type(opts) == 'table' then for k, m in pairs(opts) do if type(m) == 'table' and m.servers then if not m.type then m.type = k end + if not m.name then m.name = k end local cb = add_antivirus_rule(k, m) if not cb then diff --git a/src/plugins/lua/external_services.lua b/src/plugins/lua/external_services.lua index 1b6c9b752..8e101ab9a 100644 --- a/src/plugins/lua/external_services.lua +++ b/src/plugins/lua/external_services.lua @@ -1,5 +1,6 @@ --[[ Copyright (c) 2019, Vsevolod Stakhov <vsevolod@highsecure.ru> +Copyright (c) 2019, Carsten Rosenberg <c.rosenberg@heinlein-support.de> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,9 +16,10 @@ limitations under the License. ]] -- local rspamd_logger = require "rspamd_logger" -local rspamd_regexp = require "rspamd_regexp" local lua_util = require "lua_util" +local fun = require "fun" local lua_scanners = require("lua_scanners").filter('scanner') +local common = require "lua_scanners/common" local redis_params local N = "external_services" @@ -28,13 +30,72 @@ if confighelp then [[ external_services { # multiple scanners could be checked, for each we create a configuration block with an arbitrary name + + oletools { + # If set force this action if any virus is found (default unset: no action is forced) + # action = "reject"; + # If set, then rejection message is set to this value (mention single quotes) + # If `max_size` is set, messages > n bytes in size are not scanned + # max_size = 20000000; + # log_clean = true; + # servers = "127.0.0.1:10050"; + # cache_expire = 86400; + # scan_mime_parts = true; + # extended = false; + # if `patterns` is specified virus name will be matched against provided regexes and the related + # symbol will be yielded if a match is found. If no match is found, default symbol is yielded. + patterns { + # symbol_name = "pattern"; + JUST_EICAR = "^Eicar-Test-Signature$"; + } + # mime-part regex matching in content-type or filename + mime_parts_filter_regex { + #GEN1 = "application\/octet-stream"; + DOC2 = "application\/msword"; + DOC3 = "application\/vnd\.ms-word.*"; + XLS = "application\/vnd\.ms-excel.*"; + PPT = "application\/vnd\.ms-powerpoint.*"; + GEN2 = "application\/vnd\.openxmlformats-officedocument.*"; + } + # Mime-Part filename extension matching (no regex) + mime_parts_filter_ext { + doc = "doc"; + dot = "dot"; + docx = "docx"; + dotx = "dotx"; + docm = "docm"; + dotm = "dotm"; + xls = "xls"; + xlt = "xlt"; + xla = "xla"; + xlsx = "xlsx"; + xltx = "xltx"; + xlsm = "xlsm"; + xltm = "xltm"; + xlam = "xlam"; + xlsb = "xlsb"; + ppt = "ppt"; + pot = "pot"; + pps = "pps"; + ppa = "ppa"; + pptx = "pptx"; + potx = "potx"; + ppsx = "ppsx"; + ppam = "ppam"; + pptm = "pptm"; + potm = "potm"; + ppsm = "ppsm"; + } + # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned. + whitelist = "/etc/rspamd/antivirus.wl"; + } dcc { # If set force this action if any virus is found (default unset: no action is forced) # action = "reject"; # If set, then rejection message is set to this value (mention single quotes) # If `max_size` is set, messages > n bytes in size are not scanned max_size = 20000000; - servers = "127.0.0.1:3310"; + #servers = "127.0.0.1:10045; # if `patterns` is specified virus name will be matched against provided regexes and the related # symbol will be yielded if a match is found. If no match is found, default symbol is yielded. patterns { @@ -51,22 +112,22 @@ end local function add_scanner_rule(sym, opts) - if not opts['type'] then + if not opts.type then rspamd_logger.errx(rspamd_config, 'unknown type for external scanner rule %s', sym) return nil end - if not opts['symbol'] then opts['symbol'] = sym:upper() end - local cfg = lua_scanners[opts['type']] + if not opts.symbol then opts.symbol = sym:upper() end + local cfg = lua_scanners[opts.type] if not cfg then - rspamd_logger.errx(rspamd_config, 'unknown antivirus type: %s', - opts['type']) + rspamd_logger.errx(rspamd_config, 'unknown external scanner type: %s', + opts.type) return nil end - if not opts['symbol_fail'] then - opts['symbol_fail'] = string.upper(opts['type']) .. '_FAIL' + if not opts.symbol_fail then + opts.symbol_fail = opts.symbol .. '_FAIL' end local rule = cfg.configure(opts) @@ -76,37 +137,39 @@ local function add_scanner_rule(sym, opts) if not rule then rspamd_logger.errx(rspamd_config, 'cannot configure %s for %s', - opts['type'], opts['symbol']) + opts.type, opts.symbol) return nil end - if type(opts['patterns']) == 'table' then - rule['patterns'] = {} - if opts['patterns'][1] then - for i, p in ipairs(opts['patterns']) do - if type(p) == 'table' then - local new_set = {} - for k, v in pairs(p) do - new_set[k] = rspamd_regexp.create_cached(v) - end - rule['patterns'][i] = new_set - else - rule['patterns'][i] = {} - end - end - else - for k, v in pairs(opts['patterns']) do - rule['patterns'][k] = rspamd_regexp.create_cached(v) - end - end + -- if any mime_part filter defined, do not scan all attachments + if opts.mime_parts_filter_regex ~= nil + or opts.mime_parts_filter_ext ~= nil then + rule.scan_all_mime_parts = false end - if opts['whitelist'] then - rule['whitelist'] = rspamd_config:add_hash_map(opts['whitelist']) + rule.patterns = common.create_regex_table(opts.patterns or {}) + + rule.mime_parts_filter_regex = common.create_regex_table(opts.mime_parts_filter_regex or {}) + + rule.mime_parts_filter_ext = common.create_regex_table(opts.mime_parts_filter_ext or {}) + + if opts.whitelist then + rule.whitelist = rspamd_config:add_hash_map(opts.whitelist) end return function(task) - cfg.check(task, task:get_content(), task:get_digest(), rule) + if rule.scan_mime_parts then + + fun.each(function(p) + local content = p:get_content() + if content and #content > 0 then + cfg.check(task, content, p:get_digest(), rule) + end + end, common.check_parts_match(task, rule)) + + else + cfg.check(task, task:get_content(), task:get_digest(), rule) + end end end @@ -118,6 +181,7 @@ if opts and type(opts) == 'table' then for k, m in pairs(opts) do if type(m) == 'table' and m.servers then if not m.type then m.type = k end + if not m.name then m.name = k end local cb = add_scanner_rule(k, m) if not cb then |