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.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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 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, maybe_part)
  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. request_data.upstream = upstream
  150. http.request(request_data)
  151. else
  152. rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits ' ..
  153. 'exceed', rule.log_prefix)
  154. task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan and ' ..
  155. 'retransmits exceed')
  156. end
  157. end
  158. if http_err then
  159. requery()
  160. else
  161. -- Parse the response
  162. if upstream then
  163. upstream:ok()
  164. end
  165. if code ~= 200 then
  166. rspamd_logger.errx(task, 'invalid HTTP code: %s, body: %s, headers: %s', code, body, headers)
  167. task:insert_result(rule.symbol_fail, 1.0, 'Bad HTTP code: ' .. code)
  168. return
  169. end
  170. local data = string.gsub(tostring(body), '[\r\n%s]$', '')
  171. local cached
  172. lua_util.debugm(rule.name, task, '%s: got reply data: "%s"',
  173. rule.log_prefix, data)
  174. if data:find('^CLEAN') then
  175. -- Handle CLEAN replies
  176. if data == 'CLEAN' then
  177. cached = 'OK'
  178. if rule['log_clean'] then
  179. rspamd_logger.infox(task, '%s: message or mime_part is clean',
  180. rule.log_prefix)
  181. else
  182. lua_util.debugm(rule.name, task, '%s: message or mime_part is clean',
  183. rule.log_prefix)
  184. end
  185. elseif data == 'CLEAN AND CONTAINS OFFICE MACRO' then
  186. common.yield_result(task, rule, 'File contains macros',
  187. 0.0, 'macro', maybe_part)
  188. cached = 'MACRO'
  189. else
  190. rspamd_logger.errx(task, '%s: unhandled clean response: %s', rule.log_prefix, data)
  191. common.yield_result(task, rule, 'unhandled response:' .. data,
  192. 0.0, 'fail', maybe_part)
  193. end
  194. elseif data == 'SERVER_ERROR' then
  195. rspamd_logger.errx(task, '%s: error: %s', rule.log_prefix, data)
  196. common.yield_result(task, rule, 'error:' .. data,
  197. 0.0, 'fail', maybe_part)
  198. elseif string.match(data, 'DETECT (.+)') then
  199. local vname = string.match(data, 'DETECT (.+)')
  200. common.yield_result(task, rule, vname, 1.0, nil, maybe_part)
  201. cached = vname
  202. elseif string.match(data, 'NON_SCANNED %((.+)%)') then
  203. local why = string.match(data, 'NON_SCANNED %((.+)%)')
  204. if why == 'PASSWORD PROTECTED' then
  205. rspamd_logger.errx(task, '%s: File is encrypted', rule.log_prefix)
  206. common.yield_result(task, rule, 'File is encrypted: ' .. why,
  207. 0.0, 'encrypted', maybe_part)
  208. cached = 'ENCRYPTED'
  209. else
  210. common.yield_result(task, rule, 'unhandled response:' .. data,
  211. 0.0, 'fail', maybe_part)
  212. end
  213. else
  214. rspamd_logger.errx(task, '%s: unhandled response: %s', rule.log_prefix, data)
  215. common.yield_result(task, rule, 'unhandled response:' .. data,
  216. 0.0, 'fail', maybe_part)
  217. end
  218. if cached then
  219. common.save_cache(task, digest, rule, cached, 1.0, maybe_part)
  220. end
  221. end
  222. end
  223. request_data.callback = kas_callback
  224. http.request(request_data)
  225. end
  226. if common.condition_check_and_continue(task, content, rule, digest,
  227. kaspersky_se_check_uncached, maybe_part) then
  228. return
  229. else
  230. kaspersky_se_check_uncached()
  231. end
  232. end
  233. return {
  234. type = 'antivirus',
  235. description = 'Kaspersky Scan Engine interface',
  236. configure = kaspersky_se_config,
  237. check = kaspersky_se_check,
  238. name = N
  239. }