diff options
Diffstat (limited to 'lualib/lua_scanners/vadesecure.lua')
-rw-r--r-- | lualib/lua_scanners/vadesecure.lua | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/lualib/lua_scanners/vadesecure.lua b/lualib/lua_scanners/vadesecure.lua new file mode 100644 index 0000000..826573a --- /dev/null +++ b/lualib/lua_scanners/vadesecure.lua @@ -0,0 +1,351 @@ +--[[ +Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + +--[[[ +-- @module vadesecure +-- This module contains Vadesecure Filterd interface +--]] + +local lua_util = require "lua_util" +local http = require "rspamd_http" +local upstream_list = require "rspamd_upstream_list" +local rspamd_logger = require "rspamd_logger" +local ucl = require "ucl" +local common = require "lua_scanners/common" + +local N = 'vadesecure' + +local function vade_config(opts) + + local vade_conf = { + name = N, + default_port = 23808, + url = '/api/v1/scan', + use_https = false, + timeout = 5.0, + log_clean = false, + retransmits = 1, + cache_expire = 7200, -- expire redis in 2h + message = '${SCANNER}: spam message found: "${VIRUS}"', + detection_category = "hash", + default_score = 1, + action = false, + log_spamcause = true, + symbol_fail = 'VADE_FAIL', + symbol = 'VADE_CHECK', + settings_outbound = nil, -- Set when there is a settings id for outbound messages + symbols = { + clean = { + symbol = 'VADE_CLEAN', + score = -0.5, + description = 'VadeSecure decided message to be clean' + }, + spam = { + high = { + symbol = 'VADE_SPAM_HIGH', + score = 8.0, + description = 'VadeSecure decided message to be clearly spam' + }, + medium = { + symbol = 'VADE_SPAM_MEDIUM', + score = 5.0, + description = 'VadeSecure decided message to be highly likely spam' + }, + low = { + symbol = 'VADE_SPAM_LOW', + score = 2.0, + description = 'VadeSecure decided message to be likely spam' + }, + }, + malware = { + symbol = 'VADE_MALWARE', + score = 8.0, + description = 'VadeSecure decided message to be malware' + }, + scam = { + symbol = 'VADE_SCAM', + score = 7.0, + description = 'VadeSecure decided message to be scam' + }, + phishing = { + symbol = 'VADE_PHISHING', + score = 8.0, + description = 'VadeSecure decided message to be phishing' + }, + commercial = { + symbol = 'VADE_COMMERCIAL', + score = 0.0, + description = 'VadeSecure decided message to be commercial message' + }, + community = { + symbol = 'VADE_COMMUNITY', + score = 0.0, + description = 'VadeSecure decided message to be community message' + }, + transactional = { + symbol = 'VADE_TRANSACTIONAL', + score = 0.0, + description = 'VadeSecure decided message to be transactional message' + }, + suspect = { + symbol = 'VADE_SUSPECT', + score = 3.0, + description = 'VadeSecure decided message to be suspicious message' + }, + bounce = { + symbol = 'VADE_BOUNCE', + score = 0.0, + description = 'VadeSecure decided message to be bounce message' + }, + other = 'VADE_OTHER', + } + } + + vade_conf = lua_util.override_defaults(vade_conf, opts) + + if not vade_conf.prefix then + vade_conf.prefix = 'rs_' .. vade_conf.name .. '_' + end + + if not vade_conf.log_prefix then + if vade_conf.name:lower() == vade_conf.type:lower() then + vade_conf.log_prefix = vade_conf.name + else + vade_conf.log_prefix = vade_conf.name .. ' (' .. vade_conf.type .. ')' + end + end + + if not vade_conf.servers and vade_conf.socket then + vade_conf.servers = vade_conf.socket + end + + if not vade_conf.servers then + rspamd_logger.errx(rspamd_config, 'no servers defined') + + return nil + end + + vade_conf.upstreams = upstream_list.create(rspamd_config, + vade_conf.servers, + vade_conf.default_port) + + if vade_conf.upstreams then + lua_util.add_debug_alias('external_services', vade_conf.name) + return vade_conf + end + + rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', + vade_conf['servers']) + return nil +end + +local function vade_check(task, content, digest, rule, maybe_part) + 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 + + if rule.settings_outbound then + local settings_id = task:get_settings_id() + + if settings_id then + local lua_settings = require "lua_settings" + -- Convert to string + settings_id = lua_settings.settings_by_id(settings_id) + + if settings_id then + settings_id = settings_id.name or '' + + if settings_id == rule.settings_outbound then + lua_util.debugm(rule.name, task, '%s settings has matched outbound', + settings_id) + hdrs['X-Params'] = 'mode=smtpout' + end + end + end + 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.condition_check_and_continue(task, content, rule, digest, + vade_check_uncached, maybe_part) then + return + else + vade_check_uncached() + end + +end + +return { + type = { 'vadesecure', 'scanner' }, + description = 'VadeSecure Filterd interface', + configure = vade_config, + check = vade_check, + name = N +} |