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 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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 opts.symbol = sym:upper() end
  79. local cfg = lua_antivirus[opts.type]
  80. if not cfg then
  81. rspamd_logger.errx(rspamd_config, 'unknown antivirus type: %s',
  82. opts.type)
  83. return nil
  84. end
  85. if not opts.symbol_fail then
  86. opts.symbol_fail = opts.symbol .. '_FAIL'
  87. end
  88. if not opts.symbol_encrypted then
  89. opts.symbol_encrypted = opts.symbol .. '_ENCRYPTED'
  90. end
  91. if not opts.symbol_macro then
  92. opts.symbol_macro = opts.symbol .. '_MACRO'
  93. end
  94. -- WORKAROUND for deprecated attachments_only
  95. if opts.attachments_only ~= nil then
  96. opts.scan_mime_parts = opts.attachments_only
  97. rspamd_logger.warnx(rspamd_config, '%s [%s]: Using attachments_only is deprecated. '..
  98. 'Please use scan_mime_parts = %s instead', opts.symbol, opts.type, opts.attachments_only)
  99. end
  100. -- WORKAROUND for deprecated attachments_only
  101. local rule = cfg.configure(opts)
  102. if not rule then return nil end
  103. rule.type = opts.type
  104. rule.symbol_fail = opts.symbol_fail
  105. rule.symbol_encrypted = opts.symbol_encrypted
  106. rule.redis_params = redis_params
  107. if not rule then
  108. rspamd_logger.errx(rspamd_config, 'cannot configure %s for %s',
  109. opts.type, opts.symbol)
  110. return nil
  111. end
  112. rule.patterns = common.create_regex_table(opts.patterns or {})
  113. rule.patterns_fail = common.create_regex_table(opts.patterns_fail or {})
  114. lua_redis.register_prefix(rule.prefix .. '_*', N,
  115. string.format('Antivirus cache for rule "%s"',
  116. rule.type), {
  117. type = 'string',
  118. })
  119. if opts.whitelist then
  120. rule.whitelist = rspamd_config:add_hash_map(opts.whitelist)
  121. end
  122. return function(task)
  123. if rule.scan_mime_parts then
  124. fun.each(function(p)
  125. local content = p:get_content()
  126. local clen = #content
  127. if content and clen > 0 then
  128. if opts.eicar_fake_pattern then
  129. if type(opts.eicar_fake_pattern) == 'string' then
  130. -- Convert it to Rspamd text
  131. local rspamd_text = require "rspamd_text"
  132. opts.eicar_fake_pattern = rspamd_text.fromstring(opts.eicar_fake_pattern)
  133. end
  134. if clen == #opts.eicar_fake_pattern and content == opts.eicar_fake_pattern then
  135. rspamd_logger.infox(task, 'found eicar fake replacement part in the part (filename="%s")',
  136. p:get_filename())
  137. content = eicar_pattern
  138. end
  139. end
  140. cfg.check(task, content, p:get_digest(), rule, p)
  141. end
  142. end, common.check_parts_match(task, rule))
  143. else
  144. cfg.check(task, task:get_content(), task:get_digest(), rule)
  145. end
  146. end
  147. end
  148. -- Registration
  149. local opts = rspamd_config:get_all_opt(N)
  150. if opts and type(opts) == 'table' then
  151. redis_params = lua_redis.parse_redis_server(N)
  152. local has_valid = false
  153. for k, m in pairs(opts) do
  154. if type(m) == 'table' then
  155. if not m.type then m.type = k end
  156. if not m.name then m.name = k end
  157. local cb = add_antivirus_rule(k, m)
  158. if not cb then
  159. rspamd_logger.errx(rspamd_config, 'cannot add rule: "' .. k .. '"')
  160. else
  161. rspamd_logger.infox(rspamd_config, 'added antivirus engine %s -> %s', k, m.symbol)
  162. local t = {
  163. name = m.symbol,
  164. callback = cb,
  165. score = 0.0,
  166. group = N
  167. }
  168. if m.symbol_type == 'postfilter' then
  169. t.type = 'postfilter'
  170. t.priority = lua_util.symbols_priorities.medium
  171. else
  172. t.type = 'normal'
  173. end
  174. t.augmentations = {}
  175. if type(m.timeout) == 'number' then
  176. -- Here, we ignore possible DNS timeout and timeout from multiple retries
  177. -- as these situations are not usual nor likely for the antivirus module
  178. table.insert(t.augmentations, string.format("timeout=%f", m.timeout))
  179. end
  180. local id = rspamd_config:register_symbol(t)
  181. rspamd_config:register_symbol({
  182. type = 'virtual',
  183. name = m['symbol_fail'],
  184. parent = id,
  185. score = 0.0,
  186. group = N
  187. })
  188. rspamd_config:register_symbol({
  189. type = 'virtual',
  190. name = m['symbol_encrypted'],
  191. parent = id,
  192. score = 0.0,
  193. group = N
  194. })
  195. rspamd_config:register_symbol({
  196. type = 'virtual',
  197. name = m['symbol_macro'],
  198. parent = id,
  199. score = 0.0,
  200. group = N
  201. })
  202. has_valid = true
  203. if type(m['patterns']) == 'table' then
  204. if m['patterns'][1] then
  205. for _, p in ipairs(m['patterns']) do
  206. if type(p) == 'table' then
  207. for sym in pairs(p) do
  208. rspamd_logger.debugm(N, rspamd_config, 'registering: %1', {
  209. type = 'virtual',
  210. name = sym,
  211. parent = m['symbol'],
  212. parent_id = id,
  213. group = N
  214. })
  215. rspamd_config:register_symbol({
  216. type = 'virtual',
  217. name = sym,
  218. parent = id,
  219. score = 0.0,
  220. group = N
  221. })
  222. end
  223. end
  224. end
  225. else
  226. for sym in pairs(m['patterns']) do
  227. rspamd_config:register_symbol({
  228. type = 'virtual',
  229. name = sym,
  230. parent = id,
  231. score = 0.0,
  232. group = N
  233. })
  234. end
  235. end
  236. end
  237. if type(m['patterns_fail']) == 'table' then
  238. if m['patterns_fail'][1] then
  239. for _, p in ipairs(m['patterns_fail']) do
  240. if type(p) == 'table' then
  241. for sym in pairs(p) do
  242. rspamd_logger.debugm(N, rspamd_config, 'registering: %1', {
  243. type = 'virtual',
  244. name = sym,
  245. parent = m['symbol'],
  246. parent_id = id,
  247. group = N
  248. })
  249. rspamd_config:register_symbol({
  250. type = 'virtual',
  251. name = sym,
  252. parent = id,
  253. score = 0.0,
  254. group = N
  255. })
  256. end
  257. end
  258. end
  259. else
  260. for sym in pairs(m['patterns_fail']) do
  261. rspamd_config:register_symbol({
  262. type = 'virtual',
  263. name = sym,
  264. parent = id,
  265. score = 0.0,
  266. group = N
  267. })
  268. end
  269. end
  270. end
  271. if m['score'] then
  272. -- Register metric symbol
  273. local description = 'antivirus symbol'
  274. local group = N
  275. if m['description'] then
  276. description = m['description']
  277. end
  278. if m['group'] then
  279. group = m['group']
  280. end
  281. rspamd_config:set_metric_symbol({
  282. name = m['symbol'],
  283. score = m['score'],
  284. description = description,
  285. group = group or 'antivirus'
  286. })
  287. end
  288. end
  289. end
  290. end
  291. if not has_valid then
  292. lua_util.disable_module(N, 'config')
  293. end
  294. end