summaryrefslogtreecommitdiffstats
path: root/lualib
diff options
context:
space:
mode:
authorVsevolod Stakhov <vsevolod@highsecure.ru>2019-10-04 11:39:31 +0100
committerGitHub <noreply@github.com>2019-10-04 11:39:31 +0100
commit403d8eef0db29e81b4524844b9630ccfc01b8305 (patch)
treef7eb3edb579a05a1bf4e3985ed0911414632b1ad /lualib
parent1880656d8c4e5c52f0d890250871efa0927d8101 (diff)
parentadc10228b43a7b1a8d6203579611d9cca04545ea (diff)
downloadrspamd-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.lua21
-rw-r--r--lualib/lua_scanners/common.lua100
-rw-r--r--lualib/lua_scanners/dcc.lua138
-rw-r--r--lualib/lua_scanners/fprot.lua21
-rw-r--r--lualib/lua_scanners/icap.lua520
-rw-r--r--lualib/lua_scanners/kaspersky_av.lua21
-rw-r--r--lualib/lua_scanners/oletools.lua393
-rw-r--r--lualib/lua_scanners/savapi.lua23
-rw-r--r--lualib/lua_scanners/sophos.lua23
-rw-r--r--lualib/lua_scanners/spamassassin.lua134
-rw-r--r--lualib/lua_scanners/vadesecure.lua334
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',