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.

savapi.lua 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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 savapi
  15. -- This module contains avira savapi antivirus access functions
  16. --]]
  17. local lua_util = require "lua_util"
  18. local tcp = require "rspamd_tcp"
  19. local upstream_list = require "rspamd_upstream_list"
  20. local rspamd_util = require "rspamd_util"
  21. local rspamd_logger = require "rspamd_logger"
  22. local common = require "lua_scanners/common"
  23. local N = "savapi"
  24. local default_message = '${SCANNER}: virus found: "${VIRUS}"'
  25. local function savapi_config(opts)
  26. local savapi_conf = {
  27. name = N,
  28. scan_mime_parts = true,
  29. scan_text_mime = false,
  30. scan_image_mime = false,
  31. default_port = 4444, -- note: You must set ListenAddress in savapi.conf
  32. product_id = 0,
  33. log_clean = false,
  34. timeout = 15.0, -- FIXME: this will break task_timeout!
  35. retransmits = 1, -- FIXME: useless, for local files
  36. cache_expire = 3600, -- expire redis in one hour
  37. message = default_message,
  38. detection_category = "virus",
  39. tmpdir = '/tmp',
  40. }
  41. savapi_conf = lua_util.override_defaults(savapi_conf, opts)
  42. if not savapi_conf.prefix then
  43. savapi_conf.prefix = 'rs_' .. savapi_conf.name .. '_'
  44. end
  45. if not savapi_conf.log_prefix then
  46. if savapi_conf.name:lower() == savapi_conf.type:lower() then
  47. savapi_conf.log_prefix = savapi_conf.name
  48. else
  49. savapi_conf.log_prefix = savapi_conf.name .. ' (' .. savapi_conf.type .. ')'
  50. end
  51. end
  52. if not savapi_conf['servers'] then
  53. rspamd_logger.errx(rspamd_config, 'no servers defined')
  54. return nil
  55. end
  56. savapi_conf['upstreams'] = upstream_list.create(rspamd_config,
  57. savapi_conf['servers'],
  58. savapi_conf.default_port)
  59. if savapi_conf['upstreams'] then
  60. lua_util.add_debug_alias('antivirus', savapi_conf.name)
  61. return savapi_conf
  62. end
  63. rspamd_logger.errx(rspamd_config, 'cannot parse servers %s',
  64. savapi_conf['servers'])
  65. return nil
  66. end
  67. local function savapi_check(task, content, digest, rule)
  68. local function savapi_check_uncached ()
  69. local upstream = rule.upstreams:get_upstream_round_robin()
  70. local addr = upstream:get_addr()
  71. local retransmits = rule.retransmits
  72. local fname = string.format('%s/%s.tmp',
  73. rule.tmpdir, rspamd_util.random_hex(32))
  74. local message_fd = rspamd_util.create_file(fname)
  75. if not message_fd then
  76. rspamd_logger.errx('cannot store file for savapi scan: %s', fname)
  77. return
  78. end
  79. if type(content) == 'string' then
  80. -- Create rspamd_text
  81. local rspamd_text = require "rspamd_text"
  82. content = rspamd_text.fromstring(content)
  83. end
  84. content:save_in_file(message_fd)
  85. -- Ensure cleanup
  86. task:get_mempool():add_destructor(function()
  87. os.remove(fname)
  88. rspamd_util.close_file(message_fd)
  89. end)
  90. local vnames = {}
  91. -- Forward declaration for recursive calls
  92. local savapi_scan1_cb
  93. local function savapi_fin_cb(err, conn)
  94. local vnames_reordered = {}
  95. -- Swap table
  96. for virus, _ in pairs(vnames) do
  97. table.insert(vnames_reordered, virus)
  98. end
  99. lua_util.debugm(rule.name, task, "%s: number of virus names found %s", rule['type'], #vnames_reordered)
  100. if #vnames_reordered > 0 then
  101. local vname = {}
  102. for _, virus in ipairs(vnames_reordered) do
  103. table.insert(vname, virus)
  104. end
  105. common.yield_result(task, rule, vname)
  106. common.save_cache(task, digest, rule, vname)
  107. end
  108. if conn then
  109. conn:close()
  110. end
  111. end
  112. local function savapi_scan2_cb(err, data, conn)
  113. local result = tostring(data)
  114. lua_util.debugm(rule.name, task, "%s: got reply: %s",
  115. rule.type, result)
  116. -- Terminal response - clean
  117. if string.find(result, '200') or string.find(result, '210') then
  118. if rule['log_clean'] then
  119. rspamd_logger.infox(task, '%s: message or mime_part is clean', rule['type'])
  120. end
  121. common.save_cache(task, digest, rule, 'OK')
  122. conn:add_write(savapi_fin_cb, 'QUIT\n')
  123. -- Terminal response - infected
  124. elseif string.find(result, '319') then
  125. conn:add_write(savapi_fin_cb, 'QUIT\n')
  126. -- Non-terminal response
  127. elseif string.find(result, '310') then
  128. local virus
  129. virus = result:match "310.*<<<%s(.*)%s+;.*;.*"
  130. if not virus then
  131. virus = result:match "310%s(.*)%s+;.*;.*"
  132. if not virus then
  133. rspamd_logger.errx(task, "%s: virus result unparseable: %s",
  134. rule['type'], result)
  135. common.yield_result(task, rule, 'virus result unparseable: ' .. result, 0.0, 'fail')
  136. return
  137. end
  138. end
  139. -- Store unique virus names
  140. vnames[virus] = 1
  141. -- More content is expected
  142. conn:add_write(savapi_scan1_cb, '\n')
  143. end
  144. end
  145. savapi_scan1_cb = function(err, conn)
  146. conn:add_read(savapi_scan2_cb, '\n')
  147. end
  148. -- 100 PRODUCT:xyz
  149. local function savapi_greet2_cb(err, data, conn)
  150. local result = tostring(data)
  151. if string.find(result, '100 PRODUCT') then
  152. lua_util.debugm(rule.name, task, "%s: scanning file: %s",
  153. rule['type'], fname)
  154. conn:add_write(savapi_scan1_cb, { string.format('SCAN %s\n',
  155. fname) })
  156. else
  157. rspamd_logger.errx(task, '%s: invalid product id %s', rule['type'],
  158. rule['product_id'])
  159. common.yield_result(task, rule, 'invalid product id: ' .. result, 0.0, 'fail')
  160. conn:add_write(savapi_fin_cb, 'QUIT\n')
  161. end
  162. end
  163. local function savapi_greet1_cb(err, conn)
  164. conn:add_read(savapi_greet2_cb, '\n')
  165. end
  166. local function savapi_callback_init(err, data, conn)
  167. if err then
  168. -- retry with another upstream until retransmits exceeds
  169. if retransmits > 0 then
  170. retransmits = retransmits - 1
  171. -- Select a different upstream!
  172. upstream = rule.upstreams:get_upstream_round_robin()
  173. addr = upstream:get_addr()
  174. lua_util.debugm(rule.name, task, '%s: error: %s; retry IP: %s; retries left: %s',
  175. rule.log_prefix, err, addr, retransmits)
  176. tcp.request({
  177. task = task,
  178. host = addr:to_string(),
  179. port = addr:get_port(),
  180. upstream = upstream,
  181. timeout = rule['timeout'],
  182. callback = savapi_callback_init,
  183. stop_pattern = { '\n' },
  184. })
  185. else
  186. rspamd_logger.errx(task, '%s [%s]: failed to scan, maximum retransmits exceed', rule['symbol'], rule['type'])
  187. common.yield_result(task, rule, 'failed to scan and retransmits exceed', 0.0, 'fail')
  188. end
  189. else
  190. local result = tostring(data)
  191. -- 100 SAVAPI:4.0 greeting
  192. if string.find(result, '100') then
  193. conn:add_write(savapi_greet1_cb, { string.format('SET PRODUCT %s\n', rule['product_id']) })
  194. end
  195. end
  196. end
  197. tcp.request({
  198. task = task,
  199. host = addr:to_string(),
  200. port = addr:get_port(),
  201. upstream = upstream,
  202. timeout = rule['timeout'],
  203. callback = savapi_callback_init,
  204. stop_pattern = { '\n' },
  205. })
  206. end
  207. if common.condition_check_and_continue(task, content, rule, digest, savapi_check_uncached) then
  208. return
  209. else
  210. savapi_check_uncached()
  211. end
  212. end
  213. return {
  214. type = 'antivirus',
  215. description = 'savapi avira antivirus',
  216. configure = savapi_config,
  217. check = savapi_check,
  218. name = N
  219. }