You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

external_services.lua 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Copyright (c) 2019, Carsten Rosenberg <c.rosenberg@heinlein-support.de>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]] --
  14. local rspamd_logger = require "rspamd_logger"
  15. local lua_util = require "lua_util"
  16. local lua_redis = require "lua_redis"
  17. local fun = require "fun"
  18. local lua_scanners = require("lua_scanners").filter('scanner')
  19. local common = require "lua_scanners/common"
  20. local redis_params
  21. local N = "external_services"
  22. if confighelp then
  23. rspamd_config:add_example(nil, 'external_services',
  24. "Check messages using external services (e.g. OEM AS engines, DCC, Pyzor etc)",
  25. [[
  26. external_services {
  27. # multiple scanners could be checked, for each we create a configuration block with an arbitrary name
  28. oletools {
  29. # If set force this action if any virus is found (default unset: no action is forced)
  30. # action = "reject";
  31. # If set, then rejection message is set to this value (mention single quotes)
  32. # If `max_size` is set, messages > n bytes in size are not scanned
  33. # max_size = 20000000;
  34. # log_clean = true;
  35. # servers = "127.0.0.1:10050";
  36. # cache_expire = 86400;
  37. # scan_mime_parts = true;
  38. # extended = false;
  39. # if `patterns` is specified virus name will be matched against provided regexes and the related
  40. # symbol will be yielded if a match is found. If no match is found, default symbol is yielded.
  41. patterns {
  42. # symbol_name = "pattern";
  43. JUST_EICAR = "^Eicar-Test-Signature$";
  44. }
  45. # mime-part regex matching in content-type or filename
  46. mime_parts_filter_regex {
  47. #GEN1 = "application\/octet-stream";
  48. DOC2 = "application\/msword";
  49. DOC3 = "application\/vnd\.ms-word.*";
  50. XLS = "application\/vnd\.ms-excel.*";
  51. PPT = "application\/vnd\.ms-powerpoint.*";
  52. GEN2 = "application\/vnd\.openxmlformats-officedocument.*";
  53. }
  54. # Mime-Part filename extension matching (no regex)
  55. mime_parts_filter_ext {
  56. doc = "doc";
  57. dot = "dot";
  58. docx = "docx";
  59. dotx = "dotx";
  60. docm = "docm";
  61. dotm = "dotm";
  62. xls = "xls";
  63. xlt = "xlt";
  64. xla = "xla";
  65. xlsx = "xlsx";
  66. xltx = "xltx";
  67. xlsm = "xlsm";
  68. xltm = "xltm";
  69. xlam = "xlam";
  70. xlsb = "xlsb";
  71. ppt = "ppt";
  72. pot = "pot";
  73. pps = "pps";
  74. ppa = "ppa";
  75. pptx = "pptx";
  76. potx = "potx";
  77. ppsx = "ppsx";
  78. ppam = "ppam";
  79. pptm = "pptm";
  80. potm = "potm";
  81. ppsm = "ppsm";
  82. }
  83. # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned.
  84. whitelist = "/etc/rspamd/antivirus.wl";
  85. }
  86. dcc {
  87. # If set force this action if any virus is found (default unset: no action is forced)
  88. # action = "reject";
  89. # If set, then rejection message is set to this value (mention single quotes)
  90. # If `max_size` is set, messages > n bytes in size are not scanned
  91. max_size = 20000000;
  92. #servers = "127.0.0.1:10045;
  93. # if `patterns` is specified virus name will be matched against provided regexes and the related
  94. # symbol will be yielded if a match is found. If no match is found, default symbol is yielded.
  95. patterns {
  96. # symbol_name = "pattern";
  97. JUST_EICAR = "^Eicar-Test-Signature$";
  98. }
  99. # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned.
  100. whitelist = "/etc/rspamd/antivirus.wl";
  101. }
  102. }
  103. ]])
  104. return
  105. end
  106. local function add_scanner_rule(sym, opts)
  107. if not opts.type then
  108. rspamd_logger.errx(rspamd_config, 'unknown type for external scanner rule %s', sym)
  109. return nil
  110. end
  111. local cfg = lua_scanners[opts.type]
  112. if not cfg then
  113. rspamd_logger.errx(rspamd_config, 'unknown external scanner type: %s',
  114. opts.type)
  115. return nil
  116. end
  117. local rule = cfg.configure(opts)
  118. if not rule then
  119. rspamd_logger.errx(rspamd_config, 'cannot configure %s for %s',
  120. opts.type, rule.symbol or sym:upper())
  121. return nil
  122. end
  123. rule.type = opts.type
  124. -- Fill missing symbols
  125. if not rule.symbol then
  126. rule.symbol = sym:upper()
  127. end
  128. if not rule.symbol_fail then
  129. rule.symbol_fail = rule.symbol .. '_FAIL'
  130. end
  131. if not rule.symbol_encrypted then
  132. rule.symbol_encrypted = rule.symbol .. '_ENCRYPTED'
  133. end
  134. if not rule.symbol_macro then
  135. rule.symbol_macro = rule.symbol .. '_MACRO'
  136. end
  137. rule.redis_params = redis_params
  138. lua_redis.register_prefix(rule.prefix .. '_*', N,
  139. string.format('External services cache for rule "%s"',
  140. rule.type), {
  141. type = 'string',
  142. })
  143. -- if any mime_part filter defined, do not scan all attachments
  144. if opts.mime_parts_filter_regex ~= nil
  145. or opts.mime_parts_filter_ext ~= nil then
  146. rule.scan_all_mime_parts = false
  147. else
  148. rule.scan_all_mime_parts = true
  149. end
  150. rule.patterns = common.create_regex_table(opts.patterns or {})
  151. rule.patterns_fail = common.create_regex_table(opts.patterns_fail or {})
  152. rule.mime_parts_filter_regex = common.create_regex_table(opts.mime_parts_filter_regex or {})
  153. rule.mime_parts_filter_ext = common.create_regex_table(opts.mime_parts_filter_ext or {})
  154. if opts.whitelist then
  155. rule.whitelist = rspamd_config:add_hash_map(opts.whitelist)
  156. end
  157. local function scan_cb(task)
  158. if rule.scan_mime_parts then
  159. fun.each(function(p)
  160. local content = p:get_content()
  161. if content and #content > 0 then
  162. cfg.check(task, content, p:get_digest(), rule)
  163. end
  164. end, common.check_parts_match(task, rule))
  165. else
  166. cfg.check(task, task:get_content(), task:get_digest(), rule)
  167. end
  168. end
  169. rspamd_logger.infox(rspamd_config, 'registered external services rule: symbol %s; type %s',
  170. rule.symbol, rule.type)
  171. return scan_cb, rule
  172. end
  173. -- Registration
  174. local opts = rspamd_config:get_all_opt(N)
  175. if opts and type(opts) == 'table' then
  176. redis_params = lua_redis.parse_redis_server(N)
  177. local has_valid = false
  178. for k, m in pairs(opts) do
  179. if type(m) == 'table' and m.servers then
  180. if not m.type then m.type = k end
  181. if not m.name then m.name = k end
  182. local cb, nrule = add_scanner_rule(k, m)
  183. if not cb then
  184. rspamd_logger.errx(rspamd_config, 'cannot add rule: "' .. k .. '"')
  185. else
  186. m = nrule
  187. local t = {
  188. name = m.symbol,
  189. callback = cb,
  190. score = 0.0,
  191. group = N
  192. }
  193. if m.symbol_type == 'postfilter' then
  194. t.type = 'postfilter'
  195. t.priority = lua_util.symbols_priorities.medium
  196. else
  197. t.type = 'normal'
  198. end
  199. t.augmentations = {}
  200. if type(m.timeout) == 'number' then
  201. -- Here, we ignore possible DNS timeout and timeout from multiple retries
  202. -- as these situations are not usual nor likely for the external_services module
  203. table.insert(t.augmentations, string.format("timeout=%f", m.timeout))
  204. end
  205. local id = rspamd_config:register_symbol(t)
  206. if m.symbol_fail then
  207. rspamd_config:register_symbol({
  208. type = 'virtual',
  209. name = m['symbol_fail'],
  210. parent = id,
  211. score = 0.0,
  212. group = N
  213. })
  214. end
  215. if m.symbol_encrypted then
  216. rspamd_config:register_symbol({
  217. type = 'virtual',
  218. name = m['symbol_encrypted'],
  219. parent = id,
  220. score = 0.0,
  221. group = N
  222. })
  223. end
  224. if m.symbol_macro then
  225. rspamd_config:register_symbol({
  226. type = 'virtual',
  227. name = m['symbol_macro'],
  228. parent = id,
  229. score = 0.0,
  230. group = N
  231. })
  232. end
  233. has_valid = true
  234. if type(m['patterns']) == 'table' then
  235. if m['patterns'][1] then
  236. for _, p in ipairs(m['patterns']) do
  237. if type(p) == 'table' then
  238. for sym in pairs(p) do
  239. rspamd_logger.debugm(N, rspamd_config, 'registering: %1', {
  240. type = 'virtual',
  241. name = sym,
  242. parent = m['symbol'],
  243. parent_id = id,
  244. })
  245. rspamd_config:register_symbol({
  246. type = 'virtual',
  247. name = sym,
  248. parent = id,
  249. score = 0.0,
  250. group = N
  251. })
  252. end
  253. end
  254. end
  255. else
  256. for sym in pairs(m['patterns']) do
  257. rspamd_config:register_symbol({
  258. type = 'virtual',
  259. name = sym,
  260. parent = id,
  261. score = 0.0,
  262. group = N
  263. })
  264. end
  265. end
  266. end
  267. if type(m['patterns_fail']) == 'table' then
  268. if m['patterns_fail'][1] then
  269. for _, p in ipairs(m['patterns_fail']) do
  270. if type(p) == 'table' then
  271. for sym in pairs(p) do
  272. rspamd_logger.debugm(N, rspamd_config, 'registering: %1', {
  273. type = 'virtual',
  274. name = sym,
  275. parent = m['symbol'],
  276. parent_id = id,
  277. })
  278. rspamd_config:register_symbol({
  279. type = 'virtual',
  280. name = sym,
  281. parent = id,
  282. score = 0.0,
  283. group = N
  284. })
  285. end
  286. end
  287. end
  288. else
  289. for sym in pairs(m['patterns_fail']) do
  290. rspamd_config:register_symbol({
  291. type = 'virtual',
  292. name = sym,
  293. parent = id,
  294. score = 0.0,
  295. group = N
  296. })
  297. end
  298. end
  299. end
  300. if m.symbols then
  301. local function reg_symbols(tbl)
  302. for _,sym in pairs(tbl) do
  303. if type(sym) == 'string' then
  304. rspamd_config:register_symbol({
  305. type = 'virtual',
  306. name = sym,
  307. parent = id,
  308. group = N
  309. })
  310. elseif type(sym) == 'table' then
  311. if sym.symbol then
  312. rspamd_config:register_symbol({
  313. type = 'virtual',
  314. name = sym.symbol,
  315. parent = id,
  316. group = N
  317. })
  318. if sym.score then
  319. rspamd_config:set_metric_symbol({
  320. name = sym.symbol,
  321. score = sym.score,
  322. description = sym.description,
  323. group = sym.group or N,
  324. })
  325. end
  326. else
  327. reg_symbols(sym)
  328. end
  329. end
  330. end
  331. end
  332. reg_symbols(m.symbols)
  333. end
  334. if m['score'] then
  335. -- Register metric symbol
  336. local description = 'external services symbol'
  337. local group = N
  338. if m['description'] then
  339. description = m['description']
  340. end
  341. if m['group'] then
  342. group = m['group']
  343. end
  344. rspamd_config:set_metric_symbol({
  345. name = m['symbol'],
  346. score = m['score'],
  347. description = description,
  348. group = group
  349. })
  350. end
  351. -- Add preloads if a module requires that
  352. if type(m.preloads) == 'table' then
  353. for _,preload in ipairs(m.preloads) do
  354. rspamd_config:add_on_load(function(cfg, ev_base, worker)
  355. preload(m, cfg, ev_base, worker)
  356. end)
  357. end
  358. end
  359. end
  360. end
  361. end
  362. if not has_valid then
  363. lua_util.disable_module(N, 'config')
  364. end
  365. end