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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. --[[[
  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, maybe_part)
  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
  227. upstream:ok()
  228. end
  229. if code ~= 200 then
  230. rspamd_logger.errx(task, 'invalid HTTP code: %s, body: %s, headers: %s', code, body, headers)
  231. task:insert_result(rule.symbol_fail, 1.0, 'Bad HTTP code: ' .. code)
  232. return
  233. end
  234. local parser = ucl.parser()
  235. local ret, err = parser:parse_string(body)
  236. if not ret then
  237. rspamd_logger.errx(task, 'vade: bad response body (raw): %s', body)
  238. task:insert_result(rule.symbol_fail, 1.0, 'Parser error: ' .. err)
  239. return
  240. end
  241. local obj = parser:get_object()
  242. local verdict = obj.verdict
  243. if not verdict then
  244. rspamd_logger.errx(task, 'vade: bad response JSON (no verdict): %s', obj)
  245. task:insert_result(rule.symbol_fail, 1.0, 'No verdict/unknown verdict')
  246. return
  247. end
  248. local vparts = lua_util.str_split(verdict, ":")
  249. verdict = table.remove(vparts, 1) or verdict
  250. local sym = rule.symbols[verdict]
  251. if not sym then
  252. sym = rule.symbols.other
  253. end
  254. if not sym.symbol then
  255. -- Subcategory match
  256. local lvl = 'low'
  257. if vparts and vparts[1] then
  258. lvl = vparts[1]
  259. end
  260. if sym[lvl] then
  261. sym = sym[lvl]
  262. else
  263. sym = rule.symbols.other
  264. end
  265. end
  266. local opts = {}
  267. if obj.score then
  268. table.insert(opts, 'score=' .. obj.score)
  269. end
  270. if obj.elapsed then
  271. table.insert(opts, 'elapsed=' .. obj.elapsed)
  272. end
  273. if rule.log_spamcause and obj.spamcause then
  274. rspamd_logger.infox(task, 'vadesecure verdict="%s", score=%s, spamcause="%s", message-id="%s"',
  275. verdict, obj.score, obj.spamcause, task:get_message_id())
  276. else
  277. lua_util.debugm(rule.name, task, 'vadesecure returned verdict="%s", score=%s, spamcause="%s"',
  278. verdict, obj.score, obj.spamcause)
  279. end
  280. if #vparts > 0 then
  281. table.insert(opts, 'verdict=' .. verdict .. ';' .. table.concat(vparts, ':'))
  282. end
  283. task:insert_result(sym.symbol, 1.0, opts)
  284. end
  285. end
  286. request_data.callback = vade_callback
  287. http.request(request_data)
  288. end
  289. if common.condition_check_and_continue(task, content, rule, digest,
  290. vade_check_uncached, maybe_part) then
  291. return
  292. else
  293. vade_check_uncached()
  294. end
  295. end
  296. return {
  297. type = { 'vadesecure', 'scanner' },
  298. description = 'VadeSecure Filterd interface',
  299. configure = vade_config,
  300. check = vade_check,
  301. name = N
  302. }