summaryrefslogtreecommitdiffstats
path: root/src/plugins/lua/external_services.lua
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/lua/external_services.lua')
-rw-r--r--src/plugins/lua/external_services.lua408
1 files changed, 408 insertions, 0 deletions
diff --git a/src/plugins/lua/external_services.lua b/src/plugins/lua/external_services.lua
new file mode 100644
index 0000000..e299d9f
--- /dev/null
+++ b/src/plugins/lua/external_services.lua
@@ -0,0 +1,408 @@
+--[[
+Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
+Copyright (c) 2019, Carsten Rosenberg <c.rosenberg@heinlein-support.de>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+]] --
+
+local rspamd_logger = require "rspamd_logger"
+local lua_util = require "lua_util"
+local lua_redis = require "lua_redis"
+local fun = require "fun"
+local lua_scanners = require("lua_scanners").filter('scanner')
+local common = require "lua_scanners/common"
+local redis_params
+
+local N = "external_services"
+
+if confighelp then
+ rspamd_config:add_example(nil, 'external_services',
+ "Check messages using external services (e.g. OEM AS engines, DCC, Pyzor etc)",
+ [[
+ external_services {
+ # multiple scanners could be checked, for each we create a configuration block with an arbitrary name
+
+ oletools {
+ # If set force this action if any virus is found (default unset: no action is forced)
+ # action = "reject";
+ # If set, then rejection message is set to this value (mention single quotes)
+ # If `max_size` is set, messages > n bytes in size are not scanned
+ # max_size = 20000000;
+ # log_clean = true;
+ # servers = "127.0.0.1:10050";
+ # cache_expire = 86400;
+ # scan_mime_parts = true;
+ # extended = false;
+ # if `patterns` is specified virus name will be matched against provided regexes and the related
+ # symbol will be yielded if a match is found. If no match is found, default symbol is yielded.
+ patterns {
+ # symbol_name = "pattern";
+ JUST_EICAR = "^Eicar-Test-Signature$";
+ }
+ # mime-part regex matching in content-type or filename
+ mime_parts_filter_regex {
+ #GEN1 = "application\/octet-stream";
+ DOC2 = "application\/msword";
+ DOC3 = "application\/vnd\.ms-word.*";
+ XLS = "application\/vnd\.ms-excel.*";
+ PPT = "application\/vnd\.ms-powerpoint.*";
+ GEN2 = "application\/vnd\.openxmlformats-officedocument.*";
+ }
+ # Mime-Part filename extension matching (no regex)
+ mime_parts_filter_ext {
+ doc = "doc";
+ dot = "dot";
+ docx = "docx";
+ dotx = "dotx";
+ docm = "docm";
+ dotm = "dotm";
+ xls = "xls";
+ xlt = "xlt";
+ xla = "xla";
+ xlsx = "xlsx";
+ xltx = "xltx";
+ xlsm = "xlsm";
+ xltm = "xltm";
+ xlam = "xlam";
+ xlsb = "xlsb";
+ ppt = "ppt";
+ pot = "pot";
+ pps = "pps";
+ ppa = "ppa";
+ pptx = "pptx";
+ potx = "potx";
+ ppsx = "ppsx";
+ ppam = "ppam";
+ pptm = "pptm";
+ potm = "potm";
+ ppsm = "ppsm";
+ }
+ # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned.
+ whitelist = "/etc/rspamd/antivirus.wl";
+ }
+ dcc {
+ # If set force this action if any virus is found (default unset: no action is forced)
+ # action = "reject";
+ # If set, then rejection message is set to this value (mention single quotes)
+ # If `max_size` is set, messages > n bytes in size are not scanned
+ max_size = 20000000;
+ #servers = "127.0.0.1:10045;
+ # if `patterns` is specified virus name will be matched against provided regexes and the related
+ # symbol will be yielded if a match is found. If no match is found, default symbol is yielded.
+ patterns {
+ # symbol_name = "pattern";
+ JUST_EICAR = "^Eicar-Test-Signature$";
+ }
+ # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned.
+ whitelist = "/etc/rspamd/antivirus.wl";
+ }
+ }
+ ]])
+ return
+end
+
+local function add_scanner_rule(sym, opts)
+ if not opts.type then
+ rspamd_logger.errx(rspamd_config, 'unknown type for external scanner rule %s', sym)
+ return nil
+ end
+
+ local cfg = lua_scanners[opts.type]
+
+ if not cfg then
+ rspamd_logger.errx(rspamd_config, 'unknown external scanner type: %s',
+ opts.type)
+ return nil
+ end
+
+ local rule = cfg.configure(opts)
+
+ if not rule then
+ rspamd_logger.errx(rspamd_config, 'cannot configure %s for %s',
+ opts.type, rule.symbol or sym:upper())
+ return nil
+ end
+
+ rule.type = opts.type
+ -- Fill missing symbols
+ if not rule.symbol then
+ rule.symbol = sym:upper()
+ end
+ if not rule.symbol_fail then
+ rule.symbol_fail = rule.symbol .. '_FAIL'
+ end
+ if not rule.symbol_encrypted then
+ rule.symbol_encrypted = rule.symbol .. '_ENCRYPTED'
+ end
+ if not rule.symbol_macro then
+ rule.symbol_macro = rule.symbol .. '_MACRO'
+ end
+
+ rule.redis_params = redis_params
+
+ lua_redis.register_prefix(rule.prefix .. '_*', N,
+ string.format('External services cache for rule "%s"',
+ rule.type), {
+ type = 'string',
+ })
+
+ -- if any mime_part filter defined, do not scan all attachments
+ if opts.mime_parts_filter_regex ~= nil
+ or opts.mime_parts_filter_ext ~= nil then
+ rule.scan_all_mime_parts = false
+ else
+ rule.scan_all_mime_parts = true
+ end
+
+ rule.patterns = common.create_regex_table(opts.patterns or {})
+ rule.patterns_fail = common.create_regex_table(opts.patterns_fail or {})
+
+ rule.mime_parts_filter_regex = common.create_regex_table(opts.mime_parts_filter_regex or {})
+
+ rule.mime_parts_filter_ext = common.create_regex_table(opts.mime_parts_filter_ext or {})
+
+ if opts.whitelist then
+ rule.whitelist = rspamd_config:add_hash_map(opts.whitelist)
+ end
+
+ local function scan_cb(task)
+ if rule.scan_mime_parts then
+
+ fun.each(function(p)
+ local content = p:get_content()
+ if content and #content > 0 then
+ cfg.check(task, content, p:get_digest(), rule, p)
+ end
+ end, common.check_parts_match(task, rule))
+
+ else
+ cfg.check(task, task:get_content(), task:get_digest(), rule, nil)
+ end
+ end
+
+ rspamd_logger.infox(rspamd_config, 'registered external services rule: symbol %s; type %s',
+ rule.symbol, rule.type)
+
+ return scan_cb, rule
+end
+
+-- Registration
+local opts = rspamd_config:get_all_opt(N)
+if opts and type(opts) == 'table' then
+ redis_params = lua_redis.parse_redis_server(N)
+ local has_valid = false
+ for k, m in pairs(opts) do
+ if type(m) == 'table' and m.servers then
+ if not m.type then
+ m.type = k
+ end
+ if not m.name then
+ m.name = k
+ end
+ local cb, nrule = add_scanner_rule(k, m)
+
+ if not cb then
+ rspamd_logger.errx(rspamd_config, 'cannot add rule: "' .. k .. '"')
+ else
+ m = nrule
+
+ local t = {
+ name = m.symbol,
+ callback = cb,
+ score = 0.0,
+ group = N
+ }
+
+ if m.symbol_type == 'postfilter' then
+ t.type = 'postfilter'
+ t.priority = lua_util.symbols_priorities.medium
+ else
+ t.type = 'normal'
+ end
+
+ t.augmentations = {}
+
+ if type(m.timeout) == 'number' then
+ -- Here, we ignore possible DNS timeout and timeout from multiple retries
+ -- as these situations are not usual nor likely for the external_services module
+ table.insert(t.augmentations, string.format("timeout=%f", m.timeout))
+ end
+
+ local id = rspamd_config:register_symbol(t)
+
+ if m.symbol_fail then
+ rspamd_config:register_symbol({
+ type = 'virtual',
+ name = m['symbol_fail'],
+ parent = id,
+ score = 0.0,
+ group = N
+ })
+ end
+
+ if m.symbol_encrypted then
+ rspamd_config:register_symbol({
+ type = 'virtual',
+ name = m['symbol_encrypted'],
+ parent = id,
+ score = 0.0,
+ group = N
+ })
+ end
+ if m.symbol_macro then
+ rspamd_config:register_symbol({
+ type = 'virtual',
+ name = m['symbol_macro'],
+ parent = id,
+ score = 0.0,
+ group = N
+ })
+ end
+ has_valid = true
+ if type(m['patterns']) == 'table' then
+ if m['patterns'][1] then
+ for _, p in ipairs(m['patterns']) do
+ if type(p) == 'table' then
+ for sym in pairs(p) do
+ rspamd_logger.debugm(N, rspamd_config, 'registering: %1', {
+ type = 'virtual',
+ name = sym,
+ parent = m['symbol'],
+ parent_id = id,
+ })
+ rspamd_config:register_symbol({
+ type = 'virtual',
+ name = sym,
+ parent = id,
+ score = 0.0,
+ group = N
+ })
+ end
+ end
+ end
+ else
+ for sym in pairs(m['patterns']) do
+ rspamd_config:register_symbol({
+ type = 'virtual',
+ name = sym,
+ parent = id,
+ score = 0.0,
+ group = N
+ })
+ end
+ end
+ end
+ if type(m['patterns_fail']) == 'table' then
+ if m['patterns_fail'][1] then
+ for _, p in ipairs(m['patterns_fail']) do
+ if type(p) == 'table' then
+ for sym in pairs(p) do
+ rspamd_logger.debugm(N, rspamd_config, 'registering: %1', {
+ type = 'virtual',
+ name = sym,
+ parent = m['symbol'],
+ parent_id = id,
+ })
+ rspamd_config:register_symbol({
+ type = 'virtual',
+ name = sym,
+ parent = id,
+ score = 0.0,
+ group = N
+ })
+ end
+ end
+ end
+ else
+ for sym in pairs(m['patterns_fail']) do
+ rspamd_config:register_symbol({
+ type = 'virtual',
+ name = sym,
+ parent = id,
+ score = 0.0,
+ group = N
+ })
+ end
+ end
+ end
+ if m.symbols then
+ local function reg_symbols(tbl)
+ for _, sym in pairs(tbl) do
+ if type(sym) == 'string' then
+ rspamd_config:register_symbol({
+ type = 'virtual',
+ name = sym,
+ parent = id,
+ group = N
+ })
+ elseif type(sym) == 'table' then
+ if sym.symbol then
+ rspamd_config:register_symbol({
+ type = 'virtual',
+ name = sym.symbol,
+ parent = id,
+ group = N
+ })
+
+ if sym.score then
+ rspamd_config:set_metric_symbol({
+ name = sym.symbol,
+ score = sym.score,
+ description = sym.description,
+ group = sym.group or N,
+ })
+ end
+ else
+ reg_symbols(sym)
+ end
+ end
+ end
+ end
+
+ reg_symbols(m.symbols)
+ end
+
+ if m['score'] then
+ -- Register metric symbol
+ local description = 'external services symbol'
+ local group = N
+ if m['description'] then
+ description = m['description']
+ end
+ if m['group'] then
+ group = m['group']
+ end
+ rspamd_config:set_metric_symbol({
+ name = m['symbol'],
+ score = m['score'],
+ description = description,
+ group = group
+ })
+ end
+
+ -- Add preloads if a module requires that
+ if type(m.preloads) == 'table' then
+ for _, preload in ipairs(m.preloads) do
+ rspamd_config:add_on_load(function(cfg, ev_base, worker)
+ preload(m, cfg, ev_base, worker)
+ end)
+ end
+ end
+ end
+ end
+ end
+
+ if not has_valid then
+ lua_util.disable_module(N, 'config')
+ end
+end