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.

clamav.lua 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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 clamav
  15. -- This module contains clamav 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 = "clamav"
  24. local default_message = '${SCANNER}: virus found: "${VIRUS}"'
  25. local function clamav_config(opts)
  26. local clamav_conf = {
  27. name = N,
  28. scan_mime_parts = true,
  29. scan_text_mime = false,
  30. scan_image_mime = false,
  31. default_port = 3310,
  32. log_clean = false,
  33. timeout = 5.0, -- FIXME: this will break task_timeout!
  34. detection_category = "virus",
  35. retransmits = 2,
  36. cache_expire = 3600, -- expire redis in one hour
  37. message = default_message,
  38. }
  39. clamav_conf = lua_util.override_defaults(clamav_conf, opts)
  40. if not clamav_conf.prefix then
  41. clamav_conf.prefix = 'rs_' .. clamav_conf.name .. '_'
  42. end
  43. if not clamav_conf.log_prefix then
  44. if clamav_conf.name:lower() == clamav_conf.type:lower() then
  45. clamav_conf.log_prefix = clamav_conf.name
  46. else
  47. clamav_conf.log_prefix = clamav_conf.name .. ' (' .. clamav_conf.type .. ')'
  48. end
  49. end
  50. if not clamav_conf['servers'] then
  51. rspamd_logger.errx(rspamd_config, 'no servers defined')
  52. return nil
  53. end
  54. clamav_conf['upstreams'] = upstream_list.create(rspamd_config,
  55. clamav_conf['servers'],
  56. clamav_conf.default_port)
  57. if clamav_conf['upstreams'] then
  58. lua_util.add_debug_alias('antivirus', clamav_conf.name)
  59. return clamav_conf
  60. end
  61. rspamd_logger.errx(rspamd_config, 'cannot parse servers %s',
  62. clamav_conf['servers'])
  63. return nil
  64. end
  65. local function clamav_check(task, content, digest, rule, maybe_part)
  66. local function clamav_check_uncached ()
  67. local upstream = rule.upstreams:get_upstream_round_robin()
  68. local addr = upstream:get_addr()
  69. local retransmits = rule.retransmits
  70. local header = rspamd_util.pack("c9 c1 >I4", "zINSTREAM", "\0",
  71. #content)
  72. local footer = rspamd_util.pack(">I4", 0)
  73. local function clamav_callback(err, data)
  74. if err then
  75. -- retry with another upstream until retransmits exceeds
  76. if retransmits > 0 then
  77. retransmits = retransmits - 1
  78. -- Select a different upstream!
  79. upstream = rule.upstreams:get_upstream_round_robin()
  80. addr = upstream:get_addr()
  81. lua_util.debugm(rule.name, task, '%s: error: %s; retry IP: %s; retries left: %s',
  82. rule.log_prefix, err, addr, retransmits)
  83. tcp.request({
  84. task = task,
  85. host = addr:to_string(),
  86. port = addr:get_port(),
  87. upstream = upstream,
  88. timeout = rule['timeout'],
  89. callback = clamav_callback,
  90. data = { header, content, footer },
  91. stop_pattern = '\0'
  92. })
  93. else
  94. rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits exceed', rule.log_prefix)
  95. common.yield_result(task, rule,
  96. 'failed to scan and retransmits exceed', 0.0, 'fail',
  97. maybe_part)
  98. end
  99. else
  100. data = tostring(data)
  101. local cached
  102. lua_util.debugm(rule.name, task, '%s: got reply: %s',
  103. rule.log_prefix, data)
  104. if data == 'stream: OK' then
  105. cached = 'OK'
  106. if rule['log_clean'] then
  107. rspamd_logger.infox(task, '%s: message or mime_part is clean',
  108. rule.log_prefix)
  109. else
  110. lua_util.debugm(rule.name, task, '%s: message or mime_part is clean', rule.log_prefix)
  111. end
  112. else
  113. local vname = string.match(data, 'stream: (.+) FOUND')
  114. if string.find(vname, '^Heuristics%.Encrypted') then
  115. rspamd_logger.errx(task, '%s: File is encrypted', rule.log_prefix)
  116. common.yield_result(task, rule, 'File is encrypted: ' .. vname,
  117. 0.0, 'encrypted', maybe_part)
  118. cached = 'ENCRYPTED'
  119. elseif string.find(vname, '^Heuristics%.OLE2%.ContainsMacros') then
  120. rspamd_logger.errx(task, '%s: ClamAV Found an OLE2 Office Macro', rule.log_prefix)
  121. common.yield_result(task, rule, vname, 0.0, 'macro', maybe_part)
  122. cached = 'MACRO'
  123. elseif string.find(vname, '^Heuristics%.Limits%.Exceeded') then
  124. rspamd_logger.errx(task, '%s: ClamAV Limits Exceeded', rule.log_prefix)
  125. common.yield_result(task, rule, 'Limits Exceeded: ' .. vname, 0.0,
  126. 'fail', maybe_part)
  127. elseif vname then
  128. common.yield_result(task, rule, vname, 1.0, nil, maybe_part)
  129. cached = vname
  130. else
  131. rspamd_logger.errx(task, '%s: unhandled response: %s', rule.log_prefix, data)
  132. common.yield_result(task, rule, 'unhandled response:' .. vname, 0.0,
  133. 'fail', maybe_part)
  134. end
  135. end
  136. if cached then
  137. common.save_cache(task, digest, rule, cached, 1.0, maybe_part)
  138. end
  139. end
  140. end
  141. tcp.request({
  142. task = task,
  143. host = addr:to_string(),
  144. port = addr:get_port(),
  145. timeout = rule['timeout'],
  146. callback = clamav_callback,
  147. upstream = upstream,
  148. data = { header, content, footer },
  149. stop_pattern = '\0'
  150. })
  151. end
  152. if common.condition_check_and_continue(task, content, rule, digest,
  153. clamav_check_uncached, maybe_part) then
  154. return
  155. else
  156. clamav_check_uncached()
  157. end
  158. end
  159. return {
  160. type = 'antivirus',
  161. description = 'clamav antivirus',
  162. configure = clamav_config,
  163. check = clamav_check,
  164. name = N
  165. }