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.

antivirus.lua 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]] --
  13. local rspamd_logger = require "rspamd_logger"
  14. local lua_util = require "lua_util"
  15. local rspamd_util = require "rspamd_util"
  16. local lua_redis = require "lua_redis"
  17. local fun = require "fun"
  18. local lua_antivirus = require("lua_scanners").filter('antivirus')
  19. local common = require "lua_scanners/common"
  20. local redis_params
  21. local N = "antivirus"
  22. if confighelp then
  23. rspamd_config:add_example(nil, 'antivirus',
  24. "Check messages for viruses",
  25. [[
  26. antivirus {
  27. # multiple scanners could be checked, for each we create a configuration block with an arbitrary name
  28. clamav {
  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. # message = '${SCANNER}: virus found: "${VIRUS}"';
  33. # Scan mime_parts separately - otherwise the complete mail will be transferred to AV Scanner
  34. #scan_mime_parts = true;
  35. # Scanning Text is suitable for some av scanner databases (e.g. Sanesecurity)
  36. #scan_text_mime = false;
  37. #scan_image_mime = false;
  38. # If `max_size` is set, messages > n bytes in size are not scanned
  39. max_size = 20000000;
  40. # symbol to add (add it to metric if you want non-zero weight)
  41. symbol = "CLAM_VIRUS";
  42. # type of scanner: "clamav", "fprot", "sophos" or "savapi"
  43. type = "clamav";
  44. # For "savapi" you must also specify the following variable
  45. product_id = 12345;
  46. # You can enable logging for clean messages
  47. log_clean = true;
  48. # servers to query (if port is unspecified, scanner-specific default is used)
  49. # can be specified multiple times to pool servers
  50. # can be set to a path to a unix socket
  51. # Enable this in local.d/antivirus.conf
  52. servers = "127.0.0.1:3310";
  53. # if `patterns` is specified virus name will be matched against provided regexes and the related
  54. # symbol will be yielded if a match is found. If no match is found, default symbol is yielded.
  55. patterns {
  56. # symbol_name = "pattern";
  57. JUST_EICAR = "^Eicar-Test-Signature$";
  58. }
  59. # `whitelist` points to a map of IP addresses. Mail from these addresses is not scanned.
  60. whitelist = "/etc/rspamd/antivirus.wl";
  61. # Replace content that exactly matches the following string to the EICAR pattern
  62. # Useful for E2E testing when another party removes/blocks EICAR attachments
  63. #eicar_fake_pattern = 'testpatterneicar';
  64. }
  65. }
  66. ]])
  67. return
  68. end
  69. -- Encode as base32 in the source to avoid crappy stuff
  70. local eicar_pattern = rspamd_util.decode_base32(
  71. [[akp6woykfbonrepmwbzyfpbmibpone3mj3pgwbffzj9e1nfjdkorisckwkohrnfe1nt41y3jwk1cirjki4w4nkieuni4ndfjcktnn1yjmb1wn]]
  72. )
  73. local function add_antivirus_rule(sym, opts)
  74. if not opts.type then
  75. rspamd_logger.errx(rspamd_config, 'unknown type for AV rule %s', sym)
  76. return nil
  77. end
  78. if not opts.symbol then
  79. opts.symbol = sym:upper()
  80. end
  81. local cfg = lua_antivirus[opts.type]
  82. if not cfg then
  83. rspamd_logger.errx(rspamd_config, 'unknown antivirus type: %s',
  84. opts.type)
  85. return nil
  86. end
  87. if not opts.symbol_fail then
  88. opts.symbol_fail = opts.symbol .. '_FAIL'
  89. end
  90. if not opts.symbol_encrypted then
  91. opts.symbol_encrypted = opts.symbol .. '_ENCRYPTED'
  92. end
  93. if not opts.symbol_macro then
  94. opts.symbol_macro = opts.symbol .. '_MACRO'
  95. end
  96. -- WORKAROUND for deprecated attachments_only
  97. if opts.attachments_only ~= nil then
  98. opts.scan_mime_parts = opts.attachments_only
  99. rspamd_logger.warnx(rspamd_config, '%s [%s]: Using attachments_only is deprecated. ' ..
  100. 'Please use scan_mime_parts = %s instead', opts.symbol, opts.type, opts.attachments_only)
  101. end
  102. -- WORKAROUND for deprecated attachments_only
  103. local rule = cfg.configure(opts)
  104. if not rule then
  105. return nil
  106. end
  107. rule.type = opts.type
  108. rule.symbol_fail = opts.symbol_fail
  109. rule.symbol_encrypted = opts.symbol_encrypted
  110. rule.redis_params = redis_params
  111. if not rule then
  112. rspamd_logger.errx(rspamd_config, 'cannot configure %s for %s',
  113. opts.type, opts.symbol)
  114. return nil
  115. end
  116. rule.patterns = common.create_regex_table(opts.patterns or {})
  117. rule.patterns_fail = common.create_regex_table(opts.patterns_fail or {})
  118. lua_redis.register_prefix(rule.prefix .. '_*', N,
  119. string.format('Antivirus cache for rule "%s"',
  120. rule.type), {
  121. type = 'string',
  122. })
  123. -- if any mime_part filter defined, do not scan all attachments
  124. if opts.mime_parts_filter_regex ~= nil
  125. or opts.mime_parts_filter_ext ~= nil then
  126. rule.scan_all_mime_parts = false
  127. else
  128. rule.scan_all_mime_parts = true
  129. end
  130. rule.patterns = common.create_regex_table(opts.patterns or {})
  131. rule.patterns_fail = common.create_regex_table(opts.patterns_fail or {})
  132. rule.mime_parts_filter_regex = common.create_regex_table(opts.mime_parts_filter_regex or {})
  133. rule.mime_parts_filter_ext = common.create_regex_table(opts.mime_parts_filter_ext or {})
  134. if opts.whitelist then
  135. rule.whitelist = rspamd_config:add_hash_map(opts.whitelist)
  136. end
  137. return function(task)
  138. if rule.scan_mime_parts then
  139. fun.each(function(p)
  140. local content = p:get_content()
  141. local clen = #content
  142. if content and clen > 0 then
  143. if opts.eicar_fake_pattern then
  144. if type(opts.eicar_fake_pattern) == 'string' then
  145. -- Convert it to Rspamd text
  146. local rspamd_text = require "rspamd_text"
  147. opts.eicar_fake_pattern = rspamd_text.fromstring(opts.eicar_fake_pattern)
  148. end
  149. if clen == #opts.eicar_fake_pattern and content == opts.eicar_fake_pattern then
  150. rspamd_logger.infox(task, 'found eicar fake replacement part in the part (filename="%s")',
  151. p:get_filename())
  152. content = eicar_pattern
  153. end
  154. end
  155. cfg.check(task, content, p:get_digest(), rule, p)
  156. end
  157. end, common.check_parts_match(task, rule))
  158. else
  159. cfg.check(task, task:get_content(), task:get_digest(), rule)
  160. end
  161. end
  162. end
  163. -- Registration
  164. local opts = rspamd_config:get_all_opt(N)
  165. if opts and type(opts) == 'table' then
  166. redis_params = lua_redis.parse_redis_server(N)
  167. local has_valid = false
  168. for k, m in pairs(opts) do
  169. if type(m) == 'table' then
  170. if not m.type then
  171. m.type = k
  172. end
  173. if not m.name then
  174. m.name = k
  175. end
  176. local cb = add_antivirus_rule(k, m)
  177. if not cb then
  178. rspamd_logger.errx(rspamd_config, 'cannot add rule: "' .. k .. '"')
  179. lua_util.config_utils.push_config_error(N, 'cannot add AV rule: "' .. k .. '"')
  180. else
  181. rspamd_logger.infox(rspamd_config, 'added antivirus engine %s -> %s', k, m.symbol)
  182. local t = {
  183. name = m.symbol,
  184. callback = cb,
  185. score = 0.0,
  186. group = N
  187. }
  188. if m.symbol_type == 'postfilter' then
  189. t.type = 'postfilter'
  190. t.priority = lua_util.symbols_priorities.medium
  191. else
  192. t.type = 'normal'
  193. end
  194. t.augmentations = {}
  195. if type(m.timeout) == 'number' then
  196. -- Here, we ignore possible DNS timeout and timeout from multiple retries
  197. -- as these situations are not usual nor likely for the antivirus module
  198. table.insert(t.augmentations, string.format("timeout=%f", m.timeout))
  199. end
  200. local id = rspamd_config:register_symbol(t)
  201. rspamd_config:register_symbol({
  202. type = 'virtual',
  203. name = m['symbol_fail'],
  204. parent = id,
  205. score = 0.0,
  206. group = N
  207. })
  208. rspamd_config:register_symbol({
  209. type = 'virtual',
  210. name = m['symbol_encrypted'],
  211. parent = id,
  212. score = 0.0,
  213. group = N
  214. })
  215. rspamd_config:register_symbol({
  216. type = 'virtual',
  217. name = m['symbol_macro'],
  218. parent = id,
  219. score = 0.0,
  220. group = N
  221. })
  222. has_valid = true
  223. if type(m['patterns']) == 'table' then
  224. if m['patterns'][1] then
  225. for _, p in ipairs(m['patterns']) do
  226. if type(p) == 'table' then
  227. for sym in pairs(p) do
  228. rspamd_logger.debugm(N, rspamd_config, 'registering: %1', {
  229. type = 'virtual',
  230. name = sym,
  231. parent = m['symbol'],
  232. parent_id = id,
  233. group = N
  234. })
  235. rspamd_config:register_symbol({
  236. type = 'virtual',
  237. name = sym,
  238. parent = id,
  239. score = 0.0,
  240. group = N
  241. })
  242. end
  243. end
  244. end
  245. else
  246. for sym in pairs(m['patterns']) do
  247. rspamd_config:register_symbol({
  248. type = 'virtual',
  249. name = sym,
  250. parent = id,
  251. score = 0.0,
  252. group = N
  253. })
  254. end
  255. end
  256. end
  257. if type(m['patterns_fail']) == 'table' then
  258. if m['patterns_fail'][1] then
  259. for _, p in ipairs(m['patterns_fail']) do
  260. if type(p) == 'table' then
  261. for sym in pairs(p) do
  262. rspamd_logger.debugm(N, rspamd_config, 'registering: %1', {
  263. type = 'virtual',
  264. name = sym,
  265. parent = m['symbol'],
  266. parent_id = id,
  267. group = N
  268. })
  269. rspamd_config:register_symbol({
  270. type = 'virtual',
  271. name = sym,
  272. parent = id,
  273. score = 0.0,
  274. group = N
  275. })
  276. end
  277. end
  278. end
  279. else
  280. for sym in pairs(m['patterns_fail']) do
  281. rspamd_config:register_symbol({
  282. type = 'virtual',
  283. name = sym,
  284. parent = id,
  285. score = 0.0,
  286. group = N
  287. })
  288. end
  289. end
  290. end
  291. if m['score'] then
  292. -- Register metric symbol
  293. local description = 'antivirus symbol'
  294. local group = N
  295. if m['description'] then
  296. description = m['description']
  297. end
  298. if m['group'] then
  299. group = m['group']
  300. end
  301. rspamd_config:set_metric_symbol({
  302. name = m['symbol'],
  303. score = m['score'],
  304. description = description,
  305. group = group or 'antivirus'
  306. })
  307. end
  308. end
  309. end
  310. end
  311. if not has_valid then
  312. lua_util.disable_module(N, 'config')
  313. end
  314. end