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.

common.lua 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. --[[
  2. Copyright (c) 2018, Vsevolod Stakhov <vsevolod@highsecure.ru>
  3. Copyright (c) 2019, Carsten Rosenberg <c.rosenberg@heinlein-support.de>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]]--
  14. --[[[
  15. -- @module lua_scanners_common
  16. -- This module contains common external scanners functions
  17. --]]
  18. local rspamd_logger = require "rspamd_logger"
  19. local rspamd_regexp = require "rspamd_regexp"
  20. local lua_util = require "lua_util"
  21. local lua_redis = require "lua_redis"
  22. local lua_magic_types = require "lua_magic/types"
  23. local fun = require "fun"
  24. local exports = {}
  25. local function log_clean(task, rule, msg)
  26. msg = msg or 'message or mime_part is clean'
  27. if rule.log_clean then
  28. rspamd_logger.infox(task, '%s: %s', rule.log_prefix, msg)
  29. else
  30. lua_util.debugm(rule.name, task, '%s: %s', rule.log_prefix, msg)
  31. end
  32. end
  33. local function match_patterns(default_sym, found, patterns, dyn_weight)
  34. if type(patterns) ~= 'table' then return default_sym, dyn_weight end
  35. if not patterns[1] then
  36. for sym, pat in pairs(patterns) do
  37. if pat:match(found) then
  38. return sym, '1'
  39. end
  40. end
  41. return default_sym, dyn_weight
  42. else
  43. for _, p in ipairs(patterns) do
  44. for sym, pat in pairs(p) do
  45. if pat:match(found) then
  46. return sym, '1'
  47. end
  48. end
  49. end
  50. return default_sym, dyn_weight
  51. end
  52. end
  53. local function yield_result(task, rule, vname, dyn_weight, is_fail, maybe_part)
  54. local all_whitelisted = true
  55. local patterns
  56. local symbol
  57. local threat_table
  58. local threat_info
  59. local flags
  60. if type(vname) == 'string' then
  61. threat_table = {vname}
  62. elseif type(vname) == 'table' then
  63. threat_table = vname
  64. end
  65. -- This should be more generic
  66. if not is_fail then
  67. patterns = rule.patterns
  68. symbol = rule.symbol
  69. threat_info = rule.detection_category .. 'found'
  70. if not dyn_weight then dyn_weight = 1.0 end
  71. elseif is_fail == 'fail' then
  72. patterns = rule.patterns_fail
  73. symbol = rule.symbol_fail
  74. threat_info = "FAILED with error"
  75. dyn_weight = 0.0
  76. elseif is_fail == 'encrypted' then
  77. patterns = rule.patterns
  78. symbol = rule.symbol_encrypted
  79. threat_info = "Scan has returned that input was encrypted"
  80. dyn_weight = 1.0
  81. elseif is_fail == 'macro' then
  82. patterns = rule.patterns
  83. symbol = rule.symbol_macro
  84. threat_info = "Scan has returned that input contains macros"
  85. dyn_weight = 1.0
  86. end
  87. for _, tm in ipairs(threat_table) do
  88. local symname, symscore = match_patterns(symbol, tm, patterns, dyn_weight)
  89. if rule.whitelist and rule.whitelist:get_key(tm) then
  90. rspamd_logger.infox(task, '%s: "%s" is in whitelist', rule.log_prefix, tm)
  91. else
  92. all_whitelisted = false
  93. rspamd_logger.infox(task, '%s: result - %s: "%s - score: %s"',
  94. rule.log_prefix, threat_info, tm, symscore)
  95. if maybe_part and rule.show_attachments and maybe_part:get_filename() then
  96. local fname = maybe_part:get_filename()
  97. task:insert_result(symname, symscore, string.format("%s|%s",
  98. tm, fname))
  99. else
  100. task:insert_result(symname, symscore, tm)
  101. end
  102. end
  103. end
  104. if rule.action and is_fail ~= 'fail' and not all_whitelisted then
  105. threat_table = table.concat(threat_table, '; ')
  106. if rule.action ~= 'reject' then
  107. flags = 'least'
  108. end
  109. task:set_pre_result(rule.action,
  110. lua_util.template(rule.message or 'Rejected', {
  111. SCANNER = rule.name,
  112. VIRUS = threat_table,
  113. }), rule.name, nil, nil, flags)
  114. end
  115. end
  116. local function message_not_too_large(task, content, rule)
  117. local max_size = tonumber(rule.max_size)
  118. if not max_size then return true end
  119. if #content > max_size then
  120. rspamd_logger.infox(task, "skip %s check as it is too large: %s (%s is allowed)",
  121. rule.log_prefix, #content, max_size)
  122. return false
  123. end
  124. return true
  125. end
  126. local function message_not_too_small(task, content, rule)
  127. local min_size = tonumber(rule.min_size)
  128. if not min_size then return true end
  129. if #content < min_size then
  130. rspamd_logger.infox(task, "skip %s check as it is too small: %s (%s is allowed)",
  131. rule.log_prefix, #content, min_size)
  132. return false
  133. end
  134. return true
  135. end
  136. local function message_min_words(task, rule)
  137. if rule.text_part_min_words and tonumber(rule.text_part_min_words) > 0 then
  138. local text_part_above_limit = false
  139. local text_parts = task:get_text_parts()
  140. local filter_func = function(p)
  141. return p:get_words_count() >= tonumber(rule.text_part_min_words)
  142. end
  143. fun.each(function(p)
  144. text_part_above_limit = true
  145. end, fun.filter(filter_func, text_parts))
  146. if not text_part_above_limit then
  147. rspamd_logger.infox(task, '%s: #words in all text parts is below text_part_min_words limit: %s',
  148. rule.log_prefix, rule.text_part_min_words)
  149. end
  150. return text_part_above_limit
  151. else
  152. return true
  153. end
  154. end
  155. local function dynamic_scan(task, rule)
  156. if rule.dynamic_scan then
  157. if rule.action ~= 'reject' then
  158. local metric_result = task:get_metric_score('default')
  159. local metric_action = task:get_metric_action('default')
  160. local has_pre_result = task:has_pre_result()
  161. -- ToDo: needed?
  162. -- Sometimes leads to FPs
  163. --if rule.symbol_type == 'postfilter' and metric_action == 'reject' then
  164. -- rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, "result is already reject")
  165. -- return false
  166. --elseif metric_result[1] > metric_result[2]*2 then
  167. if metric_result[1] > metric_result[2]*2 then
  168. rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, 'score > 2 * reject_level: ' .. metric_result[1])
  169. return false
  170. elseif has_pre_result and metric_action == 'reject' then
  171. rspamd_logger.infox(task, '%s: aborting: %s', rule.log_prefix, 'pre_result reject is set')
  172. return false
  173. else
  174. return true, 'undecided'
  175. end
  176. else
  177. return true, 'dynamic_scan is not possible with config `action=reject;`'
  178. end
  179. else
  180. return true
  181. end
  182. end
  183. local function need_check(task, content, rule, digest, fn, maybe_part)
  184. local uncached = true
  185. local key = digest
  186. local function redis_av_cb(err, data)
  187. if data and type(data) == 'string' then
  188. -- Cached
  189. data = lua_util.str_split(data, '\t')
  190. local threat_string = lua_util.str_split(data[1], '\v')
  191. local score = data[2] or rule.default_score
  192. if threat_string[1] ~= 'OK' then
  193. if threat_string[1] == 'MACRO' then
  194. yield_result(task, rule, 'File contains macros',
  195. 0.0, 'macro', maybe_part)
  196. elseif threat_string[1] == 'ENCRYPTED' then
  197. yield_result(task, rule, 'File is encrypted',
  198. 0.0, 'encrypted', maybe_part)
  199. else
  200. lua_util.debugm(rule.name, task, '%s: got cached threat result for %s: %s - score: %s',
  201. rule.log_prefix, key, threat_string[1], score)
  202. yield_result(task, rule, threat_string, score, false, maybe_part)
  203. end
  204. else
  205. lua_util.debugm(rule.name, task, '%s: got cached negative result for %s: %s',
  206. rule.log_prefix, key, threat_string[1])
  207. end
  208. uncached = false
  209. else
  210. if err then
  211. rspamd_logger.errx(task, 'got error checking cache: %s', err)
  212. end
  213. end
  214. local f_message_not_too_large = message_not_too_large(task, content, rule)
  215. local f_message_not_too_small = message_not_too_small(task, content, rule)
  216. local f_message_min_words = message_min_words(task, rule)
  217. local f_dynamic_scan = dynamic_scan(task, rule)
  218. if uncached and
  219. f_message_not_too_large and
  220. f_message_not_too_small and
  221. f_message_min_words and
  222. f_dynamic_scan then
  223. fn()
  224. end
  225. end
  226. if rule.redis_params and not rule.no_cache then
  227. key = rule.prefix .. key
  228. if lua_redis.redis_make_request(task,
  229. rule.redis_params, -- connect params
  230. key, -- hash key
  231. false, -- is write
  232. redis_av_cb, --callback
  233. 'GET', -- command
  234. {key} -- arguments)
  235. ) then
  236. return true
  237. end
  238. end
  239. return false
  240. end
  241. local function save_cache(task, digest, rule, to_save, dyn_weight, maybe_part)
  242. local key = digest
  243. if not dyn_weight then dyn_weight = 1.0 end
  244. local function redis_set_cb(err)
  245. -- Do nothing
  246. if err then
  247. rspamd_logger.errx(task, 'failed to save %s cache for %s -> "%s": %s',
  248. rule.detection_category, to_save, key, err)
  249. else
  250. lua_util.debugm(rule.name, task, '%s: saved cached result for %s: %s - score %s - ttl %s',
  251. rule.log_prefix, key, to_save, dyn_weight, rule.cache_expire)
  252. end
  253. end
  254. if type(to_save) == 'table' then
  255. to_save = table.concat(to_save, '\v')
  256. end
  257. local value_tbl = {to_save, dyn_weight}
  258. if maybe_part and rule.show_attachments and maybe_part:get_filename() then
  259. local fname = maybe_part:get_filename()
  260. table.insert(value_tbl, fname)
  261. end
  262. local value = table.concat(value_tbl, '\t')
  263. if rule.redis_params and rule.prefix then
  264. key = rule.prefix .. key
  265. lua_redis.redis_make_request(task,
  266. rule.redis_params, -- connect params
  267. key, -- hash key
  268. true, -- is write
  269. redis_set_cb, --callback
  270. 'SETEX', -- command
  271. { key, rule.cache_expire or 0, value }
  272. )
  273. end
  274. return false
  275. end
  276. local function create_regex_table(patterns)
  277. local regex_table = {}
  278. if patterns[1] then
  279. for i, p in ipairs(patterns) do
  280. if type(p) == 'table' then
  281. local new_set = {}
  282. for k, v in pairs(p) do
  283. new_set[k] = rspamd_regexp.create_cached(v)
  284. end
  285. regex_table[i] = new_set
  286. else
  287. regex_table[i] = {}
  288. end
  289. end
  290. else
  291. for k, v in pairs(patterns) do
  292. regex_table[k] = rspamd_regexp.create_cached(v)
  293. end
  294. end
  295. return regex_table
  296. end
  297. local function match_filter(task, rule, found, patterns, pat_type)
  298. if type(patterns) ~= 'table' or not found then
  299. return false
  300. end
  301. if not patterns[1] then
  302. for _, pat in pairs(patterns) do
  303. if pat_type == 'ext' and tostring(pat) == tostring(found) then
  304. return true
  305. elseif pat_type == 'regex' and pat:match(found) then
  306. return true
  307. end
  308. end
  309. return false
  310. else
  311. for _, p in ipairs(patterns) do
  312. for _, pat in ipairs(p) do
  313. if pat_type == 'ext' and tostring(pat) == tostring(found) then
  314. return true
  315. elseif pat_type == 'regex' and pat:match(found) then
  316. return true
  317. end
  318. end
  319. end
  320. return false
  321. end
  322. end
  323. -- borrowed from mime_types.lua
  324. -- ext is the last extension, LOWERCASED
  325. -- ext2 is the one before last extension LOWERCASED
  326. local function gen_extension(fname)
  327. local filename_parts = lua_util.str_split(fname, '.')
  328. local ext = {}
  329. for n = 1, 2 do
  330. ext[n] = #filename_parts > n and string.lower(filename_parts[#filename_parts + 1 - n]) or nil
  331. end
  332. return ext[1],ext[2],filename_parts
  333. end
  334. local function check_parts_match(task, rule)
  335. local filter_func = function(p)
  336. local mtype,msubtype = p:get_type()
  337. local detected_ext = p:get_detected_ext()
  338. local fname = p:get_filename()
  339. local ext, ext2
  340. if rule.scan_all_mime_parts == false then
  341. -- check file extension and filename regex matching
  342. --lua_util.debugm(rule.name, task, '%s: filename: |%s|%s|', rule.log_prefix, fname)
  343. if fname ~= nil then
  344. ext,ext2 = gen_extension(fname)
  345. --lua_util.debugm(rule.name, task, '%s: extension, fname: |%s|%s|%s|', rule.log_prefix, ext, ext2, fname)
  346. if match_filter(task, rule, ext, rule.mime_parts_filter_ext, 'ext')
  347. or match_filter(task, rule, ext2, rule.mime_parts_filter_ext, 'ext') then
  348. lua_util.debugm(rule.name, task, '%s: extension matched: |%s|%s|', rule.log_prefix, ext, ext2)
  349. return true
  350. elseif match_filter(task, rule, fname, rule.mime_parts_filter_regex, 'regex') then
  351. lua_util.debugm(rule.name, task, '%s: filname regex matched', rule.log_prefix)
  352. return true
  353. end
  354. end
  355. -- check content type string regex matching
  356. if mtype ~= nil and msubtype ~= nil then
  357. local ct = string.format('%s/%s', mtype, msubtype):lower()
  358. if match_filter(task, rule, ct, rule.mime_parts_filter_regex, 'regex') then
  359. lua_util.debugm(rule.name, task, '%s: regex content-type: %s', rule.log_prefix, ct)
  360. return true
  361. end
  362. end
  363. -- check detected content type (libmagic) regex matching
  364. if detected_ext then
  365. local magic = lua_magic_types[detected_ext] or {}
  366. if match_filter(task, rule, detected_ext, rule.mime_parts_filter_ext, 'ext') then
  367. lua_util.debugm(rule.name, task, '%s: detected extension matched: |%s|', rule.log_prefix, detected_ext)
  368. return true
  369. elseif magic.ct and match_filter(task, rule, magic.ct, rule.mime_parts_filter_regex, 'regex') then
  370. lua_util.debugm(rule.name, task, '%s: regex detected libmagic content-type: %s',
  371. rule.log_prefix, magic.ct)
  372. return true
  373. end
  374. end
  375. -- check filenames in archives
  376. if p:is_archive() then
  377. local arch = p:get_archive()
  378. local filelist = arch:get_files_full(1000)
  379. for _,f in ipairs(filelist) do
  380. ext,ext2 = gen_extension(f.name)
  381. if match_filter(task, rule, ext, rule.mime_parts_filter_ext, 'ext')
  382. or match_filter(task, rule, ext2, rule.mime_parts_filter_ext, 'ext') then
  383. lua_util.debugm(rule.name, task, '%s: extension matched in archive: |%s|%s|', rule.log_prefix, ext, ext2)
  384. --lua_util.debugm(rule.name, task, '%s: extension matched in archive: %s', rule.log_prefix, ext)
  385. return true
  386. elseif match_filter(task, rule, f.name, rule.mime_parts_filter_regex, 'regex') then
  387. lua_util.debugm(rule.name, task, '%s: filename regex matched in archive', rule.log_prefix)
  388. return true
  389. end
  390. end
  391. end
  392. end
  393. -- check text_part has more words than text_part_min_words_check
  394. if rule.scan_text_mime and rule.text_part_min_words and p:is_text() and
  395. p:get_words_count() >= tonumber(rule.text_part_min_words) then
  396. return true
  397. end
  398. if rule.scan_image_mime and p:is_image() then
  399. return true
  400. end
  401. if rule.scan_all_mime_parts ~= false then
  402. local is_part_checkable = (p:is_attachment() and (not p:is_image() or rule.scan_image_mime))
  403. if detected_ext then
  404. -- We know what to scan!
  405. local magic = lua_magic_types[detected_ext] or {}
  406. if magic.av_check ~= false or is_part_checkable then
  407. return true
  408. end
  409. elseif is_part_checkable then
  410. -- Just rely on attachment property
  411. return true
  412. end
  413. end
  414. return false
  415. end
  416. return fun.filter(filter_func, task:get_parts())
  417. end
  418. local function check_metric_results(task, rule)
  419. if rule.action ~= 'reject' then
  420. local metric_result = task:get_metric_score('default')
  421. local metric_action = task:get_metric_action('default')
  422. local has_pre_result = task:has_pre_result()
  423. if rule.symbol_type == 'postfilter' and metric_action == 'reject' then
  424. return true, 'result is already reject'
  425. elseif metric_result[1] > metric_result[2]*2 then
  426. return true, 'score > 2 * reject_level: ' .. metric_result[1]
  427. elseif has_pre_result and metric_action == 'reject' then
  428. return true, 'pre_result reject is set'
  429. else
  430. return false, 'undecided'
  431. end
  432. else
  433. return false, 'dynamic_scan is not possible with config `action=reject;`'
  434. end
  435. end
  436. exports.log_clean = log_clean
  437. exports.yield_result = yield_result
  438. exports.match_patterns = match_patterns
  439. exports.condition_check_and_continue = need_check
  440. exports.save_cache = save_cache
  441. exports.create_regex_table = create_regex_table
  442. exports.check_parts_match = check_parts_match
  443. exports.check_metric_results = check_metric_results
  444. setmetatable(exports, {
  445. __call = function(t, override)
  446. for k, v in pairs(t) do
  447. if _G[k] ~= nil then
  448. local msg = 'function ' .. k .. ' already exists in global scope.'
  449. if override then
  450. _G[k] = v
  451. print('WARNING: ' .. msg .. ' Overwritten.')
  452. else
  453. print('NOTICE: ' .. msg .. ' Skipped.')
  454. end
  455. else
  456. _G[k] = v
  457. end
  458. end
  459. end,
  460. })
  461. return exports