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.

kaspersky_se.lua 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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 kaspersky_se
  15. -- This module contains Kaspersky Scan Engine integration support
  16. -- https://www.kaspersky.com/scan-engine
  17. --]]
  18. local lua_util = require "lua_util"
  19. local rspamd_util = require "rspamd_util"
  20. local http = require "rspamd_http"
  21. local upstream_list = require "rspamd_upstream_list"
  22. local rspamd_logger = require "rspamd_logger"
  23. local common = require "lua_scanners/common"
  24. local N = 'kaspersky_se'
  25. local function kaspersky_se_config(opts)
  26. local default_conf = {
  27. name = N,
  28. default_port = 9999,
  29. use_https = false,
  30. use_files = false,
  31. timeout = 5.0,
  32. log_clean = false,
  33. tmpdir = '/tmp',
  34. retransmits = 1,
  35. cache_expire = 7200, -- expire redis in 2h
  36. message = '${SCANNER}: spam message found: "${VIRUS}"',
  37. detection_category = "virus",
  38. default_score = 1,
  39. action = false,
  40. scan_mime_parts = true,
  41. scan_text_mime = false,
  42. scan_image_mime = false,
  43. }
  44. default_conf = lua_util.override_defaults(default_conf, opts)
  45. if not default_conf.prefix then
  46. default_conf.prefix = 'rs_' .. default_conf.name .. '_'
  47. end
  48. if not default_conf.log_prefix then
  49. if default_conf.name:lower() == default_conf.type:lower() then
  50. default_conf.log_prefix = default_conf.name
  51. else
  52. default_conf.log_prefix = default_conf.name .. ' (' .. default_conf.type .. ')'
  53. end
  54. end
  55. if not default_conf.servers and default_conf.socket then
  56. default_conf.servers = default_conf.socket
  57. end
  58. if not default_conf.servers then
  59. rspamd_logger.errx(rspamd_config, 'no servers defined')
  60. return nil
  61. end
  62. default_conf.upstreams = upstream_list.create(rspamd_config,
  63. default_conf.servers,
  64. default_conf.default_port)
  65. if default_conf.upstreams then
  66. lua_util.add_debug_alias('external_services', default_conf.name)
  67. return default_conf
  68. end
  69. rspamd_logger.errx(rspamd_config, 'cannot parse servers %s',
  70. default_conf['servers'])
  71. return nil
  72. end
  73. local function kaspersky_se_check(task, content, digest, rule)
  74. local function kaspersky_se_check_uncached()
  75. local function make_url(addr)
  76. local url
  77. local suffix = '/scanmemory'
  78. if rule.use_files then
  79. suffix = '/scanfile'
  80. end
  81. if rule.use_https then
  82. url = string.format('https://%s:%d%s', tostring(addr),
  83. addr:get_port(), suffix)
  84. else
  85. url = string.format('http://%s:%d%s', tostring(addr),
  86. addr:get_port(), suffix)
  87. end
  88. return url
  89. end
  90. local upstream = rule.upstreams:get_upstream_round_robin()
  91. local addr = upstream:get_addr()
  92. local retransmits = rule.retransmits
  93. local url = make_url(addr)
  94. local hdrs = {
  95. ['X-KAV-ProtocolVersion'] = '1',
  96. ['X-KAV-Timeout'] = tostring(rule.timeout * 1000),
  97. }
  98. if task:has_from() then
  99. hdrs['X-KAV-ObjectURL'] = string.format('[from:%s]', task:get_from()[1].addr)
  100. end
  101. local req_body
  102. if rule.use_files then
  103. local fname = string.format('%s/%s.tmp',
  104. rule.tmpdir, rspamd_util.random_hex(32))
  105. local message_fd = rspamd_util.create_file(fname)
  106. if not message_fd then
  107. rspamd_logger.errx('cannot store file for savapi scan: %s', fname)
  108. return
  109. end
  110. if type(content) == 'string' then
  111. -- Create rspamd_text
  112. local rspamd_text = require "rspamd_text"
  113. content = rspamd_text.fromstring(content)
  114. end
  115. content:save_in_file(message_fd)
  116. -- Ensure cleanup
  117. task:get_mempool():add_destructor(function()
  118. os.remove(fname)
  119. rspamd_util.close_file(message_fd)
  120. end)
  121. req_body = fname
  122. else
  123. req_body = content
  124. end
  125. local request_data = {
  126. task = task,
  127. url = url,
  128. body = req_body,
  129. headers = hdrs,
  130. timeout = rule.timeout,
  131. }
  132. local function kas_callback(http_err, code, body, headers)
  133. local function requery()
  134. -- set current upstream to fail because an error occurred
  135. upstream:fail()
  136. -- retry with another upstream until retransmits exceeds
  137. if retransmits > 0 then
  138. retransmits = retransmits - 1
  139. lua_util.debugm(rule.name, task,
  140. '%s: Request Error: %s - retries left: %s',
  141. rule.log_prefix, http_err, retransmits)
  142. -- Select a different upstream!
  143. upstream = rule.upstreams:get_upstream_round_robin()
  144. addr = upstream:get_addr()
  145. url = make_url(addr)
  146. lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s',
  147. rule.log_prefix, addr, addr:get_port())
  148. request_data.url = url
  149. http.request(request_data)
  150. else
  151. rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits '..
  152. 'exceed', rule.log_prefix)
  153. task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan and '..
  154. 'retransmits exceed')
  155. end
  156. end
  157. if http_err then
  158. requery()
  159. else
  160. -- Parse the response
  161. if upstream then upstream:ok() end
  162. if code ~= 200 then
  163. rspamd_logger.errx(task, 'invalid HTTP code: %s, body: %s, headers: %s', code, body, headers)
  164. task:insert_result(rule.symbol_fail, 1.0, 'Bad HTTP code: ' .. code)
  165. return
  166. end
  167. lua_util.debugm(rule.name, task, 'got reply: %s', body)
  168. local data = tostring(body)
  169. local cached
  170. lua_util.debugm(rule.name, task, '%s: got reply: %s',
  171. rule.log_prefix, data)
  172. if data == 'CLEAN' then
  173. cached = 'CLEAN'
  174. if rule['log_clean'] then
  175. rspamd_logger.infox(task, '%s: message or mime_part is clean',
  176. rule.log_prefix)
  177. else
  178. lua_util.debugm(rule.name, task, '%s: message or mime_part is clean',
  179. rule.log_prefix)
  180. end
  181. elseif data == 'SERVER_ERROR' then
  182. rspamd_logger.errx(task, '%s: error: %s', rule.log_prefix, data)
  183. common.yield_result(task, rule, 'error:' .. data,
  184. 0.0, 'fail')
  185. elseif string.match(data, 'DETECT (.+)') then
  186. local vname = string.match(data, 'DETECT (.+)')
  187. common.yield_result(task, rule, vname)
  188. cached = vname
  189. elseif string.match(data, 'NON_SCANNED %((.+)%)') then
  190. local why = string.match(data, 'NON_SCANNED %((.+)%)')
  191. if why == 'PASSWORD PROTECTED' then
  192. rspamd_logger.errx(task, '%s: File is encrypted', rule.log_prefix)
  193. common.yield_result(task, rule, 'File is encrypted: '.. why,
  194. 0.0, 'encrypted')
  195. else
  196. common.yield_result(task, rule, 'unhandled response:' .. data, 0.0, 'fail')
  197. end
  198. else
  199. rspamd_logger.errx(task, '%s: unhandled response: %s', rule.log_prefix, data)
  200. common.yield_result(task, rule, 'unhandled response:' .. data, 0.0, 'fail')
  201. end
  202. if cached then
  203. common.save_cache(task, digest, rule, cached)
  204. end
  205. end
  206. end
  207. request_data.callback = kas_callback
  208. http.request(request_data)
  209. end
  210. if common.need_check(task, content, rule, digest, kaspersky_se_check_uncached) then
  211. return
  212. else
  213. kaspersky_se_check_uncached()
  214. end
  215. end
  216. return {
  217. type = 'antivirus',
  218. description = 'Kaspersky Scan Engine interface',
  219. configure = kaspersky_se_config,
  220. check = kaspersky_se_check,
  221. name = N
  222. }