diff options
author | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2019-10-04 11:39:31 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-04 11:39:31 +0100 |
commit | 403d8eef0db29e81b4524844b9630ccfc01b8305 (patch) | |
tree | f7eb3edb579a05a1bf4e3985ed0911414632b1ad /lualib | |
parent | 1880656d8c4e5c52f0d890250871efa0927d8101 (diff) | |
parent | adc10228b43a7b1a8d6203579611d9cca04545ea (diff) | |
download | rspamd-403d8eef0db29e81b4524844b9630ccfc01b8305.tar.gz rspamd-403d8eef0db29e81b4524844b9630ccfc01b8305.zip |
Merge pull request #3063 from HeinleinSupport/master
[Rework] lua_scanner adjustments, support more icap scanners
Diffstat (limited to 'lualib')
-rw-r--r-- | lualib/lua_scanners/clamav.lua | 21 | ||||
-rw-r--r-- | lualib/lua_scanners/common.lua | 100 | ||||
-rw-r--r-- | lualib/lua_scanners/dcc.lua | 138 | ||||
-rw-r--r-- | lualib/lua_scanners/fprot.lua | 21 | ||||
-rw-r--r-- | lualib/lua_scanners/icap.lua | 520 | ||||
-rw-r--r-- | lualib/lua_scanners/kaspersky_av.lua | 21 | ||||
-rw-r--r-- | lualib/lua_scanners/oletools.lua | 393 | ||||
-rw-r--r-- | lualib/lua_scanners/savapi.lua | 23 | ||||
-rw-r--r-- | lualib/lua_scanners/sophos.lua | 23 | ||||
-rw-r--r-- | lualib/lua_scanners/spamassassin.lua | 134 | ||||
-rw-r--r-- | lualib/lua_scanners/vadesecure.lua | 334 |
11 files changed, 905 insertions, 823 deletions
diff --git a/lualib/lua_scanners/clamav.lua b/lualib/lua_scanners/clamav.lua index 01386cfe7..90cd67cef 100644 --- a/lualib/lua_scanners/clamav.lua +++ b/lualib/lua_scanners/clamav.lua @@ -151,19 +151,11 @@ local function clamav_check(task, content, digest, rule) end end if cached then - common.save_av_cache(task, digest, rule, cached) + common.save_cache(task, digest, rule, cached) end end end - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - tcp.request({ task = task, host = addr:to_string(), @@ -175,13 +167,12 @@ local function clamav_check(task, content, digest, rule) }) end - 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() - end + if common.need_check(task, content, rule, digest, clamav_check_uncached) then + return + else + clamav_check_uncached() end + end return { diff --git a/lualib/lua_scanners/common.lua b/lualib/lua_scanners/common.lua index 65dd4aef8..2869cc2ed 100644 --- a/lualib/lua_scanners/common.lua +++ b/lualib/lua_scanners/common.lua @@ -125,18 +125,77 @@ local function message_not_too_large(task, content, rule) return true end -local function need_av_check(task, content, rule) - return message_not_too_large(task, content, rule) +local function message_not_too_small(task, content, rule) + local min_size = tonumber(rule.min_size) + if not min_size then return true end + if #content < min_size then + rspamd_logger.infox(task, "skip %s check as it is too small: %s (%s is allowed)", + rule.log_prefix, #content, min_size) + return false + end + return true +end + +local function message_min_words(task, rule) + if rule.text_part_min_words then + local text_parts_empty = false + local text_parts = task:get_text_parts() + + local filter_func = function(p) + return p:get_words_count() <= tonumber(rule.text_part_min_words) + end + + fun.each(function(p) + text_parts_empty = true + rspamd_logger.infox(task, '%s: #words is less then text_part_min_words: %s', + rule.log_prefix, rule.text_part_min_words) + end, fun.filter(filter_func, text_parts)) + + return text_parts_empty + else + return true + end end -local function check_av_cache(task, digest, rule, fn) +local function dynamic_scan(task, rule) + if rule.dynamic_scan then + if rule.action ~= 'reject' then + local metric_result = task:get_metric_score('default') + local metric_action = task:get_metric_action('default') + local has_pre_result = task:has_pre_result() + -- ToDo: needed? + -- Sometimes leads to FPs + --if rule.symbol_type == 'postfilter' and metric_action == 'reject' then + -- rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, "result is already reject") + -- return false + --elseif metric_result[1] > metric_result[2]*2 then + if metric_result[1] > metric_result[2]*2 then + rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, 'score > 2 * reject_level: ' .. metric_result[1]) + return false + elseif has_pre_result and metric_action == 'reject' then + rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, 'pre_result reject is set') + return false + else + return true, 'undecided' + end + else + return true, 'dynamic_scan is not possible with config `action=reject;`' + end + else + return true + end +end + +local function need_check(task, content, rule, digest, fn) + + local uncached = true local key = digest local function redis_av_cb(err, data) if data and type(data) == 'string' then -- Cached - data = rspamd_str_split(data, '\t') - local threat_string = rspamd_str_split(data[1], '\v') + data = lua_util.str_split(data, '\t') + local threat_string = lua_util.str_split(data[1], '\v') local score = data[2] or rule.default_score if threat_string[1] ~= 'OK' then lua_util.debugm(rule.name, task, '%s: got cached threat result for %s: %s - score: %s', @@ -146,12 +205,28 @@ local function check_av_cache(task, digest, rule, fn) lua_util.debugm(rule.name, task, '%s: got cached negative result for %s: %s', rule.log_prefix, key, threat_string[1]) end + uncached = false else if err then rspamd_logger.errx(task, 'got error checking cache: %s', err) end + end + + local f_message_not_too_large = message_not_too_large(task, content, rule) or true + local f_message_not_too_small = message_not_too_small(task, content, rule) or true + local f_message_min_words = message_min_words(task, rule) or true + local f_dynamic_scan = dynamic_scan(task, rule) or true + + if uncached and + f_message_not_too_large and + f_message_not_too_small and + f_message_min_words and + f_dynamic_scan then + fn() + end + end if rule.redis_params then @@ -171,9 +246,10 @@ local function check_av_cache(task, digest, rule, fn) end return false + end -local function save_av_cache(task, digest, rule, to_save, dyn_weight) +local function save_cache(task, digest, rule, to_save, dyn_weight) local key = digest if not dyn_weight then dyn_weight = 1.0 end @@ -183,8 +259,8 @@ local function save_av_cache(task, digest, rule, to_save, dyn_weight) rspamd_logger.errx(task, 'failed to save %s cache for %s -> "%s": %s', rule.detection_category, to_save, key, err) else - lua_util.debugm(rule.name, task, '%s: saved cached result for %s: %s - score %s', - rule.log_prefix, key, to_save, dyn_weight) + lua_util.debugm(rule.name, task, '%s: saved cached result for %s: %s - score %s - ttl %s', + rule.log_prefix, key, to_save, dyn_weight, rule.cache_expire) end end @@ -257,7 +333,7 @@ end -- 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 filename_parts = lua_util.str_split(fname, '.') local ext = {} for n = 1, 2 do @@ -363,9 +439,9 @@ 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.need_check = need_check +exports.check_cache = check_cache +exports.save_cache = save_cache exports.create_regex_table = create_regex_table exports.check_parts_match = check_parts_match exports.check_metric_results = check_metric_results diff --git a/lualib/lua_scanners/dcc.lua b/lualib/lua_scanners/dcc.lua index 9043391d2..db1ac4497 100644 --- a/lualib/lua_scanners/dcc.lua +++ b/lualib/lua_scanners/dcc.lua @@ -29,6 +29,62 @@ local fun = require "fun" local N = 'dcc' +local function dcc_config(opts) + + local dcc_conf = { + name = 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, + action = false, + client = '0.0.0.0', + symbol_fail = 'DCC_FAIL', + symbol = 'DCC_REJECT', + symbol_bulk = 'DCC_BULK', + body_max = 999999, + fuz1_max = 999999, + fuz2_max = 999999, + } + + 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 = dcc_conf.name + end + + if not dcc_conf.servers and dcc_conf.socket then + dcc_conf.servers = dcc_conf.socket + end + + if not dcc_conf.servers then + rspamd_logger.errx(rspamd_config, 'no servers defined') + + return nil + end + + dcc_conf.upstreams = upstream_list.create(rspamd_config, + dcc_conf.servers, + dcc_conf.default_port) + + if dcc_conf.upstreams then + lua_util.add_debug_alias('external_services', dcc_conf.name) + return dcc_conf + end + + rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', + dcc_conf['servers']) + return nil +end + local function dcc_check(task, content, digest, rule) local function dcc_check_uncached () local upstream = rule.upstreams:get_upstream_round_robin() @@ -137,7 +193,7 @@ local function dcc_check(task, content, digest, rule) 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) + common.save_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) @@ -192,9 +248,9 @@ local function dcc_check(task, content, digest, rule) task:insert_result(rule.symbol_bulk, score, opts) - common.save_av_cache(task, digest, rule, opts, score) + common.save_cache(task, digest, rule, opts, score) else - common.save_av_cache(task, digest, rule, 'OK') + common.save_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) @@ -205,7 +261,7 @@ local function dcc_check(task, content, digest, rule) end elseif result == 'G' then -- do nothing - common.save_av_cache(task, digest, rule, 'OK') + common.save_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 @@ -213,7 +269,7 @@ local function dcc_check(task, content, digest, rule) end elseif result == 'S' then -- do nothing - common.save_av_cache(task, digest, rule, 'OK') + common.save_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 @@ -228,14 +284,6 @@ local function dcc_check(task, content, digest, rule) end end - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - tcp.request({ task = task, host = addr:to_string(), @@ -249,69 +297,13 @@ local function dcc_check(task, content, digest, rule) fuz2_max = 999999, }) end - 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 = { - name = 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, - action = false, - client = '0.0.0.0', - symbol_fail = 'DCC_FAIL', - symbol = 'DCC_REJECT', - symbol_bulk = 'DCC_BULK', - body_max = 999999, - fuz1_max = 999999, - fuz2_max = 999999, - } - - 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 = dcc_conf.name - end - - if not dcc_conf.servers and dcc_conf.socket then - dcc_conf.servers = dcc_conf.socket - end - - if not dcc_conf.servers then - rspamd_logger.errx(rspamd_config, 'no servers defined') - - return nil - end - - dcc_conf.upstreams = upstream_list.create(rspamd_config, - dcc_conf.servers, - dcc_conf.default_port) - - if dcc_conf.upstreams then - lua_util.add_debug_alias('external_services', dcc_conf.name) - return dcc_conf + if common.need_check(task, content, rule, digest, dcc_check_uncached) then + return + else + dcc_check_uncached() end - rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', - dcc_conf['servers']) - return nil end return { diff --git a/lualib/lua_scanners/fprot.lua b/lualib/lua_scanners/fprot.lua index 907fab139..4061251cb 100644 --- a/lualib/lua_scanners/fprot.lua +++ b/lualib/lua_scanners/fprot.lua @@ -144,19 +144,11 @@ local function fprot_check(task, content, digest, rule) end end if cached then - common.save_av_cache(task, digest, rule, cached) + common.save_cache(task, digest, rule, cached) end end end - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - tcp.request({ task = task, host = addr:to_string(), @@ -168,13 +160,12 @@ local function fprot_check(task, content, digest, rule) }) end - 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() - end + if common.need_check(task, content, rule, digest, fprot_check_uncached) then + return + else + fprot_check_uncached() end + end return { diff --git a/lualib/lua_scanners/icap.lua b/lualib/lua_scanners/icap.lua index d00954f41..1f3ada5c9 100644 --- a/lualib/lua_scanners/icap.lua +++ b/lualib/lua_scanners/icap.lua @@ -18,7 +18,13 @@ limitations under the License. --[[[ -- @module icap -- This module contains icap access functions. --- Currently tested with Symantec, Sophos Savdi, ClamAV/c-icap +-- Currently tested with +-- - Symantec +-- - Sophos Savdi +-- - ClamAV/c-icap +-- - Kaspersky Web Traffic Security +-- - Trend Micro IWSVA +-- - F-Secure Internet Gatekeeper Strings --]] local lua_util = require "lua_util" @@ -29,6 +35,65 @@ local common = require "lua_scanners/common" local N = 'icap' +local function icap_config(opts) + + local icap_conf = { + name = 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, + dynamic_scan = 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.name) + return icap_conf + end + + rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', + icap_conf.servers) + return nil +end + local function icap_check(task, content, digest, rule) local function icap_check_uncached () local upstream = rule.upstreams:get_upstream_round_robin() @@ -38,203 +103,269 @@ local function icap_check(task, content, digest, rule) -- 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", + string.format("OPTIONS icap://%s:%s/%s ICAP/1.0\r\n", addr:to_string(), addr:get_port(), rule.scheme), + string.format('Host: %s\r\n', addr:to_string()), "User-Agent: Rspamd\r\n", "Encapsulated: null-body=0\r\n\r\n", } local size = string.format("%x", tonumber(#content)) - 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?(.+)") - if key ~= nil then - icap_headers[key] = value - end - end - end - lua_util.debugm(rule.name, task, '%s: icap_headers: %s', - rule.log_prefix, icap_headers) - return icap_headers - end - - local function icap_parse_result(icap_headers) + local function icap_callback(err, conn) - local threat_string = {} + local function icap_requery(err_m, info) + -- set current upstream to fail because an error occurred + upstream:fail() - --[[ - @ToDo: handle type in response + -- retry with another upstream until retransmits exceeds + if retransmits > 0 then - 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 - Kaspersky Strings: - X-Virus-ID: HEUR:Backdoor.Java.QRat.gen - X-Response-Info: blocked + retransmits = retransmits - 1 - X-Virus-ID: no threats - X-Response-Info: blocked + lua_util.debugm(rule.name, task, + '%s: %s Request Error: %s - retries left: %s', + rule.log_prefix, info, err_m, retransmits) - X-Response-Info: passed - ]] -- + -- Select a different upstream! + upstream = rule.upstreams:get_upstream_round_robin() + addr = upstream:get_addr() - if icap_headers['X-Infection-Found'] ~= nil then - local _,_,icap_type,_,icap_threat = - icap_headers['X-Infection-Found']:find("Type=(.-); Resolution=(.-); Threat=(.-);$") + lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s', + rule.log_prefix, addr, addr:get_port()) - if not icap_type or icap_type == 2 then - -- error returned - lua_util.debugm(rule.name, task, - '%s: icap error X-Infection-Found: %s', rule.log_prefix, icap_threat) - common.yield_result(task, rule, icap_threat, 0, 'fail') + 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 - lua_util.debugm(rule.name, task, - '%s: icap X-Infection-Found: %s', rule.log_prefix, icap_threat) - table.insert(threat_string, icap_threat) + rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits '.. + 'exceed - error: %s', rule.log_prefix, err_m or '') + common.yield_result(task, rule, 'failed - error: ' .. err_m or '', 0.0, 'fail') end + end - elseif icap_headers['X-Virus-ID'] ~= nil and icap_headers['X-Virus-ID'] ~= "no threats" then - lua_util.debugm(rule.name, task, - '%s: icap X-Virus-ID: %s', rule.log_prefix, icap_headers['X-Virus-ID']) + local function get_respond_query() + table.insert(respond_headers, 1, string.format( + 'RESPMOD icap://%s:%s/%s ICAP/1.0\r\n', addr:to_string(), addr:get_port(), rule.scheme)) + 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 - if string.find(icap_headers['X-Virus-ID'], ', ') then - local vnames = rspamd_str_split(string.gsub(icap_headers['X-Virus-ID'], "%s", ""), ',') or {} + local function add_respond_header(name, value) + if name and value then + table.insert(respond_headers, string.format('%s: %s\r\n', name, value)) + end + end - for _,v in ipairs(vnames) do - table.insert(threat_string, v) + 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?(.+)") + if key ~= nil then + icap_headers[key] = value + end end - else - table.insert(threat_string, icap_headers['X-Virus-ID']) end + lua_util.debugm(rule.name, task, '%s: icap_headers: %s', + rule.log_prefix, icap_headers) + return icap_headers 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_parse_result(icap_headers) - local function icap_r_respond_cb(err, data, conn) - local result = tostring(data) - conn:close() + local threat_string = {} - 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 --[[ + @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: - 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) - common.yield_result(task, rule, icap_headers.icap, 0.0, 'fail') - return false - else - rspamd_logger.errx(task, '%s: unhandled response |%s|', - rule.log_prefix, string.gsub(result, "\r\n", ", ")) - common.yield_result(task, rule, 'unhandled icap response: ' .. icap_headers.icap, 0.0, 'fail') - end - end + 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 + + Kaspersky Web Traffic Security Strings: + X-Virus-ID: HEUR:Backdoor.Java.QRat.gen + X-Response-Info: blocked + X-Virus-ID: no threats + X-Response-Info: blocked + X-Response-Info: passed + + Trend Micro IWSVA Strings: + X-Virus-ID: Trojan.W97M.POWLOAD.SMTHF1 + X-Infection-Found: Type=0; Resolution=2; Threat=Trojan.W97M.POWLOAD.SMTHF1; + + F-Secure Internet Gatekeeper Strings: + X-FSecure-Scan-Result: infected + X-FSecure-Infection-Name: "Malware.W97M/Agent.32584203" + X-FSecure-Infected-Filename: "virus.doc" + + ESET File Security for Linux 7.0 + X-Infection-Found: Type=0; Resolution=0; Threat=VBA/TrojanDownloader.Agent.JOA; + X-Virus-ID: Trojaner + X-Response-Info: Blocked + ]] -- + + if icap_headers['X-Infection-Found'] then + local _,_,icap_type,_,icap_threat = + icap_headers['X-Infection-Found']:find("Type=(.-); Resolution=(.-); Threat=(.-);$") + + if not icap_type or icap_type == 2 then + -- error returned + lua_util.debugm(rule.name, task, + '%s: icap error X-Infection-Found: %s', rule.log_prefix, icap_threat) + common.yield_result(task, rule, icap_threat, 0, 'fail') + else + lua_util.debugm(rule.name, task, + '%s: icap X-Infection-Found: %s', rule.log_prefix, icap_threat) + table.insert(threat_string, icap_threat) + end - local function icap_w_respond_cb(err, conn) - conn:add_read(icap_r_respond_cb, '\r\n\r\n') - end + elseif icap_headers['X-Virus-ID'] and icap_headers['X-Virus-ID'] ~= "no threats" then + lua_util.debugm(rule.name, task, + '%s: icap X-Virus-ID: %s', rule.log_prefix, icap_headers['X-Virus-ID']) - local function icap_r_options_cb(err, data, conn) - local icap_headers = icap_result_header_table(tostring(data)) + if string.find(icap_headers['X-Virus-ID'], ', ') then + local vnames = lua_util.str_split(string.gsub(icap_headers['X-Virus-ID'], "%s", ""), ',') or {} - 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') + for _,v in ipairs(vnames) do + table.insert(threat_string, v) + end + else + table.insert(threat_string, icap_headers['X-Virus-ID']) 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']) - common.yield_result(task, rule, 'NO RESPMOD', 0.0, 'fail') - end - else - rspamd_logger.errx(task, '%s: OPTIONS query failed: %s', - rule.log_prefix, icap_headers.icap) - common.yield_result(task, rule, 'OPTIONS query failed', 0.0, 'fail') - end - end + elseif icap_headers['X-FSecure-Scan-Result'] and icap_headers['X-FSecure-Scan-Result'] ~= "clean" then - local function icap_callback(err, conn) + local infected_filename = "" + local infection_name = "-unknown-" - local function icap_requery(error) - -- set current upstream to fail because an error occurred - upstream:fail() + if icap_headers['X-FSecure-Infected-Filename'] then + infected_filename = string.gsub(icap_headers['X-FSecure-Infected-Filename'], '[%s"]', '') + end + if icap_headers['X-FSecure-Infection-Name'] then + infection_name = string.gsub(icap_headers['X-FSecure-Infection-Name'], '[%s"]', '') + end - -- retry with another upstream until retransmits exceeds - if retransmits > 0 then + lua_util.debugm(rule.name, task, + '%s: icap X-FSecure-Infection-Name (X-FSecure-Infected-Filename): %s (%s)', + rule.log_prefix, infection_name, infected_filename) - retransmits = retransmits - 1 + if string.find(infection_name, ', ') then + local vnames = lua_util.str_split(infection_name, ',') or {} - lua_util.debugm(rule.name, task, - '%s: Request Error: %s - retries left: %s', - rule.log_prefix, error, retransmits) + for _,v in ipairs(vnames) do + table.insert(threat_string, v) + end + else + table.insert(threat_string, infection_name) + end + end + if #threat_string > 0 then + common.yield_result(task, rule, threat_string, rule.default_score) + common.save_cache(task, digest, rule, threat_string, rule.default_score) + else + common.save_cache(task, digest, rule, 'OK', 0) + common.log_clean(task, rule) + end + end - -- Select a different upstream! - upstream = rule.upstreams:get_upstream_round_robin() - addr = upstream:get_addr() + local function icap_r_respond_cb(err_m, data, connection) + if err_m or connection == nil then + icap_requery(err_m, "icap_r_respond_cb") + else + local result = tostring(data) + conn:close() + + local icap_headers = icap_result_header_table(result) or {} + -- Find ICAP/1.x 2xx response + if icap_headers.icap and string.find(icap_headers.icap, 'ICAP%/1%.. 2%d%d') then + icap_parse_result(icap_headers) + elseif icap_headers.icap and 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) + common.yield_result(task, rule, icap_headers.icap, 0.0, 'fail') + return false + else + rspamd_logger.errx(task, '%s: unhandled response |%s|', + rule.log_prefix, string.gsub(result, "\r\n", ", ")) + common.yield_result(task, rule, 'unhandled icap response: ' .. icap_headers.icap or "-", 0.0, 'fail') + end + end + end - lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s', - rule.log_prefix, addr, addr:get_port()) + local function icap_w_respond_cb(err_m, connection) + if err_m or connection == nil then + icap_requery(err_m, "icap_w_respond_cb") + else + connection:add_read(icap_r_respond_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, - }) + local function icap_r_options_cb(err_m, data, connection) + if err_m or connection == nil then + icap_requery(err_m, "icap_r_options_cb") else - rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits '.. - 'exceed - err: %s', rule.log_prefix, error) - common.yield_result(task, rule, 'failed - err: ' .. error, 0.0, 'fail') + local icap_headers = icap_result_header_table(tostring(data)) + + if icap_headers.icap and string.find(icap_headers.icap, 'ICAP%/1%.. 2%d%d') then + if icap_headers['Methods'] and string.find(icap_headers['Methods'], 'RESPMOD') then + if icap_headers['Allow'] and string.find(icap_headers['Allow'], '204') then + add_respond_header('Allow', '204') + end + if icap_headers['Service'] and string.find(icap_headers['Service'], 'IWSVA 6.5') then + add_respond_header('Encapsulated', 'res-hdr=0 res-body=0') + else + add_respond_header('Encapsulated', 'res-body=0') + end + if icap_headers['Server'] and string.find(icap_headers['Server'], 'F-Secure ICAP Server') then + local from = task:get_from('mime') + local rcpt_to = task:get_principal_recipient() + local client = task:get_from_ip() + if client then add_respond_header('X-Client-IP', client:to_string()) end + add_respond_header('X-Mail-From', from[1].addr) + add_respond_header('X-Rcpt-To', rcpt_to) + 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']) + common.yield_result(task, rule, 'NO RESPMOD', 0.0, 'fail') + end + else + rspamd_logger.errx(task, '%s: OPTIONS query failed: %s', + rule.log_prefix, icap_headers.icap or "-") + common.yield_result(task, rule, 'OPTIONS query failed', 0.0, 'fail') + end end end - if err then - icap_requery(err) + if err or conn == nil then + icap_requery(err, "options_request") else -- set upstream ok if upstream then upstream:ok() end @@ -242,14 +373,6 @@ local function icap_check(task, content, digest, rule) end end - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - tcp.request({ task = task, host = addr:to_string(), @@ -261,72 +384,13 @@ local function icap_check(task, content, digest, rule) 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 = { - name = 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 + if common.need_check(task, content, rule, digest, icap_check_uncached) then + return + else + icap_check_uncached() 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.name) - return icap_conf - end - - rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', - icap_conf.servers) - return nil end return { diff --git a/lualib/lua_scanners/kaspersky_av.lua b/lualib/lua_scanners/kaspersky_av.lua index 87411c3b9..767ff2a94 100644 --- a/lualib/lua_scanners/kaspersky_av.lua +++ b/lualib/lua_scanners/kaspersky_av.lua @@ -162,19 +162,11 @@ local function kaspersky_check(task, content, digest, rule) end end if cached then - common.save_av_cache(task, digest, rule, cached) + common.save_cache(task, digest, rule, cached) end end end - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - tcp.request({ task = task, host = addr:to_string(), @@ -186,13 +178,12 @@ local function kaspersky_check(task, content, digest, rule) }) end - 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() - end + if common.need_check(task, content, rule, digest, kaspersky_check_uncached) then + return + else + kaspersky_check_uncached() end + end return { diff --git a/lualib/lua_scanners/oletools.lua b/lualib/lua_scanners/oletools.lua index 91094ccb3..cc973d4d5 100644 --- a/lualib/lua_scanners/oletools.lua +++ b/lualib/lua_scanners/oletools.lua @@ -30,14 +30,72 @@ local common = require "lua_scanners/common" local N = 'oletools' +local function oletools_config(opts) + + local oletools_conf = { + name = N, + scan_mime_parts = true, + scan_text_mime = false, + scan_image_mime = false, + default_port = 10050, + timeout = 15.0, + log_clean = false, + retransmits = 2, + cache_expire = 86400, -- expire redis in 1d + min_size = 500, + symbol = "OLETOOLS", + message = '${SCANNER}: Oletools threat message found: "${VIRUS}"', + detection_category = "office macro", + default_score = 1, + action = false, + extended = false, + symbol_type = 'postfilter', + dynamic_scan = true, + } + + 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.name) + return oletools_conf + end + + rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', + oletools_conf.servers) + return nil +end + 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 json_response = "" - local function oletools_callback(err, data) + local function oletools_callback(err, data, conn) local function oletools_requery(error) -- set current upstream to fail because an error occurred @@ -83,156 +141,157 @@ local function oletools_check(task, content, digest, rule) -- Parse the response if upstream then upstream:ok() end - data = tostring(data) + json_response = json_response .. 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 + if not string.find(json_response, '\t\n\n\t') and #data == 8192 then + lua_util.debugm(rule.name, task, '%s: no stop word: add_read - #json: %s / current packet: %s', + rule.log_prefix, #json_response, #data) + conn:add_read(oletools_callback) + else - 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) + local ucl_parser = ucl.parser() + local ok, ucl_err = ucl_parser:parse_string(tostring(json_response)) + if not ok then + rspamd_logger.errx(task, "%s: error parsing json response, retry: %s", + rule.log_prefix, ucl_err) + oletools_requery(ucl_err) + return end - elseif result[3]['return_code'] == 9 then - rspamd_logger.warnx(task, '%s: File is encrypted.', rule.log_prefix) - common.yield_result(task, rule, 'failed - err: ' .. oletools_rc[result[3]['return_code']], 0.0, 'fail') - 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']) - common.yield_result(task, rule, 'failed - err: ' .. oletools_rc[result[3]['return_code']], 0.0, 'fail') - 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 type(result[2]['analysis']) == 'table' and #result[2]['analysis'] == 0 and #result[2]['macros'] == 0 then - rspamd_logger.warnx(task, '%s: maybe unhandled python or oletools error', rule.log_prefix) - common.yield_result(task, rule, 'oletools unhandled error', 0.0, 'fail') - elseif type(result[2]['analysis']) ~= 'table' 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.name, task, - '%s: filename: %s', rule.log_prefix, result[2]['file']) - lua_util.debugm(rule.name, task, - '%s: type: %s', rule.log_prefix, result[2]['type']) + 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_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) + common.yield_result(task, rule, 'failed - err: ' .. oletools_rc[result[3]['return_code']], 0.0, 'fail') + 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']) + common.yield_result(task, rule, 'failed - err: ' .. oletools_rc[result[3]['return_code']], 0.0, 'fail') + 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 type(result[2]['analysis']) == 'table' and #result[2]['analysis'] == 0 + and #result[2]['macros'] == 0 then + rspamd_logger.warnx(task, '%s: maybe unhandled python or oletools error', rule.log_prefix) + common.yield_result(task, rule, 'oletools unhandled error', 0.0, 'fail') + elseif type(result[2]['analysis']) ~= 'table' and #result[2]['macros'] == 0 then + common.save_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.name, task, + '%s: filename: %s', rule.log_prefix, result[2]['file']) + lua_util.debugm(rule.name, task, + '%s: type: %s', rule.log_prefix, result[2]['type']) + + for _,m in ipairs(result[2]['macros']) do + lua_util.debugm(rule.name, task, '%s: macros found - code: %s, ole_stream: %s, '.. + 'vba_filename: %s', rule.log_prefix, m.code, m.ole_stream, m.vba_filename) + end - for _,m in ipairs(result[2]['macros']) do - lua_util.debugm(rule.name, 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 = {} - local analysis_keyword_table = {} - - for _,a in ipairs(result[2]['analysis']) do - lua_util.debugm(rule.name, 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 - if rule.extended == true or - (a.keyword ~= 'Base64 Strings' and a.keyword ~= 'Hex Strings') - then - m_suspicious = 'S' + for _,a in ipairs(result[2]['analysis']) do + lua_util.debugm(rule.name, 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 + if rule.extended == true or + (a.keyword ~= 'Base64 Strings' and a.keyword ~= 'Hex Strings') + then + m_suspicious = 'S' + table.insert(analysis_keyword_table, a.keyword) + end + elseif a.type == 'IOC' 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 - elseif a.type == 'IOC' 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.name, 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.name, 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) + --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.name, task, '%s: threat result: %s', rule.log_prefix, threat) + common.yield_result(task, rule, threat, rule.default_score) + common.save_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.name, 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_cache(task, digest, rule, analysis_keyword_table, rule.default_score) + else + common.save_cache(task, digest, rule, 'OK') + common.log_clean(task, rule, 'Scanned Macro is OK') + end + else - common.save_av_cache(task, digest, rule, 'OK') - common.log_clean(task, rule, 'Scanned Macro is OK') + rspamd_logger.warnx(task, '%s: unhandled response', rule.log_prefix) + common.yield_result(task, rule, 'unhandled error', 0.0, 'fail') end - - else - rspamd_logger.warnx(task, '%s: unhandled response', rule.log_prefix) - common.yield_result(task, rule, 'unhandled error', 0.0, 'fail') end end end - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - tcp.request({ task = task, host = addr:to_string(), @@ -244,69 +303,13 @@ local function oletools_check(task, content, digest, rule) }) 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 = { - name = N, - scan_mime_parts = true, - scan_text_mime = false, - scan_image_mime = false, - default_port = 10050, - timeout = 15.0, - log_clean = false, - retransmits = 2, - cache_expire = 86400, -- expire redis in 1d - symbol = "OLETOOLS", - message = '${SCANNER}: Oletools threat message found: "${VIRUS}"', - detection_category = "office macro", - default_score = 1, - action = false, - extended = false, - symbol_type = 'postfilter', - dynamic_scan = true, - } - - 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.name) - return oletools_conf + if common.need_check(task, content, rule, digest, oletools_check_uncached) then + return + else + oletools_check_uncached() end - rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', - oletools_conf.servers) - return nil end return { diff --git a/lualib/lua_scanners/savapi.lua b/lualib/lua_scanners/savapi.lua index 65a9c825c..b36e6e148 100644 --- a/lualib/lua_scanners/savapi.lua +++ b/lualib/lua_scanners/savapi.lua @@ -127,7 +127,7 @@ local function savapi_check(task, content, digest, rule) end common.yield_result(task, rule, vname) - common.save_av_cache(task, digest, rule, vname) + common.save_cache(task, digest, rule, vname) end if conn then conn:close() @@ -144,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') + common.save_cache(task, digest, rule, 'OK') conn:add_write(savapi_fin_cb, 'QUIT\n') -- Terminal response - infected @@ -237,14 +237,6 @@ local function savapi_check(task, content, digest, rule) end end - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - tcp.request({ task = task, host = addr:to_string(), @@ -255,13 +247,12 @@ local function savapi_check(task, content, digest, rule) }) end - 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() - end + if common.need_check(task, content, rule, digest, savapi_check_uncached) then + return + else + savapi_check_uncached() end + end return { diff --git a/lualib/lua_scanners/sophos.lua b/lualib/lua_scanners/sophos.lua index 59facc845..60a23c20b 100644 --- a/lualib/lua_scanners/sophos.lua +++ b/lualib/lua_scanners/sophos.lua @@ -125,7 +125,7 @@ local function sophos_check(task, content, digest, rule) local vname = string.match(data, 'VIRUS (%S+) ') if vname then common.yield_result(task, rule, vname) - common.save_av_cache(task, digest, rule, vname) + common.save_cache(task, digest, rule, vname) else if string.find(data, 'DONE OK') then if rule['log_clean'] then @@ -134,7 +134,7 @@ local function sophos_check(task, content, digest, rule) lua_util.debugm(rule.name, task, '%s: message or mime_part is clean', rule.log_prefix) end - common.save_av_cache(task, digest, rule, 'OK') + common.save_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) @@ -157,14 +157,6 @@ local function sophos_check(task, content, digest, rule) end end - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - tcp.request({ task = task, host = addr:to_string(), @@ -175,13 +167,12 @@ local function sophos_check(task, content, digest, rule) }) end - 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() - end + if common.need_check(task, content, rule, digest, sophos_check_uncached) then + return + else + sophos_check_uncached() end + end return { diff --git a/lualib/lua_scanners/spamassassin.lua b/lualib/lua_scanners/spamassassin.lua index 2227de235..06fcf5791 100644 --- a/lualib/lua_scanners/spamassassin.lua +++ b/lualib/lua_scanners/spamassassin.lua @@ -28,6 +28,62 @@ local common = require "lua_scanners/common" local N = 'spamassassin' +local function spamassassin_config(opts) + + local spamassassin_conf = { + N = N, + scan_mime_parts = false, + scan_text_mime = false, + scan_image_mime = false, + default_port = 783, + timeout = 15.0, + log_clean = false, + retransmits = 2, + cache_expire = 3600, -- expire redis in one hour + symbol = "SPAMD", + message = '${SCANNER}: Spamassassin bulk message found: "${VIRUS}"', + detection_category = "spam", + default_score = 1, + action = false, + extended = false, + symbol_type = 'postfilter', + dynamic_scan = true, + } + + spamassassin_conf = lua_util.override_defaults(spamassassin_conf, opts) + + if not spamassassin_conf.prefix then + spamassassin_conf.prefix = 'rs_' .. spamassassin_conf.name .. '_' + end + + if not spamassassin_conf.log_prefix then + if spamassassin_conf.name:lower() == spamassassin_conf.type:lower() then + spamassassin_conf.log_prefix = spamassassin_conf.name + else + spamassassin_conf.log_prefix = spamassassin_conf.name .. ' (' .. spamassassin_conf.type .. ')' + end + end + + if not spamassassin_conf.servers then + rspamd_logger.errx(rspamd_config, 'no servers defined') + + return nil + end + + spamassassin_conf.upstreams = upstream_list.create(rspamd_config, + spamassassin_conf.servers, + spamassassin_conf.default_port) + + if spamassassin_conf.upstreams then + lua_util.add_debug_alias('external_services', spamassassin_conf.N) + return spamassassin_conf + end + + rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', + spamassassin_conf.servers) + return nil +end + local function spamassassin_check(task, content, digest, rule) local function spamassassin_check_uncached () local upstream = rule.upstreams:get_upstream_round_robin() @@ -121,14 +177,14 @@ local function spamassassin_check(task, content, digest, rule) if rule.extended == false then common.yield_result(task, rule, symbols, spam_score) - common.save_av_cache(task, digest, rule, symbols, spam_score) + common.save_cache(task, digest, rule, symbols, spam_score) else local symbols_table = {} - symbols_table = rspamd_str_split(symbols, ",") + symbols_table = lua_util.str_split(symbols, ",") lua_util.debugm(rule.N, task, '%s: returned symbols as table: %s', rule.log_prefix, symbols_table) common.yield_result(task, rule, symbols_table, spam_score) - common.save_av_cache(task, digest, rule, symbols_table, spam_score) + common.save_cache(task, digest, rule, symbols_table, spam_score) end else common.log_clean(task, rule, 'no spam detected - spam score: ' .. spam_score .. ', symbols: ' .. symbols) @@ -136,14 +192,6 @@ local function spamassassin_check(task, content, digest, rule) end end - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - tcp.request({ task = task, host = addr:to_string(), @@ -153,69 +201,13 @@ local function spamassassin_check(task, content, digest, rule) callback = spamassassin_callback, }) end - if common.need_av_check(task, content, rule) then - if common.check_av_cache(task, digest, rule, spamassassin_check_uncached) then - return - else - spamassassin_check_uncached() - end - end -end - -local function spamassassin_config(opts) - - local spamassassin_conf = { - N = N, - scan_mime_parts = false, - scan_text_mime = false, - scan_image_mime = false, - default_port = 783, - timeout = 15.0, - log_clean = false, - retransmits = 2, - cache_expire = 3600, -- expire redis in one hour - symbol = "SPAMD", - message = '${SCANNER}: Spamassassin bulk message found: "${VIRUS}"', - detection_category = "spam", - default_score = 1, - action = false, - extended = false, - symbol_type = 'postfilter', - dynamic_scan = true, - } - - spamassassin_conf = lua_util.override_defaults(spamassassin_conf, opts) - if not spamassassin_conf.prefix then - spamassassin_conf.prefix = 'rs_' .. spamassassin_conf.name .. '_' - end - - if not spamassassin_conf.log_prefix then - if spamassassin_conf.name:lower() == spamassassin_conf.type:lower() then - spamassassin_conf.log_prefix = spamassassin_conf.name - else - spamassassin_conf.log_prefix = spamassassin_conf.name .. ' (' .. spamassassin_conf.type .. ')' - end - end - - if not spamassassin_conf.servers then - rspamd_logger.errx(rspamd_config, 'no servers defined') - - return nil - end - - spamassassin_conf.upstreams = upstream_list.create(rspamd_config, - spamassassin_conf.servers, - spamassassin_conf.default_port) - - if spamassassin_conf.upstreams then - lua_util.add_debug_alias('external_services', spamassassin_conf.N) - return spamassassin_conf + if common.need_check(task, content, rule, digest, spamassassin_check_uncached) then + return + else + spamassassin_check_uncached() end - rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', - spamassassin_conf.servers) - return nil end return { diff --git a/lualib/lua_scanners/vadesecure.lua b/lualib/lua_scanners/vadesecure.lua index 5c986970e..77a9e4dee 100644 --- a/lualib/lua_scanners/vadesecure.lua +++ b/lualib/lua_scanners/vadesecure.lua @@ -28,173 +28,6 @@ local common = require "lua_scanners/common" local N = 'vadesecure' -local function vade_check(task, content, digest, rule) - local function vade_url(addr) - local url - if rule.use_https then - url = string.format('https://%s:%d%s', tostring(addr), - rule.default_port, rule.url) - else - url = string.format('http://%s:%d%s', tostring(addr), - rule.default_port, rule.url) - end - - return url - end - - local upstream = rule.upstreams:get_upstream_round_robin() - local addr = upstream:get_addr() - local retransmits = rule.retransmits - - local url = vade_url(addr) - local hdrs = {} - - local helo = task:get_helo() - if helo then - hdrs['X-Helo'] = helo - end - local mail_from = task:get_from('smtp') or {} - if mail_from[1] and #mail_from[1].addr > 1 then - hdrs['X-Mailfrom'] = mail_from[1].addr - end - - local rcpt_to = task:get_recipients('smtp') - if rcpt_to then - hdrs['X-Rcptto'] = {} - for _, r in ipairs(rcpt_to) do - table.insert(hdrs['X-Rcptto'], r.addr) - end - end - - local fip = task:get_from_ip() - if fip and fip:is_valid() then - hdrs['X-Inet'] = tostring(fip) - end - - local request_data = { - task = task, - url = url, - body = task:get_content(), - headers = hdrs, - timeout = rule.timeout, - } - - local function vade_callback(http_err, code, body, headers) - - local function vade_requery() - -- 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.name, task, - '%s: Request Error: %s - retries left: %s', - rule.log_prefix, http_err, retransmits) - - -- Select a different upstream! - upstream = rule.upstreams:get_upstream_round_robin() - addr = upstream:get_addr() - url = vade_url(addr) - - lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s', - rule.log_prefix, addr, addr:get_port()) - request_data.url = url - - http.request(request_data) - else - 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 http_err then - vade_requery() - else - -- Parse the response - if upstream then upstream:ok() end - if code ~= 200 then - rspamd_logger.errx(task, 'invalid HTTP code: %s, body: %s, headers: %s', code, body, headers) - task:insert_result(rule.symbol_fail, 1.0, 'Bad HTTP code: ' .. code) - return - end - local parser = ucl.parser() - local ret, err = parser:parse_string(body) - if not ret then - rspamd_logger.errx(task, 'vade: bad response body (raw): %s', body) - task:insert_result(rule.symbol_fail, 1.0, 'Parser error: ' .. err) - return - end - local obj = parser:get_object() - local verdict = obj.verdict - if not verdict then - rspamd_logger.errx(task, 'vade: bad response JSON (no verdict): %s', obj) - task:insert_result(rule.symbol_fail, 1.0, 'No verdict/unknown verdict') - return - end - local vparts = lua_util.rspamd_str_split(verdict, ":") - verdict = table.remove(vparts, 1) or verdict - - local sym = rule.symbols[verdict] - if not sym then - sym = rule.symbols.other - end - - if not sym.symbol then - -- Subcategory match - local lvl = 'low' - if vparts and vparts[1] then - lvl = vparts[1] - end - - if sym[lvl] then - sym = sym[lvl] - else - sym = rule.symbols.other - end - end - - local opts = {} - if obj.score then - table.insert(opts, 'score=' .. obj.score) - end - if obj.elapsed then - table.insert(opts, 'elapsed=' .. obj.elapsed) - end - - if rule.log_spamcause and obj.spamcause then - rspamd_logger.infox(task, 'vadesecure verdict="%s", score=%s, spamcause="%s", message-id="%s"', - verdict, obj.score, obj.spamcause, task:get_message_id()) - else - lua_util.debugm(rule.name, task, 'vadesecure returned verdict="%s", score=%s, spamcause="%s"', - verdict, obj.score, obj.spamcause) - end - - if #vparts > 0 then - table.insert(opts, 'verdict=' .. verdict .. ';' .. table.concat(vparts, ':')) - end - - task:insert_result(sym.symbol, 1.0, opts) - end - end - - if rule.dynamic_scan then - local pre_check, pre_check_msg = common.check_metric_results(task, rule) - if pre_check then - rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, pre_check_msg) - return true - end - end - - request_data.callback = vade_callback - http.request(request_data) -end - - local function vade_config(opts) local vade_conf = { @@ -318,6 +151,173 @@ local function vade_config(opts) return nil end +local function vade_check(task, content, digest, rule) + local function vade_check_uncached() + local function vade_url(addr) + local url + if rule.use_https then + url = string.format('https://%s:%d%s', tostring(addr), + rule.default_port, rule.url) + else + url = string.format('http://%s:%d%s', tostring(addr), + rule.default_port, rule.url) + end + + return url + end + + local upstream = rule.upstreams:get_upstream_round_robin() + local addr = upstream:get_addr() + local retransmits = rule.retransmits + + local url = vade_url(addr) + local hdrs = {} + + local helo = task:get_helo() + if helo then + hdrs['X-Helo'] = helo + end + local mail_from = task:get_from('smtp') or {} + if mail_from[1] and #mail_from[1].addr > 1 then + hdrs['X-Mailfrom'] = mail_from[1].addr + end + + local rcpt_to = task:get_recipients('smtp') + if rcpt_to then + hdrs['X-Rcptto'] = {} + for _, r in ipairs(rcpt_to) do + table.insert(hdrs['X-Rcptto'], r.addr) + end + end + + local fip = task:get_from_ip() + if fip and fip:is_valid() then + hdrs['X-Inet'] = tostring(fip) + end + + local request_data = { + task = task, + url = url, + body = task:get_content(), + headers = hdrs, + timeout = rule.timeout, + } + + local function vade_callback(http_err, code, body, headers) + + local function vade_requery() + -- 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.name, task, + '%s: Request Error: %s - retries left: %s', + rule.log_prefix, http_err, retransmits) + + -- Select a different upstream! + upstream = rule.upstreams:get_upstream_round_robin() + addr = upstream:get_addr() + url = vade_url(addr) + + lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s', + rule.log_prefix, addr, addr:get_port()) + request_data.url = url + + http.request(request_data) + else + 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 http_err then + vade_requery() + else + -- Parse the response + if upstream then upstream:ok() end + if code ~= 200 then + rspamd_logger.errx(task, 'invalid HTTP code: %s, body: %s, headers: %s', code, body, headers) + task:insert_result(rule.symbol_fail, 1.0, 'Bad HTTP code: ' .. code) + return + end + local parser = ucl.parser() + local ret, err = parser:parse_string(body) + if not ret then + rspamd_logger.errx(task, 'vade: bad response body (raw): %s', body) + task:insert_result(rule.symbol_fail, 1.0, 'Parser error: ' .. err) + return + end + local obj = parser:get_object() + local verdict = obj.verdict + if not verdict then + rspamd_logger.errx(task, 'vade: bad response JSON (no verdict): %s', obj) + task:insert_result(rule.symbol_fail, 1.0, 'No verdict/unknown verdict') + return + end + local vparts = lua_util.str_split(verdict, ":") + verdict = table.remove(vparts, 1) or verdict + + local sym = rule.symbols[verdict] + if not sym then + sym = rule.symbols.other + end + + if not sym.symbol then + -- Subcategory match + local lvl = 'low' + if vparts and vparts[1] then + lvl = vparts[1] + end + + if sym[lvl] then + sym = sym[lvl] + else + sym = rule.symbols.other + end + end + + local opts = {} + if obj.score then + table.insert(opts, 'score=' .. obj.score) + end + if obj.elapsed then + table.insert(opts, 'elapsed=' .. obj.elapsed) + end + + if rule.log_spamcause and obj.spamcause then + rspamd_logger.infox(task, 'vadesecure verdict="%s", score=%s, spamcause="%s", message-id="%s"', + verdict, obj.score, obj.spamcause, task:get_message_id()) + else + lua_util.debugm(rule.name, task, 'vadesecure returned verdict="%s", score=%s, spamcause="%s"', + verdict, obj.score, obj.spamcause) + end + + if #vparts > 0 then + table.insert(opts, 'verdict=' .. verdict .. ';' .. table.concat(vparts, ':')) + end + + task:insert_result(sym.symbol, 1.0, opts) + end + end + + request_data.callback = vade_callback + http.request(request_data) + end + + if common.need_check(task, content, rule, digest, vade_check_uncached) then + return + else + vade_check_uncached() + end + +end + return { type = {'vadesecure', 'scanner'}, description = 'VadeSecure Filterd interface', |