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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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 kaspersky_se 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. local data = string.gsub(tostring(body), '[\r\n%s]$', '')
  168. local cached
  169. lua_util.debugm(rule.name, task, '%s: got reply data: "%s"',
  170. rule.log_prefix, data)
  171. if data:find('^CLEAN') then
  172. -- Handle CLEAN replies
  173. if data == 'CLEAN' then
  174. cached = 'OK'
  175. if rule['log_clean'] then
  176. rspamd_logger.infox(task, '%s: message or mime_part is clean',
  177. rule.log_prefix)
  178. else
  179. lua_util.debugm(rule.name, task, '%s: message or mime_part is clean',
  180. rule.log_prefix)
  181. end
  182. elseif data == 'CLEAN AND CONTAINS OFFICE MACRO' then
  183. common.yield_result(task, rule, 'File contains macros', 0.0, 'encrypted')
  184. cached = 'MACRO'
  185. else
  186. rspamd_logger.errx(task, '%s: unhandled clean response: %s', rule.log_prefix, data)
  187. common.yield_result(task, rule, 'unhandled response:' .. data, 0.0, 'fail')
  188. end
  189. elseif data == 'SERVER_ERROR' then
  190. rspamd_logger.errx(task, '%s: error: %s', rule.log_prefix, data)
  191. common.yield_result(task, rule, 'error:' .. data,
  192. 0.0, 'fail')
  193. elseif string.match(data, 'DETECT (.+)') then
  194. local vname = string.match(data, 'DETECT (.+)')
  195. common.yield_result(task, rule, vname)
  196. cached = vname
  197. elseif string.match(data, 'NON_SCANNED %((.+)%)') then
  198. local why = string.match(data, 'NON_SCANNED %((.+)%)')
  199. if why == 'PASSWORD PROTECTED' then
  200. rspamd_logger.errx(task, '%s: File is encrypted', rule.log_prefix)
  201. common.yield_result(task, rule, 'File is encrypted: '.. why,
  202. 0.0, 'encrypted')
  203. cached = 'ENCRYPTED'
  204. else
  205. common.yield_result(task, rule, 'unhandled response:' .. data, 0.0, 'fail')
  206. end
  207. else
  208. rspamd_logger.errx(task, '%s: unhandled response: %s', rule.log_prefix, data)
  209. common.yield_result(task, rule, 'unhandled response:' .. data, 0.0, 'fail')
  210. end
  211. if cached then
  212. common.save_cache(task, digest, rule, cached)
  213. end
  214. end
  215. end
  216. request_data.callback = kas_callback
  217. http.request(request_data)
  218. end
  219. if common.condition_check_and_continue(task, content, rule, digest,
  220. kaspersky_se_check_uncached) then
  221. return
  222. else
  223. kaspersky_se_check_uncached()
  224. end
  225. end
  226. return {
  227. type = 'antivirus',
  228. description = 'Kaspersky Scan Engine interface',
  229. configure = kaspersky_se_config,
  230. check = kaspersky_se_check,
  231. name = N
  232. }