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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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. keepalive = true,
  44. auth_string = nil
  45. }
  46. default_conf = lua_util.override_defaults(default_conf, opts)
  47. if not default_conf.prefix then
  48. default_conf.prefix = 'rs_' .. default_conf.name .. '_'
  49. end
  50. if not default_conf.log_prefix then
  51. if default_conf.name:lower() == default_conf.type:lower() then
  52. default_conf.log_prefix = default_conf.name
  53. else
  54. default_conf.log_prefix = default_conf.name .. ' (' .. default_conf.type .. ')'
  55. end
  56. end
  57. if not default_conf.servers and default_conf.socket then
  58. default_conf.servers = default_conf.socket
  59. end
  60. if not default_conf.servers then
  61. rspamd_logger.errx(rspamd_config, 'no servers defined')
  62. return nil
  63. end
  64. default_conf.upstreams = upstream_list.create(rspamd_config,
  65. default_conf.servers,
  66. default_conf.default_port)
  67. if default_conf.upstreams then
  68. lua_util.add_debug_alias('external_services', default_conf.name)
  69. return default_conf
  70. end
  71. rspamd_logger.errx(rspamd_config, 'cannot parse servers %s',
  72. default_conf['servers'])
  73. return nil
  74. end
  75. local function kaspersky_se_check(task, content, digest, rule, maybe_part)
  76. local function kaspersky_se_check_uncached()
  77. local function make_url(addr)
  78. local url
  79. local suffix = '/scanmemory'
  80. if rule.use_files then
  81. suffix = '/scanfile'
  82. end
  83. if rule.use_https then
  84. url = string.format('https://%s:%d%s', tostring(addr),
  85. addr:get_port(), suffix)
  86. else
  87. url = string.format('http://%s:%d%s', tostring(addr),
  88. addr:get_port(), suffix)
  89. end
  90. return url
  91. end
  92. local upstream = rule.upstreams:get_upstream_round_robin()
  93. local addr = upstream:get_addr()
  94. local retransmits = rule.retransmits
  95. local url = make_url(addr)
  96. local hdrs = {
  97. ['X-KAV-ProtocolVersion'] = '1',
  98. ['X-KAV-Timeout'] = tostring(rule.timeout * 1000),
  99. }
  100. local ip = task:get_from_ip()
  101. if ip and ip:is_valid() then
  102. hdrs['X-KAV-HostIP'] = tostring(ip)
  103. end
  104. if rule.auth_string then
  105. hdrs['Authorization'] = rule.auth_string
  106. end
  107. if task:has_from() then
  108. hdrs['X-KAV-ObjectURL'] = string.format('[from:%s]', task:get_from()[1].addr)
  109. end
  110. local req_body
  111. if rule.use_files then
  112. local fname = string.format('%s/%s.tmp',
  113. rule.tmpdir, rspamd_util.random_hex(32))
  114. local message_fd = rspamd_util.create_file(fname)
  115. if not message_fd then
  116. rspamd_logger.errx('cannot store file for kaspersky_se scan: %s', fname)
  117. return
  118. end
  119. if type(content) == 'string' then
  120. -- Create rspamd_text
  121. local rspamd_text = require "rspamd_text"
  122. content = rspamd_text.fromstring(content)
  123. end
  124. content:save_in_file(message_fd)
  125. -- Ensure cleanup
  126. task:get_mempool():add_destructor(function()
  127. os.remove(fname)
  128. rspamd_util.close_file(message_fd)
  129. end)
  130. req_body = fname
  131. else
  132. req_body = content
  133. end
  134. local request_data = {
  135. task = task,
  136. url = url,
  137. body = req_body,
  138. headers = hdrs,
  139. timeout = rule.timeout,
  140. keepalive = rule.keepalive,
  141. }
  142. local function kas_callback(http_err, code, body, headers)
  143. local function requery()
  144. -- set current upstream to fail because an error occurred
  145. upstream:fail()
  146. -- retry with another upstream until retransmits exceeds
  147. if retransmits > 0 then
  148. retransmits = retransmits - 1
  149. lua_util.debugm(rule.name, task,
  150. '%s: Request Error: %s - retries left: %s',
  151. rule.log_prefix, http_err, retransmits)
  152. -- Select a different upstream!
  153. upstream = rule.upstreams:get_upstream_round_robin()
  154. addr = upstream:get_addr()
  155. url = make_url(addr)
  156. lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s',
  157. rule.log_prefix, addr, addr:get_port())
  158. request_data.url = url
  159. request_data.upstream = upstream
  160. http.request(request_data)
  161. else
  162. rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits ' ..
  163. 'exceed', rule.log_prefix)
  164. task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan and ' ..
  165. 'retransmits exceed')
  166. end
  167. end
  168. if http_err then
  169. requery()
  170. else
  171. -- Parse the response
  172. if upstream then
  173. upstream:ok()
  174. end
  175. if code ~= 200 then
  176. rspamd_logger.errx(task, 'invalid HTTP code: %s, body: %s, headers: %s', code, body, headers)
  177. task:insert_result(rule.symbol_fail, 1.0, 'Bad HTTP code: ' .. code)
  178. return
  179. end
  180. local data = string.gsub(tostring(body), '[\r\n%s]$', '')
  181. local cached
  182. lua_util.debugm(rule.name, task, '%s: got reply data: "%s"',
  183. rule.log_prefix, data)
  184. if data:find('^CLEAN') then
  185. -- Handle CLEAN replies
  186. if data == 'CLEAN' then
  187. cached = 'OK'
  188. if rule['log_clean'] then
  189. rspamd_logger.infox(task, '%s: message or mime_part is clean',
  190. rule.log_prefix)
  191. else
  192. lua_util.debugm(rule.name, task, '%s: message or mime_part is clean',
  193. rule.log_prefix)
  194. end
  195. elseif data == 'CLEAN AND CONTAINS OFFICE MACRO' then
  196. common.yield_result(task, rule, 'File contains macros',
  197. 0.0, 'macro', maybe_part)
  198. cached = 'MACRO'
  199. else
  200. rspamd_logger.errx(task, '%s: unhandled clean response: %s', rule.log_prefix, data)
  201. common.yield_result(task, rule, 'unhandled response:' .. data,
  202. 0.0, 'fail', maybe_part)
  203. end
  204. elseif data == 'SERVER_ERROR' then
  205. rspamd_logger.errx(task, '%s: error: %s', rule.log_prefix, data)
  206. common.yield_result(task, rule, 'error:' .. data,
  207. 0.0, 'fail', maybe_part)
  208. elseif string.match(data, 'DETECT (.+)') then
  209. local vname = string.match(data, 'DETECT (.+)')
  210. common.yield_result(task, rule, vname, 1.0, nil, maybe_part)
  211. cached = vname
  212. elseif string.match(data, 'NON_SCANNED %((.+)%)') then
  213. local why = string.match(data, 'NON_SCANNED %((.+)%)')
  214. if why == 'PASSWORD PROTECTED' then
  215. rspamd_logger.errx(task, '%s: File is encrypted', rule.log_prefix)
  216. common.yield_result(task, rule, 'File is encrypted: ' .. why,
  217. 0.0, 'encrypted', maybe_part)
  218. cached = 'ENCRYPTED'
  219. else
  220. common.yield_result(task, rule, 'unhandled response:' .. data,
  221. 0.0, 'fail', maybe_part)
  222. end
  223. else
  224. rspamd_logger.errx(task, '%s: unhandled response: %s', rule.log_prefix, data)
  225. common.yield_result(task, rule, 'unhandled response:' .. data,
  226. 0.0, 'fail', maybe_part)
  227. end
  228. if cached then
  229. common.save_cache(task, digest, rule, cached, 1.0, maybe_part)
  230. end
  231. end
  232. end
  233. request_data.callback = kas_callback
  234. http.request(request_data)
  235. end
  236. if common.condition_check_and_continue(task, content, rule, digest,
  237. kaspersky_se_check_uncached, maybe_part) then
  238. return
  239. else
  240. kaspersky_se_check_uncached()
  241. end
  242. end
  243. return {
  244. type = 'antivirus',
  245. description = 'Kaspersky Scan Engine interface',
  246. configure = kaspersky_se_config,
  247. check = kaspersky_se_check,
  248. name = N
  249. }