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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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. 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',
  184. 0.0, 'macro', maybe_part)
  185. cached = 'MACRO'
  186. else
  187. rspamd_logger.errx(task, '%s: unhandled clean response: %s', rule.log_prefix, data)
  188. common.yield_result(task, rule, 'unhandled response:' .. data,
  189. 0.0, 'fail', maybe_part)
  190. end
  191. elseif data == 'SERVER_ERROR' then
  192. rspamd_logger.errx(task, '%s: error: %s', rule.log_prefix, data)
  193. common.yield_result(task, rule, 'error:' .. data,
  194. 0.0, 'fail', maybe_part)
  195. elseif string.match(data, 'DETECT (.+)') then
  196. local vname = string.match(data, 'DETECT (.+)')
  197. common.yield_result(task, rule, vname, 1.0, nil, maybe_part)
  198. cached = vname
  199. elseif string.match(data, 'NON_SCANNED %((.+)%)') then
  200. local why = string.match(data, 'NON_SCANNED %((.+)%)')
  201. if why == 'PASSWORD PROTECTED' then
  202. rspamd_logger.errx(task, '%s: File is encrypted', rule.log_prefix)
  203. common.yield_result(task, rule, 'File is encrypted: '.. why,
  204. 0.0, 'encrypted', maybe_part)
  205. cached = 'ENCRYPTED'
  206. else
  207. common.yield_result(task, rule, 'unhandled response:' .. data,
  208. 0.0, 'fail', maybe_part)
  209. end
  210. else
  211. rspamd_logger.errx(task, '%s: unhandled response: %s', rule.log_prefix, data)
  212. common.yield_result(task, rule, 'unhandled response:' .. data,
  213. 0.0, 'fail', maybe_part)
  214. end
  215. if cached then
  216. common.save_cache(task, digest, rule, cached, 1.0, maybe_part)
  217. end
  218. end
  219. end
  220. request_data.callback = kas_callback
  221. http.request(request_data)
  222. end
  223. if common.condition_check_and_continue(task, content, rule, digest,
  224. kaspersky_se_check_uncached, maybe_part) then
  225. return
  226. else
  227. kaspersky_se_check_uncached()
  228. end
  229. end
  230. return {
  231. type = 'antivirus',
  232. description = 'Kaspersky Scan Engine interface',
  233. configure = kaspersky_se_config,
  234. check = kaspersky_se_check,
  235. name = N
  236. }