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.

whitelist.lua 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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. if confighelp then
  14. return
  15. end
  16. local rspamd_logger = require "rspamd_logger"
  17. local rspamd_util = require "rspamd_util"
  18. local fun = require "fun"
  19. local lua_util = require "lua_util"
  20. local N = "whitelist"
  21. local options = {
  22. dmarc_allow_symbol = 'DMARC_POLICY_ALLOW',
  23. spf_allow_symbol = 'R_SPF_ALLOW',
  24. dkim_allow_symbol = 'R_DKIM_ALLOW',
  25. check_local = false,
  26. check_authed = false,
  27. rules = {}
  28. }
  29. local E = {}
  30. local function whitelist_cb(symbol, rule, task)
  31. local domains = {}
  32. local function find_domain(dom, check)
  33. local mult
  34. local how = 'wl'
  35. -- Can be overridden
  36. if rule.blacklist then how = 'bl' end
  37. local function parse_val(val)
  38. local how_override
  39. -- Strict is 'special'
  40. if rule.strict then how_override = 'both' end
  41. if val then
  42. lua_util.debugm(N, task, "found whitelist key: %s=%s", dom, val)
  43. if val == '' then
  44. return (how_override or how),1.0
  45. elseif val:match('^bl:') then
  46. return (how_override or 'bl'),(tonumber(val:sub(4)) or 1.0)
  47. elseif val:match('^wl:') then
  48. return (how_override or 'wl'),(tonumber(val:sub(4)) or 1.0)
  49. elseif val:match('^both:') then
  50. return (how_override or 'both'),(tonumber(val:sub(6)) or 1.0)
  51. else
  52. return (how_override or how),(tonumber(val) or 1.0)
  53. end
  54. end
  55. return (how_override or how),1.0
  56. end
  57. if rule['map'] then
  58. local val = rule['map']:get_key(dom)
  59. if val then
  60. how,mult = parse_val(val)
  61. if not domains[check] then
  62. domains[check] = {}
  63. end
  64. domains[check] = {
  65. [dom] = {how, mult}
  66. }
  67. lua_util.debugm(N, task, "final result: %s: %s->%s",
  68. dom, how, mult)
  69. return true,mult,how
  70. end
  71. elseif rule['maps'] then
  72. for _,v in pairs(rule['maps']) do
  73. local map = v.map
  74. if map then
  75. local val = map:get_key(dom)
  76. if val then
  77. how,mult = parse_val(val)
  78. if not domains[check] then
  79. domains[check] = {}
  80. end
  81. domains[check] = {
  82. [dom] = {how, mult}
  83. }
  84. lua_util.debugm(N, task, "final result: %s: %s->%s",
  85. dom, how, mult)
  86. return true,mult,how
  87. end
  88. end
  89. end
  90. else
  91. mult = rule['domains'][dom]
  92. if mult then
  93. if not domains[check] then
  94. domains[check] = {}
  95. end
  96. domains[check] = {
  97. [dom] = {how, mult}
  98. }
  99. return true, mult,how
  100. end
  101. end
  102. return false,0.0,how
  103. end
  104. local spf_violated = false
  105. local dmarc_violated = false
  106. local dkim_violated = false
  107. local ip_addr = task:get_ip()
  108. if rule.valid_spf then
  109. if not task:has_symbol(options['spf_allow_symbol']) then
  110. -- Not whitelisted
  111. spf_violated = true
  112. end
  113. -- Now we can check from domain or helo
  114. local from = task:get_from(1)
  115. if ((from or E)[1] or E).domain then
  116. local tld = rspamd_util.get_tld(from[1]['domain'])
  117. if tld then
  118. find_domain(tld, 'spf')
  119. end
  120. else
  121. local helo = task:get_helo()
  122. if helo then
  123. local tld = rspamd_util.get_tld(helo)
  124. if tld then
  125. find_domain(tld, 'spf')
  126. end
  127. end
  128. end
  129. end
  130. if rule.valid_dkim then
  131. if task:has_symbol('DKIM_TRACE') then
  132. local sym = task:get_symbol('DKIM_TRACE')
  133. local dkim_opts = sym[1]['options']
  134. if dkim_opts then
  135. fun.each(function(val)
  136. if val[2] == '+' then
  137. local tld = rspamd_util.get_tld(val[1])
  138. find_domain(tld, 'dkim_success')
  139. elseif val[2] == '-' then
  140. local tld = rspamd_util.get_tld(val[1])
  141. find_domain(tld, 'dkim_fail')
  142. end
  143. end,
  144. fun.map(function(s)
  145. return lua_util.rspamd_str_split(s, ':')
  146. end, dkim_opts))
  147. end
  148. end
  149. end
  150. if rule.valid_dmarc then
  151. if not task:has_symbol(options.dmarc_allow_symbol) then
  152. dmarc_violated = true
  153. end
  154. local from = task:get_from(2)
  155. if ((from or E)[1] or E).domain then
  156. local tld = rspamd_util.get_tld(from[1]['domain'])
  157. if tld then
  158. local found = find_domain(tld, 'dmarc')
  159. if not found then
  160. find_domain(from[1]['domain'], 'dmarc')
  161. end
  162. end
  163. end
  164. end
  165. local final_mult = 1.0
  166. local found_wl, found_bl = false, false
  167. local opts = {}
  168. if rule.valid_dkim then
  169. dkim_violated = true
  170. for dom,val in pairs(domains.dkim_success or E) do
  171. if val[1] == 'wl' or val[1] == 'both' then
  172. -- We have valid and whitelisted signature
  173. table.insert(opts, dom .. ':d:+')
  174. found_wl = true
  175. dkim_violated = false
  176. if not found_bl then
  177. final_mult = val[2]
  178. end
  179. end
  180. end
  181. -- Blacklist counterpart
  182. for dom,val in pairs(domains.dkim_fail or E) do
  183. if val[1] == 'bl' or val[1] == 'both' then
  184. -- We have valid and whitelisted signature
  185. table.insert(opts, dom .. ':d:-')
  186. found_bl = true
  187. final_mult = val[2]
  188. else
  189. -- Even in the case of whitelisting we need to indicate dkim failure
  190. dkim_violated = true
  191. end
  192. end
  193. end
  194. local function check_domain_violation(what, dom, val, violated)
  195. if violated then
  196. if val[1] == 'both' or val[1] == 'bl' then
  197. found_bl = true
  198. final_mult = val[2]
  199. table.insert(opts, string.format("%s:%s:-", dom, what))
  200. end
  201. else
  202. if val[1] == 'both' or val[1] == 'wl' then
  203. found_wl = true
  204. table.insert(opts, string.format("%s:%s:+", dom, what))
  205. if not found_bl then
  206. final_mult = val[2]
  207. end
  208. end
  209. end
  210. end
  211. if rule.valid_dmarc then
  212. found_wl = false
  213. for dom,val in pairs(domains.dmarc or E) do
  214. check_domain_violation('D', dom, val,
  215. (dmarc_violated or dkim_violated))
  216. end
  217. end
  218. if rule.valid_spf then
  219. found_wl = false
  220. for dom,val in pairs(domains.spf or E) do
  221. check_domain_violation('s', dom, val,
  222. (spf_violated or dkim_violated))
  223. end
  224. end
  225. lua_util.debugm(N, task, "final mult: %s", final_mult)
  226. local function add_symbol(violated, mult)
  227. local sym = symbol
  228. if violated then
  229. if rule.inverse_symbol then
  230. sym = rule.inverse_symbol
  231. elseif not rule.blacklist then
  232. mult = -mult
  233. end
  234. if rule.inverse_multiplier then
  235. mult = mult * rule.inverse_multiplier
  236. end
  237. task:insert_result(sym, mult, opts)
  238. else
  239. task:insert_result(sym, mult, opts)
  240. end
  241. end
  242. if found_bl then
  243. if not ((not options.check_authed and task:get_user()) or
  244. (not options.check_local and ip_addr and ip_addr:is_local())) then
  245. add_symbol(true, final_mult)
  246. else
  247. if rule.valid_spf or rule.valid_dmarc then
  248. rspamd_logger.infox(task, "skip DMARC/SPF blacklists for local networks and/or authorized users")
  249. else
  250. add_symbol(true, final_mult)
  251. end
  252. end
  253. elseif found_wl then
  254. add_symbol(false, final_mult)
  255. end
  256. end
  257. local function gen_whitelist_cb(symbol, rule)
  258. return function(task)
  259. whitelist_cb(symbol, rule, task)
  260. end
  261. end
  262. local configure_whitelist_module = function()
  263. local opts = rspamd_config:get_all_opt('whitelist')
  264. if opts then
  265. for k,v in pairs(opts) do
  266. options[k] = v
  267. end
  268. local auth_and_local_conf = lua_util.config_check_local_or_authed(rspamd_config, N,
  269. false, false)
  270. options.check_local = auth_and_local_conf[1]
  271. options.check_authed = auth_and_local_conf[2]
  272. else
  273. rspamd_logger.infox(rspamd_config, 'Module is unconfigured')
  274. return
  275. end
  276. if options['rules'] then
  277. fun.each(function(symbol, rule)
  278. if rule['domains'] then
  279. if type(rule['domains']) == 'string' then
  280. rule['map'] = rspamd_config:add_map{
  281. url = rule['domains'],
  282. description = "Whitelist map for " .. symbol,
  283. type = 'map'
  284. }
  285. elseif type(rule['domains']) == 'table' then
  286. -- Transform ['domain1', 'domain2' ...] to indexes:
  287. -- {'domain1' = 1, 'domain2' = 1 ...]
  288. local is_domains_list = fun.all(function(v)
  289. if type(v) == 'table' then
  290. return true
  291. elseif type(v) == 'string' and not (string.match(v, '^https?://') or
  292. string.match(v, '^ftp://') or string.match(v, '^[./]')) then
  293. return true
  294. end
  295. return false
  296. end, rule.domains)
  297. if is_domains_list then
  298. rule['domains'] = fun.tomap(fun.map(function(d)
  299. if type(d) == 'table' then
  300. return d[1],d[2]
  301. end
  302. return d,1.0
  303. end, rule['domains']))
  304. else
  305. rule['map'] = rspamd_config:add_map{
  306. url = rule['domains'],
  307. description = "Whitelist map for " .. symbol,
  308. type = 'map'
  309. }
  310. end
  311. else
  312. rspamd_logger.errx(rspamd_config, 'whitelist %s has bad "domains" value',
  313. symbol)
  314. return
  315. end
  316. local flags = 'nice,empty'
  317. if rule['blacklist'] then
  318. flags = 'empty'
  319. end
  320. local id = rspamd_config:register_symbol({
  321. name = symbol,
  322. flags = flags,
  323. callback = gen_whitelist_cb(symbol, rule),
  324. score = rule.score or 0,
  325. })
  326. if rule.inverse_symbol then
  327. rspamd_config:register_symbol({
  328. name = rule.inverse_symbol,
  329. type = 'virtual',
  330. parent = id,
  331. score = rule.score and -(rule.score) or 0,
  332. })
  333. end
  334. local spf_dep = false
  335. local dkim_dep = false
  336. if rule['valid_spf'] then
  337. rspamd_config:register_dependency(symbol, options['spf_allow_symbol'])
  338. spf_dep = true
  339. end
  340. if rule['valid_dkim'] then
  341. rspamd_config:register_dependency(symbol, options['dkim_allow_symbol'])
  342. dkim_dep = true
  343. end
  344. if rule['valid_dmarc'] then
  345. if not spf_dep then
  346. rspamd_config:register_dependency(symbol, options['spf_allow_symbol'])
  347. end
  348. if not dkim_dep then
  349. rspamd_config:register_dependency(symbol, options['dkim_allow_symbol'])
  350. end
  351. rspamd_config:register_dependency(symbol, 'DMARC_CALLBACK')
  352. end
  353. if rule['score'] then
  354. if not rule['group'] then
  355. rule['group'] = 'whitelist'
  356. end
  357. rule['name'] = symbol
  358. rspamd_config:set_metric_symbol(rule)
  359. if rule.inverse_symbol then
  360. local inv_rule = lua_util.shallowcopy(rule)
  361. inv_rule.name = rule.inverse_symbol
  362. inv_rule.score = -rule.score
  363. rspamd_config:set_metric_symbol(inv_rule)
  364. end
  365. end
  366. end
  367. end, options['rules'])
  368. else
  369. lua_util.disable_module(N, "config")
  370. end
  371. end
  372. configure_whitelist_module()