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.

vadesecure.lua 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. --[[
  2. Copyright (c) 2019, Vsevolod Stakhov <vsevolod@highsecure.ru>
  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. --[[[
  14. -- @module vadesecure
  15. -- This module contains Vadesecure Filterd interface
  16. --]]
  17. local lua_util = require "lua_util"
  18. local http = require "rspamd_http"
  19. local upstream_list = require "rspamd_upstream_list"
  20. local rspamd_logger = require "rspamd_logger"
  21. local ucl = require "ucl"
  22. local common = require "lua_scanners/common"
  23. local N = 'vadesecure'
  24. local function vade_config(opts)
  25. local vade_conf = {
  26. name = N,
  27. default_port = 23808,
  28. url = '/api/v1/scan',
  29. use_https = false,
  30. timeout = 5.0,
  31. log_clean = false,
  32. retransmits = 1,
  33. cache_expire = 7200, -- expire redis in 2h
  34. message = '${SCANNER}: spam message found: "${VIRUS}"',
  35. detection_category = "hash",
  36. default_score = 1,
  37. action = false,
  38. log_spamcause = true,
  39. symbol_fail = 'VADE_FAIL',
  40. symbol = 'VADE_CHECK',
  41. settings_outbound = nil, -- Set when there is a settings id for outbound messages
  42. symbols = {
  43. clean = {
  44. symbol = 'VADE_CLEAN',
  45. score = -0.5,
  46. description = 'VadeSecure decided message to be clean'
  47. },
  48. spam = {
  49. high = {
  50. symbol = 'VADE_SPAM_HIGH',
  51. score = 8.0,
  52. description = 'VadeSecure decided message to be clearly spam'
  53. },
  54. medium = {
  55. symbol = 'VADE_SPAM_MEDIUM',
  56. score = 5.0,
  57. description = 'VadeSecure decided message to be highly likely spam'
  58. },
  59. low = {
  60. symbol = 'VADE_SPAM_LOW',
  61. score = 2.0,
  62. description = 'VadeSecure decided message to be likely spam'
  63. },
  64. },
  65. malware = {
  66. symbol = 'VADE_MALWARE',
  67. score = 8.0,
  68. description = 'VadeSecure decided message to be malware'
  69. },
  70. scam = {
  71. symbol = 'VADE_SCAM',
  72. score = 7.0,
  73. description = 'VadeSecure decided message to be scam'
  74. },
  75. phishing = {
  76. symbol = 'VADE_PHISHING',
  77. score = 8.0,
  78. description = 'VadeSecure decided message to be phishing'
  79. },
  80. commercial = {
  81. symbol = 'VADE_COMMERCIAL',
  82. score = 0.0,
  83. description = 'VadeSecure decided message to be commercial message'
  84. },
  85. community = {
  86. symbol = 'VADE_COMMUNITY',
  87. score = 0.0,
  88. description = 'VadeSecure decided message to be community message'
  89. },
  90. transactional = {
  91. symbol = 'VADE_TRANSACTIONAL',
  92. score = 0.0,
  93. description = 'VadeSecure decided message to be transactional message'
  94. },
  95. suspect = {
  96. symbol = 'VADE_SUSPECT',
  97. score = 3.0,
  98. description = 'VadeSecure decided message to be suspicious message'
  99. },
  100. bounce = {
  101. symbol = 'VADE_BOUNCE',
  102. score = 0.0,
  103. description = 'VadeSecure decided message to be bounce message'
  104. },
  105. other = 'VADE_OTHER',
  106. }
  107. }
  108. vade_conf = lua_util.override_defaults(vade_conf, opts)
  109. if not vade_conf.prefix then
  110. vade_conf.prefix = 'rs_' .. vade_conf.name .. '_'
  111. end
  112. if not vade_conf.log_prefix then
  113. if vade_conf.name:lower() == vade_conf.type:lower() then
  114. vade_conf.log_prefix = vade_conf.name
  115. else
  116. vade_conf.log_prefix = vade_conf.name .. ' (' .. vade_conf.type .. ')'
  117. end
  118. end
  119. if not vade_conf.servers and vade_conf.socket then
  120. vade_conf.servers = vade_conf.socket
  121. end
  122. if not vade_conf.servers then
  123. rspamd_logger.errx(rspamd_config, 'no servers defined')
  124. return nil
  125. end
  126. vade_conf.upstreams = upstream_list.create(rspamd_config,
  127. vade_conf.servers,
  128. vade_conf.default_port)
  129. if vade_conf.upstreams then
  130. lua_util.add_debug_alias('external_services', vade_conf.name)
  131. return vade_conf
  132. end
  133. rspamd_logger.errx(rspamd_config, 'cannot parse servers %s',
  134. vade_conf['servers'])
  135. return nil
  136. end
  137. local function vade_check(task, content, digest, rule)
  138. local function vade_check_uncached()
  139. local function vade_url(addr)
  140. local url
  141. if rule.use_https then
  142. url = string.format('https://%s:%d%s', tostring(addr),
  143. rule.default_port, rule.url)
  144. else
  145. url = string.format('http://%s:%d%s', tostring(addr),
  146. rule.default_port, rule.url)
  147. end
  148. return url
  149. end
  150. local upstream = rule.upstreams:get_upstream_round_robin()
  151. local addr = upstream:get_addr()
  152. local retransmits = rule.retransmits
  153. local url = vade_url(addr)
  154. local hdrs = {}
  155. local helo = task:get_helo()
  156. if helo then
  157. hdrs['X-Helo'] = helo
  158. end
  159. local mail_from = task:get_from('smtp') or {}
  160. if mail_from[1] and #mail_from[1].addr > 1 then
  161. hdrs['X-Mailfrom'] = mail_from[1].addr
  162. end
  163. local rcpt_to = task:get_recipients('smtp')
  164. if rcpt_to then
  165. hdrs['X-Rcptto'] = {}
  166. for _, r in ipairs(rcpt_to) do
  167. table.insert(hdrs['X-Rcptto'], r.addr)
  168. end
  169. end
  170. local fip = task:get_from_ip()
  171. if fip and fip:is_valid() then
  172. hdrs['X-Inet'] = tostring(fip)
  173. end
  174. if rule.settings_outbound then
  175. local settings_id = task:get_settings_id()
  176. if settings_id then
  177. local lua_settings = require "lua_settings"
  178. -- Convert to string
  179. settings_id = lua_settings.settings_by_id(settings_id)
  180. if settings_id then
  181. settings_id = settings_id.name or ''
  182. if settings_id == rule.settings_outbound then
  183. lua_util.debugm(rule.name, task, '%s settings has matched outbound',
  184. settings_id)
  185. hdrs['X-Params'] = 'mode=smtpout'
  186. end
  187. end
  188. end
  189. end
  190. local request_data = {
  191. task = task,
  192. url = url,
  193. body = task:get_content(),
  194. headers = hdrs,
  195. timeout = rule.timeout,
  196. }
  197. local function vade_callback(http_err, code, body, headers)
  198. local function vade_requery()
  199. -- set current upstream to fail because an error occurred
  200. upstream:fail()
  201. -- retry with another upstream until retransmits exceeds
  202. if retransmits > 0 then
  203. retransmits = retransmits - 1
  204. lua_util.debugm(rule.name, task,
  205. '%s: Request Error: %s - retries left: %s',
  206. rule.log_prefix, http_err, retransmits)
  207. -- Select a different upstream!
  208. upstream = rule.upstreams:get_upstream_round_robin()
  209. addr = upstream:get_addr()
  210. url = vade_url(addr)
  211. lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s',
  212. rule.log_prefix, addr, addr:get_port())
  213. request_data.url = url
  214. http.request(request_data)
  215. else
  216. rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits '..
  217. 'exceed', rule.log_prefix)
  218. task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan and '..
  219. 'retransmits exceed')
  220. end
  221. end
  222. if http_err then
  223. vade_requery()
  224. else
  225. -- Parse the response
  226. if upstream then upstream:ok() end
  227. if code ~= 200 then
  228. rspamd_logger.errx(task, 'invalid HTTP code: %s, body: %s, headers: %s', code, body, headers)
  229. task:insert_result(rule.symbol_fail, 1.0, 'Bad HTTP code: ' .. code)
  230. return
  231. end
  232. local parser = ucl.parser()
  233. local ret, err = parser:parse_string(body)
  234. if not ret then
  235. rspamd_logger.errx(task, 'vade: bad response body (raw): %s', body)
  236. task:insert_result(rule.symbol_fail, 1.0, 'Parser error: ' .. err)
  237. return
  238. end
  239. local obj = parser:get_object()
  240. local verdict = obj.verdict
  241. if not verdict then
  242. rspamd_logger.errx(task, 'vade: bad response JSON (no verdict): %s', obj)
  243. task:insert_result(rule.symbol_fail, 1.0, 'No verdict/unknown verdict')
  244. return
  245. end
  246. local vparts = lua_util.str_split(verdict, ":")
  247. verdict = table.remove(vparts, 1) or verdict
  248. local sym = rule.symbols[verdict]
  249. if not sym then
  250. sym = rule.symbols.other
  251. end
  252. if not sym.symbol then
  253. -- Subcategory match
  254. local lvl = 'low'
  255. if vparts and vparts[1] then
  256. lvl = vparts[1]
  257. end
  258. if sym[lvl] then
  259. sym = sym[lvl]
  260. else
  261. sym = rule.symbols.other
  262. end
  263. end
  264. local opts = {}
  265. if obj.score then
  266. table.insert(opts, 'score=' .. obj.score)
  267. end
  268. if obj.elapsed then
  269. table.insert(opts, 'elapsed=' .. obj.elapsed)
  270. end
  271. if rule.log_spamcause and obj.spamcause then
  272. rspamd_logger.infox(task, 'vadesecure verdict="%s", score=%s, spamcause="%s", message-id="%s"',
  273. verdict, obj.score, obj.spamcause, task:get_message_id())
  274. else
  275. lua_util.debugm(rule.name, task, 'vadesecure returned verdict="%s", score=%s, spamcause="%s"',
  276. verdict, obj.score, obj.spamcause)
  277. end
  278. if #vparts > 0 then
  279. table.insert(opts, 'verdict=' .. verdict .. ';' .. table.concat(vparts, ':'))
  280. end
  281. task:insert_result(sym.symbol, 1.0, opts)
  282. end
  283. end
  284. request_data.callback = vade_callback
  285. http.request(request_data)
  286. end
  287. if common.condition_check_and_continue(task, content, rule, digest, vade_check_uncached) then
  288. return
  289. else
  290. vade_check_uncached()
  291. end
  292. end
  293. return {
  294. type = {'vadesecure', 'scanner'},
  295. description = 'VadeSecure Filterd interface',
  296. configure = vade_config,
  297. check = vade_check,
  298. name = N
  299. }