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.

reputation.lua 39KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370
  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. -- A generic plugin for reputation handling
  17. local E = {}
  18. local N = 'reputation'
  19. local rspamd_logger = require "rspamd_logger"
  20. local rspamd_util = require "rspamd_util"
  21. local lua_util = require "lua_util"
  22. local lua_maps = require "lua_maps"
  23. local lua_maps_exprs = require "lua_maps_expressions"
  24. local hash = require 'rspamd_cryptobox_hash'
  25. local lua_redis = require "lua_redis"
  26. local fun = require "fun"
  27. local lua_selectors = require "lua_selectors"
  28. local ts = require("tableshape").types
  29. local redis_params = nil
  30. local default_expiry = 864000 -- 10 day by default
  31. local default_prefix = 'RR:' -- Rspamd Reputation
  32. local tanh = math.tanh or rspamd_util.tanh
  33. local reject_threshold = rspamd_config:get_action('reject') or 10.0
  34. -- Get reputation from ham/spam/probable hits
  35. local function generic_reputation_calc(token, rule, mult, task)
  36. local cfg = rule.selector.config or E
  37. if cfg.score_calc_func then
  38. return cfg.score_calc_func(rule, token, mult)
  39. end
  40. if tonumber(token[1]) < cfg.lower_bound then
  41. lua_util.debugm(N, task, "not enough matches %s < %s for rule %s",
  42. token[1], cfg.lower_bound, rule.symbol)
  43. return 0
  44. end
  45. -- Get average score
  46. local avg_score = fun.foldl(function(acc, v)
  47. return acc + v
  48. end, 0.0, fun.map(tonumber, token[2])) / #token[2]
  49. -- Apply function tanh(x / reject_score * atanh(0.95) - atanh(0.5))
  50. -- 1.83178 0.5493
  51. local score = tanh(avg_score / reject_threshold * 1.83178 - 0.5493) * mult
  52. lua_util.debugm(N, task, "got generic average score %s -> %s for rule %s",
  53. avg_score, score, rule.symbol)
  54. return score
  55. end
  56. local function add_symbol_score(task, rule, mult, params)
  57. if not params then
  58. params = {tostring(mult)}
  59. end
  60. if rule.selector.config.split_symbols then
  61. local sym_spam = rule.symbol .. '_SPAM'
  62. local sym_ham = rule.symbol .. '_HAM'
  63. if not rule.static_symbols then
  64. rule.static_symbols = {}
  65. rule.static_symbols.ham = rspamd_config:get_symbol(sym_ham)
  66. rule.static_symbols.spam = rspamd_config:get_symbol(sym_spam)
  67. end
  68. if mult >= 0 then
  69. task:insert_result(sym_spam, mult, params)
  70. else
  71. -- Avoid multiplication of negative the `mult` by negative static score of the
  72. -- ham symbol
  73. if rule.static_symbols.ham and rule.static_symbols.ham.score then
  74. if rule.static_symbols.ham.score < 0 then
  75. mult = math.abs(mult)
  76. end
  77. end
  78. task:insert_result(sym_ham, mult, params)
  79. end
  80. else
  81. task:insert_result(rule.symbol, mult, params)
  82. end
  83. end
  84. local function sub_symbol_score(task, rule, score)
  85. local function sym_score(sym)
  86. local s = task:get_symbol(sym)[1]
  87. return s.score
  88. end
  89. if rule.selector.config.split_symbols then
  90. local spam_sym = rule.symbol .. '_SPAM'
  91. local ham_sym = rule.symbol .. '_HAM'
  92. if task:has_symbol(spam_sym) then
  93. score = score - sym_score(spam_sym)
  94. elseif task:has_symbol(ham_sym) then
  95. score = score - sym_score(ham_sym)
  96. end
  97. else
  98. if task:has_symbol(rule.symbol) then
  99. score = score - sym_score(rule.symbol)
  100. end
  101. end
  102. return score
  103. end
  104. -- Extracts task score and subtracts score of the rule itself
  105. local function extract_task_score(task, rule)
  106. local lua_verdict = require "lua_verdict"
  107. local verdict,score = lua_verdict.get_specific_verdict(N, task)
  108. if not score or verdict == 'passthrough' then return nil end
  109. return sub_symbol_score(task, rule, score)
  110. end
  111. -- DKIM Selector functions
  112. local gr
  113. local function gen_dkim_queries(task, rule)
  114. local dkim_trace = (task:get_symbol('DKIM_TRACE') or E)[1]
  115. local lpeg = require 'lpeg'
  116. local ret = {}
  117. if not gr then
  118. local semicolon = lpeg.P(':')
  119. local domain = lpeg.C((1 - semicolon)^1)
  120. local res = lpeg.S'+-?~'
  121. local function res_to_label(ch)
  122. if ch == '+' then return 'a'
  123. elseif ch == '-' then return 'r'
  124. end
  125. return 'u'
  126. end
  127. gr = domain * semicolon * (lpeg.C(res^1) / res_to_label)
  128. end
  129. if dkim_trace and dkim_trace.options then
  130. for _,opt in ipairs(dkim_trace.options) do
  131. local dom,res = lpeg.match(gr, opt)
  132. if dom and res then
  133. local tld = rspamd_util.get_tld(dom)
  134. ret[tld] = res
  135. end
  136. end
  137. end
  138. return ret
  139. end
  140. local function dkim_reputation_filter(task, rule)
  141. local requests = gen_dkim_queries(task, rule)
  142. local results = {}
  143. local dkim_tlds = lua_util.keys(requests)
  144. local requests_left = #dkim_tlds
  145. local rep_accepted = 0.0
  146. lua_util.debugm(N, task, 'dkim reputation tokens: %s', requests)
  147. local function tokens_cb(err, token, values)
  148. requests_left = requests_left - 1
  149. if values then
  150. results[token] = values
  151. end
  152. if requests_left == 0 then
  153. for k,v in pairs(results) do
  154. -- `k` in results is a prefixed and suffixed tld, so we need to look through
  155. -- all requests to find any request with the matching tld
  156. local sel_tld
  157. for _,tld in ipairs(dkim_tlds) do
  158. if k:find(tld, 1, true) then
  159. sel_tld = tld
  160. break
  161. end
  162. end
  163. if sel_tld and requests[sel_tld] then
  164. if requests[sel_tld] == 'a' then
  165. rep_accepted = rep_accepted + generic_reputation_calc(v, rule, 1.0, task)
  166. end
  167. else
  168. rspamd_logger.warnx(task, "cannot find the requested tld for a request: %s (%s tlds noticed)",
  169. k, dkim_tlds)
  170. end
  171. end
  172. -- Set local reputation symbol
  173. local rep_accepted_abs = math.abs(rep_accepted or 0)
  174. lua_util.debugm(N, task, "dkim reputation accepted: %s",
  175. rep_accepted_abs)
  176. if rep_accepted_abs then
  177. local final_rep = rep_accepted
  178. if rep_accepted > 1.0 then final_rep = 1.0 end
  179. if rep_accepted < -1.0 then final_rep = -1.0 end
  180. add_symbol_score(task, rule, final_rep)
  181. -- Store results for future DKIM results adjustments
  182. task:get_mempool():set_variable("dkim_reputation_accept", tostring(rep_accepted))
  183. end
  184. end
  185. end
  186. for dom,res in pairs(requests) do
  187. -- tld + "." + check_result, e.g. example.com.+ - reputation for valid sigs
  188. local query = string.format('%s.%s', dom, res)
  189. rule.backend.get_token(task, rule, nil, query, tokens_cb, 'string')
  190. end
  191. end
  192. local function dkim_reputation_idempotent(task, rule)
  193. local requests = gen_dkim_queries(task, rule)
  194. local sc = extract_task_score(task, rule)
  195. if sc then
  196. for dom,res in pairs(requests) do
  197. -- tld + "." + check_result, e.g. example.com.+ - reputation for valid sigs
  198. local query = string.format('%s.%s', dom, res)
  199. rule.backend.set_token(task, rule, nil, query, sc)
  200. end
  201. end
  202. end
  203. local function dkim_reputation_postfilter(task, rule)
  204. local sym_accepted = (task:get_symbol('R_DKIM_ALLOW') or E)[1]
  205. local accept_adjustment = task:get_mempool():get_variable("dkim_reputation_accept")
  206. local cfg = rule.selector.config or E
  207. if sym_accepted and sym_accepted.score and
  208. accept_adjustment and type(cfg.max_accept_adjustment) == 'number' then
  209. local final_adjustment = cfg.max_accept_adjustment *
  210. rspamd_util.tanh(tonumber(accept_adjustment) or 0)
  211. lua_util.debugm(N, task, "adjust DKIM_ALLOW: " ..
  212. "cfg.max_accept_adjustment=%s accept_adjustment=%s final_adjustment=%s sym_accepted.score=%s",
  213. cfg.max_accept_adjustment, accept_adjustment, final_adjustment,
  214. sym_accepted.score)
  215. task:adjust_result('R_DKIM_ALLOW', sym_accepted.score + final_adjustment)
  216. end
  217. end
  218. local dkim_selector = {
  219. config = {
  220. symbol = 'DKIM_SCORE', -- symbol to be inserted
  221. lower_bound = 10, -- minimum number of messages to be scored
  222. min_score = nil,
  223. max_score = nil,
  224. outbound = true,
  225. inbound = true,
  226. max_accept_adjustment = 2.0, -- How to adjust accepted DKIM score
  227. },
  228. dependencies = {"DKIM_TRACE"},
  229. filter = dkim_reputation_filter, -- used to get scores
  230. postfilter = dkim_reputation_postfilter, -- used to adjust DKIM scores
  231. idempotent = dkim_reputation_idempotent, -- used to set scores
  232. }
  233. -- URL Selector functions
  234. local function gen_url_queries(task, rule)
  235. local domains = {}
  236. fun.each(function(u)
  237. if u:is_redirected() then
  238. local redir = u:get_redirected() -- get the original url
  239. local redir_tld = redir:get_tld()
  240. if domains[redir_tld] then
  241. domains[redir_tld] = domains[redir_tld] - 1
  242. end
  243. end
  244. local dom = u:get_tld()
  245. if not domains[dom] then
  246. domains[dom] = 1
  247. else
  248. domains[dom] = domains[dom] + 1
  249. end
  250. end, fun.filter(function(u) return not u:is_html_displayed() end,
  251. task:get_urls(true)))
  252. local results = {}
  253. for k,v in lua_util.spairs(domains,
  254. function(t, a, b) return t[a] > t[b] end, rule.selector.config.max_urls) do
  255. if v > 0 then
  256. table.insert(results, {k,v})
  257. end
  258. end
  259. return results
  260. end
  261. local function url_reputation_filter(task, rule)
  262. local requests = gen_url_queries(task, rule)
  263. local url_keys = lua_util.keys(requests)
  264. local requests_left = #url_keys
  265. local results = {}
  266. local function indexed_tokens_cb(err, index, values)
  267. requests_left = requests_left - 1
  268. if values then
  269. results[index] = values
  270. end
  271. if requests_left == 0 then
  272. -- Check the url with maximum hits
  273. local mhits = 0
  274. local result_request_match_tbl = {}
  275. -- XXX: ugly O(N^2) loop to match requests and responses
  276. for _,res_pair in ipairs(results) do
  277. local result_k = res_pair[1]
  278. for _, request_k in ipairs(url_keys) do
  279. if result_k:find(request_k) then
  280. result_request_match_tbl[result_k] = request_k
  281. break
  282. end
  283. end
  284. end
  285. for k,_ in pairs(results) do
  286. local req = result_request_match_tbl[k]
  287. if req then
  288. if requests[req] > mhits then
  289. mhits = requests[req][2]
  290. else
  291. rspamd_logger.warnx(task, "cannot find the requested response for a request: %s (%s requests noticed)",
  292. k, url_keys)
  293. end
  294. end
  295. end
  296. if mhits > 0 then
  297. local score = 0
  298. for k,v in pairs(results) do
  299. local req = result_request_match_tbl[k]
  300. if req then
  301. score = score + generic_reputation_calc(v, rule,
  302. requests[req][2] / mhits, task)
  303. end
  304. end
  305. if math.abs(score) > 1e-3 then
  306. -- TODO: add description
  307. add_symbol_score(task, rule, score)
  308. end
  309. end
  310. end
  311. end
  312. for i,req in ipairs(requests) do
  313. local function tokens_cb(err, token, values)
  314. indexed_tokens_cb(err, i, values)
  315. end
  316. rule.backend.get_token(task, rule, nil, req[1], tokens_cb, 'string')
  317. end
  318. end
  319. local function url_reputation_idempotent(task, rule)
  320. local requests = gen_url_queries(task, rule)
  321. local sc = extract_task_score(task, rule)
  322. if sc then
  323. for _,tld in ipairs(requests) do
  324. rule.backend.set_token(task, rule, nil, tld[1], sc)
  325. end
  326. end
  327. end
  328. local url_selector = {
  329. config = {
  330. symbol = 'URL_SCORE', -- symbol to be inserted
  331. lower_bound = 10, -- minimum number of messages to be scored
  332. min_score = nil,
  333. max_score = nil,
  334. max_urls = 10,
  335. check_from = true,
  336. outbound = true,
  337. inbound = true,
  338. },
  339. filter = url_reputation_filter, -- used to get scores
  340. idempotent = url_reputation_idempotent -- used to set scores
  341. }
  342. -- IP Selector functions
  343. local function ip_reputation_init(rule)
  344. local cfg = rule.selector.config
  345. if cfg.asn_cc_whitelist then
  346. cfg.asn_cc_whitelist = lua_maps.map_add('reputation',
  347. 'asn_cc_whitelist',
  348. 'map',
  349. 'IP score whitelisted ASNs/countries')
  350. end
  351. return true
  352. end
  353. local function ip_reputation_filter(task, rule)
  354. local ip = task:get_from_ip()
  355. if not ip or not ip:is_valid() then return end
  356. if lua_util.is_rspamc_or_controller(task) then return end
  357. local cfg = rule.selector.config
  358. if ip:get_version() == 4 and cfg.ipv4_mask then
  359. ip = ip:apply_mask(cfg.ipv4_mask)
  360. elseif cfg.ipv6_mask then
  361. ip = ip:apply_mask(cfg.ipv6_mask)
  362. end
  363. local pool = task:get_mempool()
  364. local asn = pool:get_variable("asn")
  365. local country = pool:get_variable("country")
  366. if country and cfg.asn_cc_whitelist then
  367. if cfg.asn_cc_whitelist:get_key(country) then
  368. return
  369. end
  370. if asn and cfg.asn_cc_whitelist:get_key(asn) then
  371. return
  372. end
  373. end
  374. -- These variables are used to define if we have some specific token
  375. local has_asn = not asn
  376. local has_country = not country
  377. local has_ip = false
  378. local asn_stats, country_stats, ip_stats
  379. local function ipstats_check()
  380. local score = 0.0
  381. local description_t = {}
  382. if asn_stats then
  383. local asn_score = generic_reputation_calc(asn_stats, rule, cfg.scores.asn, task)
  384. score = score + asn_score
  385. table.insert(description_t, string.format('asn: %s(%.2f)',
  386. asn, asn_score))
  387. end
  388. if country_stats then
  389. local country_score = generic_reputation_calc(country_stats, rule,
  390. cfg.scores.country, task)
  391. score = score + country_score
  392. table.insert(description_t, string.format('country: %s(%.2f)',
  393. country, country_score))
  394. end
  395. if ip_stats then
  396. local ip_score = generic_reputation_calc(ip_stats, rule, cfg.scores.ip,
  397. task)
  398. score = score + ip_score
  399. table.insert(description_t, string.format('ip: %s(%.2f)',
  400. tostring(ip), ip_score))
  401. end
  402. if math.abs(score) > 0.001 then
  403. add_symbol_score(task, rule, score, table.concat(description_t, ', '))
  404. end
  405. end
  406. local function gen_token_callback(what)
  407. return function(err, _, values)
  408. if not err and values then
  409. if what == 'asn' then
  410. has_asn = true
  411. asn_stats = values
  412. elseif what == 'country' then
  413. has_country = true
  414. country_stats = values
  415. elseif what == 'ip' then
  416. has_ip = true
  417. ip_stats = values
  418. end
  419. else
  420. if what == 'asn' then
  421. has_asn = true
  422. elseif what == 'country' then
  423. has_country = true
  424. elseif what == 'ip' then
  425. has_ip = true
  426. end
  427. end
  428. if has_asn and has_country and has_ip then
  429. -- Check reputation
  430. ipstats_check()
  431. end
  432. end
  433. end
  434. if asn then
  435. rule.backend.get_token(task, rule, cfg.asn_prefix, asn,
  436. gen_token_callback('asn'), 'string')
  437. end
  438. if country then
  439. rule.backend.get_token(task, rule, cfg.country_prefix, country,
  440. gen_token_callback('country'), 'string')
  441. end
  442. rule.backend.get_token(task, rule, cfg.ip_prefix, ip,
  443. gen_token_callback('ip'), 'ip')
  444. end
  445. -- Used to set scores
  446. local function ip_reputation_idempotent(task, rule)
  447. if not rule.backend.set_token then return end -- Read only backend
  448. local ip = task:get_from_ip()
  449. local cfg = rule.selector.config
  450. if not ip or not ip:is_valid() then return end
  451. if lua_util.is_rspamc_or_controller(task) then return end
  452. if ip:get_version() == 4 and cfg.ipv4_mask then
  453. ip = ip:apply_mask(cfg.ipv4_mask)
  454. elseif cfg.ipv6_mask then
  455. ip = ip:apply_mask(cfg.ipv6_mask)
  456. end
  457. local pool = task:get_mempool()
  458. local asn = pool:get_variable("asn")
  459. local country = pool:get_variable("country")
  460. if country and cfg.asn_cc_whitelist then
  461. if cfg.asn_cc_whitelist:get_key(country) then
  462. return
  463. end
  464. if asn and cfg.asn_cc_whitelist:get_key(asn) then
  465. return
  466. end
  467. end
  468. local sc = extract_task_score(task, rule)
  469. if sc then
  470. if asn then
  471. rule.backend.set_token(task, rule, cfg.asn_prefix, asn, sc, nil, 'string')
  472. end
  473. if country then
  474. rule.backend.set_token(task, rule, cfg.country_prefix, country, sc, nil, 'string')
  475. end
  476. rule.backend.set_token(task, rule, cfg.ip_prefix, ip, sc, nil, 'ip')
  477. end
  478. end
  479. -- Selectors are used to extract reputation tokens
  480. local ip_selector = {
  481. config = {
  482. scores = { -- how each component is evaluated
  483. ['asn'] = 0.4,
  484. ['country'] = 0.01,
  485. ['ip'] = 1.0
  486. },
  487. symbol = 'SENDER_REP', -- symbol to be inserted
  488. split_symbols = true,
  489. asn_prefix = 'a:', -- prefix for ASN hashes
  490. country_prefix = 'c:', -- prefix for country hashes
  491. ip_prefix = 'i:',
  492. lower_bound = 10, -- minimum number of messages to be scored
  493. min_score = nil,
  494. max_score = nil,
  495. score_divisor = 1,
  496. outbound = false,
  497. inbound = true,
  498. ipv4_mask = 32, -- Mask bits for ipv4
  499. ipv6_mask = 64, -- Mask bits for ipv6
  500. },
  501. --dependencies = {"ASN"}, -- ASN is a prefilter now...
  502. init = ip_reputation_init,
  503. filter = ip_reputation_filter, -- used to get scores
  504. idempotent = ip_reputation_idempotent, -- used to set scores
  505. }
  506. -- SPF Selector functions
  507. local function spf_reputation_filter(task, rule)
  508. local spf_record = task:get_mempool():get_variable('spf_record')
  509. local spf_allow = task:has_symbol('R_SPF_ALLOW')
  510. -- Don't care about bad/missing spf
  511. if not spf_record or not spf_allow then return end
  512. local cr = require "rspamd_cryptobox_hash"
  513. local hkey = cr.create(spf_record):base32():sub(1, 32)
  514. lua_util.debugm(N, task, 'check spf record %s -> %s', spf_record, hkey)
  515. local function tokens_cb(err, token, values)
  516. if values then
  517. local score = generic_reputation_calc(values, rule, 1.0, task)
  518. if math.abs(score) > 1e-3 then
  519. -- TODO: add description
  520. add_symbol_score(task, rule, score)
  521. end
  522. end
  523. end
  524. rule.backend.get_token(task, rule, nil, hkey, tokens_cb, 'string')
  525. end
  526. local function spf_reputation_idempotent(task, rule)
  527. local sc = extract_task_score(task, rule)
  528. local spf_record = task:get_mempool():get_variable('spf_record')
  529. local spf_allow = task:has_symbol('R_SPF_ALLOW')
  530. if not spf_record or not spf_allow or not sc then return end
  531. local cr = require "rspamd_cryptobox_hash"
  532. local hkey = cr.create(spf_record):base32():sub(1, 32)
  533. lua_util.debugm(N, task, 'set spf record %s -> %s = %s',
  534. spf_record, hkey, sc)
  535. rule.backend.set_token(task, rule, nil, hkey, sc)
  536. end
  537. local spf_selector = {
  538. config = {
  539. symbol = 'SPF_REP', -- symbol to be inserted
  540. split_symbols = true,
  541. lower_bound = 10, -- minimum number of messages to be scored
  542. min_score = nil,
  543. max_score = nil,
  544. outbound = true,
  545. inbound = true,
  546. },
  547. dependencies = {"R_SPF_ALLOW"},
  548. filter = spf_reputation_filter, -- used to get scores
  549. idempotent = spf_reputation_idempotent, -- used to set scores
  550. }
  551. -- Generic selector based on lua_selectors framework
  552. local function generic_reputation_init(rule)
  553. local cfg = rule.selector.config
  554. if not cfg.selector then
  555. rspamd_logger.errx(rspamd_config, 'cannot configure generic rule: no selector specified')
  556. return false
  557. end
  558. local selector = lua_selectors.create_selector_closure(rspamd_config,
  559. cfg.selector, cfg.delimiter)
  560. if not selector then
  561. rspamd_logger.errx(rspamd_config, 'cannot configure generic rule: bad selector: %s',
  562. cfg.selector)
  563. return false
  564. end
  565. cfg.selector = selector -- Replace with closure
  566. if cfg.whitelist then
  567. cfg.whitelist = lua_maps.map_add('reputation',
  568. 'generic_whitelist',
  569. 'map',
  570. 'Whitelisted selectors')
  571. end
  572. return true
  573. end
  574. local function generic_reputation_filter(task, rule)
  575. local cfg = rule.selector.config
  576. local selector_res = cfg.selector(task)
  577. local function tokens_cb(err, token, values)
  578. if values then
  579. local score = generic_reputation_calc(values, rule, 1.0, task)
  580. if math.abs(score) > 1e-3 then
  581. -- TODO: add description
  582. add_symbol_score(task, rule, score)
  583. end
  584. end
  585. end
  586. if selector_res then
  587. if type(selector_res) == 'table' then
  588. fun.each(function(e)
  589. lua_util.debugm(N, task, 'check generic reputation (%s) %s',
  590. rule['symbol'], e)
  591. rule.backend.get_token(task, rule, nil, e, tokens_cb, 'string')
  592. end, selector_res)
  593. else
  594. lua_util.debugm(N, task, 'check generic reputation (%s) %s',
  595. rule['symbol'], selector_res)
  596. rule.backend.get_token(task, rule, nil, selector_res, tokens_cb, 'string')
  597. end
  598. end
  599. end
  600. local function generic_reputation_idempotent(task, rule)
  601. local sc = extract_task_score(task, rule)
  602. local cfg = rule.selector.config
  603. local selector_res = cfg.selector(task)
  604. if not selector_res then return end
  605. if sc then
  606. if type(selector_res) == 'table' then
  607. fun.each(function(e)
  608. lua_util.debugm(N, task, 'set generic selector (%s) %s = %s',
  609. rule['symbol'], e, sc)
  610. rule.backend.set_token(task, rule, nil, e, sc)
  611. end, selector_res)
  612. else
  613. lua_util.debugm(N, task, 'set generic selector (%s) %s = %s',
  614. rule['symbol'], selector_res, sc)
  615. rule.backend.set_token(task, rule, nil, selector_res, sc)
  616. end
  617. end
  618. end
  619. local generic_selector = {
  620. schema = ts.shape{
  621. lower_bound = ts.number + ts.string / tonumber,
  622. max_score = ts.number:is_optional(),
  623. min_score = ts.number:is_optional(),
  624. outbound = ts.boolean,
  625. inbound = ts.boolean,
  626. selector = ts.string,
  627. delimiter = ts.string,
  628. whitelist = ts.one_of(lua_maps.map_schema, lua_maps_exprs.schema):is_optional(),
  629. },
  630. config = {
  631. lower_bound = 10, -- minimum number of messages to be scored
  632. min_score = nil,
  633. max_score = nil,
  634. outbound = true,
  635. inbound = true,
  636. selector = nil,
  637. delimiter = ':',
  638. whitelist = nil
  639. },
  640. init = generic_reputation_init,
  641. filter = generic_reputation_filter, -- used to get scores
  642. idempotent = generic_reputation_idempotent -- used to set scores
  643. }
  644. local selectors = {
  645. ip = ip_selector,
  646. sender = ip_selector, -- Better name
  647. url = url_selector,
  648. dkim = dkim_selector,
  649. spf = spf_selector,
  650. generic = generic_selector
  651. }
  652. local function reputation_dns_init(rule, _, _, _)
  653. if not rule.backend.config.list then
  654. rspamd_logger.errx(rspamd_config, "rule %s with DNS backend has no `list` parameter defined",
  655. rule.symbol)
  656. return false
  657. end
  658. return true
  659. end
  660. local function gen_token_key(prefix, token, rule)
  661. if prefix then
  662. token = prefix .. token
  663. end
  664. local res = token
  665. if rule.backend.config.hashed then
  666. local hash_alg = rule.backend.config.hash_alg or "blake2"
  667. local encoding = "base32"
  668. if rule.backend.config.hash_encoding then
  669. encoding = rule.backend.config.hash_encoding
  670. end
  671. local h = hash.create_specific(hash_alg, res)
  672. if encoding == 'hex' then
  673. res = h:hex()
  674. elseif encoding == 'base64' then
  675. res = h:base64()
  676. else
  677. res = h:base32()
  678. end
  679. end
  680. if rule.backend.config.hashlen then
  681. res = string.sub(res, 1, rule.backend.config.hashlen)
  682. end
  683. if rule.backend.config.prefix then
  684. res = rule.backend.config.prefix .. res
  685. end
  686. return res
  687. end
  688. --[[
  689. -- Generic interface for get and set tokens functions:
  690. -- get_token(task, rule, prefix, token, continuation, token_type), where `continuation` is the following function:
  691. --
  692. -- function(err, token, values) ... end
  693. -- `err`: string value for error (similar to redis or DNS callbacks)
  694. -- `token`: string value of a token
  695. -- `values`: table of key=number, parsed from backend. It is selector's duty
  696. -- to deal with missing, invalid or other values
  697. --
  698. -- set_token(task, rule, token, values, continuation_cb)
  699. -- This function takes values, encodes them using whatever suitable format
  700. -- and calls for continuation:
  701. --
  702. -- function(err, token) ... end
  703. -- `err`: string value for error (similar to redis or DNS callbacks)
  704. -- `token`: string value of a token
  705. --
  706. -- example of tokens: {'s': 0, 'h': 0, 'p': 1}
  707. --]]
  708. local function reputation_dns_get_token(task, rule, prefix, token, continuation_cb, token_type)
  709. -- local r = task:get_resolver()
  710. -- In DNS we never ever use prefix as prefix, we use if as a suffix!
  711. if token_type == 'ip' then
  712. token = table.concat(token:inversed_str_octets(), '.')
  713. end
  714. local key = gen_token_key(nil, token, rule)
  715. local dns_name = key .. '.' .. rule.backend.config.list
  716. if prefix then
  717. dns_name = string.format('%s.%s.%s', key, prefix,
  718. rule.backend.config.list)
  719. else
  720. dns_name = string.format('%s.%s', key, rule.backend.config.list)
  721. end
  722. local function dns_cb(_, _, results, err)
  723. if err and (err ~= 'requested record is not found' and
  724. err ~= 'no records with this name') then
  725. rspamd_logger.warnx(task, 'error looking up %s: %s', dns_name, err)
  726. end
  727. lua_util.debugm(N, task, 'DNS RESPONSE: label=%1 results=%2 err=%3 list=%4',
  728. dns_name, results, err, rule.backend.config.list)
  729. -- Now split tokens to list of values
  730. if results and results[1] then
  731. -- Format: num_messages;sc1;sc2...scn
  732. local dns_tokens = lua_util.rspamd_str_split(results[1], ";")
  733. -- Convert all to numbers excluding any possible non-numbers
  734. dns_tokens = fun.totable(fun.filter(function(e)
  735. return type(e) == 'number'
  736. end,
  737. fun.map(function(e)
  738. local n = tonumber(e)
  739. if n then return n end
  740. return "BAD"
  741. end, dns_tokens)))
  742. if #dns_tokens < 2 then
  743. rspamd_logger.warnx(task, 'cannot parse response for reputation token %s: %s',
  744. dns_name, results[1])
  745. continuation_cb(results, dns_name, nil)
  746. else
  747. local cnt = table.remove(dns_tokens, 1)
  748. continuation_cb(nil, dns_name, { cnt, dns_tokens })
  749. end
  750. else
  751. rspamd_logger.messagex(task, 'invalid response for reputation token %s: %s',
  752. dns_name, results[1])
  753. continuation_cb(results, dns_name, nil)
  754. end
  755. end
  756. task:get_resolver():resolve_a({
  757. task = task,
  758. name = dns_name,
  759. callback = dns_cb,
  760. forced = true,
  761. })
  762. end
  763. local function reputation_redis_init(rule, cfg, ev_base, worker)
  764. local our_redis_params = {}
  765. our_redis_params = lua_redis.try_load_redis_servers(rule.backend.config, rspamd_config,
  766. true)
  767. if not our_redis_params then
  768. our_redis_params = redis_params
  769. end
  770. if not our_redis_params then
  771. rspamd_logger.errx(rspamd_config, 'cannot init redis for reputation rule: %s',
  772. rule)
  773. return false
  774. end
  775. -- Init scripts for buckets
  776. -- Redis script to extract data from Redis buckets
  777. -- KEYS[1] - key to extract
  778. -- Value returned - table of scores as a strings vector + number of scores
  779. local redis_get_script_tpl = [[
  780. local cnt = redis.call('HGET', KEYS[1], 'n')
  781. local results = {}
  782. if cnt then
  783. {% for w in windows %}
  784. local sc = tonumber(redis.call('HGET', KEYS[1], 'v' .. '{= w.name =}'))
  785. table.insert(results, tostring(sc * {= w.mult =}))
  786. {% endfor %}
  787. else
  788. {% for w in windows %}
  789. table.insert(results, '0')
  790. {% endfor %}
  791. end
  792. return {cnt or 0, results}
  793. ]]
  794. local get_script = lua_util.jinja_template(redis_get_script_tpl,
  795. {windows = rule.backend.config.buckets})
  796. rspamd_logger.debugm(N, rspamd_config, 'added extraction script %s', get_script)
  797. rule.backend.script_get = lua_redis.add_redis_script(get_script, our_redis_params)
  798. -- Redis script to update Redis buckets
  799. -- KEYS[1] - key to update
  800. -- KEYS[2] - current time in milliseconds
  801. -- KEYS[3] - message score
  802. -- KEYS[4] - expire for a bucket
  803. -- Value returned - table of scores as a strings vector
  804. local redis_adaptive_emea_script_tpl = [[
  805. local last = redis.call('HGET', KEYS[1], 'l')
  806. local score = tonumber(KEYS[3])
  807. local now = tonumber(KEYS[2])
  808. local scores = {}
  809. if last then
  810. {% for w in windows %}
  811. local last_value = tonumber(redis.call('HGET', KEYS[1], 'v' .. '{= w.name =}'))
  812. local window = {= w.time =}
  813. -- Adjust alpha
  814. local time_diff = now - last
  815. if time_diff < 0 then
  816. time_diff = 0
  817. end
  818. local alpha = 1.0 - math.exp((-time_diff) / (1000 * window))
  819. local nscore = alpha * score + (1.0 - alpha) * last_value
  820. table.insert(scores, tostring(nscore * {= w.mult =}))
  821. {% endfor %}
  822. else
  823. {% for w in windows %}
  824. table.insert(scores, tostring(score * {= w.mult =}))
  825. {% endfor %}
  826. end
  827. local i = 1
  828. {% for w in windows %}
  829. redis.call('HSET', KEYS[1], 'v' .. '{= w.name =}', scores[i])
  830. i = i + 1
  831. {% endfor %}
  832. redis.call('HSET', KEYS[1], 'l', now)
  833. redis.call('HINCRBY', KEYS[1], 'n', 1)
  834. redis.call('EXPIRE', KEYS[1], tonumber(KEYS[4]))
  835. return scores
  836. ]]
  837. local set_script = lua_util.jinja_template(redis_adaptive_emea_script_tpl,
  838. {windows = rule.backend.config.buckets})
  839. rspamd_logger.debugm(N, rspamd_config, 'added emea update script %s', set_script)
  840. rule.backend.script_set = lua_redis.add_redis_script(set_script, our_redis_params)
  841. return true
  842. end
  843. local function reputation_redis_get_token(task, rule, prefix, token, continuation_cb, token_type)
  844. if token_type and token_type == 'ip' then
  845. token = tostring(token)
  846. end
  847. local key = gen_token_key(prefix, token, rule)
  848. local function redis_get_cb(err, data)
  849. if data then
  850. if type(data) == 'table' then
  851. lua_util.debugm(N, task, 'rule %s - got values for key %s -> %s',
  852. rule['symbol'], key, data)
  853. continuation_cb(nil, key, data)
  854. else
  855. rspamd_logger.errx(task, 'rule %s - invalid type while getting reputation keys %s: %s',
  856. rule['symbol'], key, type(data))
  857. continuation_cb("invalid type", key, nil)
  858. end
  859. elseif err then
  860. rspamd_logger.errx(task, 'rule %s - got error while getting reputation keys %s: %s',
  861. rule['symbol'], key, err)
  862. continuation_cb(err, key, nil)
  863. else
  864. rspamd_logger.errx(task, 'rule %s - got error while getting reputation keys %s: %s',
  865. rule['symbol'], key, "unknown error")
  866. continuation_cb("unknown error", key, nil)
  867. end
  868. end
  869. local ret = lua_redis.exec_redis_script(rule.backend.script_get,
  870. {task = task, is_write = false},
  871. redis_get_cb,
  872. {key})
  873. if not ret then
  874. rspamd_logger.errx(task, 'cannot make redis request to check results')
  875. end
  876. end
  877. local function reputation_redis_set_token(task, rule, prefix, token, sc, continuation_cb, token_type)
  878. if token_type and token_type == 'ip' then
  879. token = tostring(token)
  880. end
  881. local key = gen_token_key(prefix, token, rule)
  882. local function redis_set_cb(err, data)
  883. if err then
  884. rspamd_logger.errx(task, 'rule %s - got error while setting reputation keys %s: %s',
  885. rule['symbol'], key, err)
  886. if continuation_cb then
  887. continuation_cb(err, key)
  888. end
  889. else
  890. if continuation_cb then
  891. continuation_cb(nil, key)
  892. end
  893. end
  894. end
  895. lua_util.debugm(N, task, 'rule %s - set values for key %s -> %s',
  896. rule['symbol'], key, sc)
  897. local ret = lua_redis.exec_redis_script(rule.backend.script_set,
  898. {task = task, is_write = true},
  899. redis_set_cb,
  900. {key, tostring(os.time() * 1000),
  901. tostring(sc),
  902. tostring(rule.backend.config.expiry)})
  903. if not ret then
  904. rspamd_logger.errx(task, 'got error while connecting to redis')
  905. end
  906. end
  907. --[[ Backends are responsible for getting reputation tokens
  908. -- Common config options:
  909. -- `hashed`: if `true` then apply hash function to the key
  910. -- `hash_alg`: use specific hash type (`blake2` by default)
  911. -- `hash_len`: strip hash to this amount of bytes (no strip by default)
  912. -- `hash_encoding`: use specific hash encoding (base32 by default)
  913. --]]
  914. local backends = {
  915. redis = {
  916. schema = ts.shape({
  917. prefix = ts.string,
  918. expiry = ts.number + ts.string / lua_util.parse_time_interval,
  919. buckets = ts.array_of(ts.shape{
  920. time = ts.number + ts.string / lua_util.parse_time_interval,
  921. name = ts.string,
  922. mult = ts.number + ts.string / tonumber
  923. }),
  924. }, {extra_fields = lua_redis.config_schema}),
  925. config = {
  926. expiry = default_expiry,
  927. prefix = default_prefix,
  928. buckets = {
  929. {
  930. time = 60 * 60 * 24 * 30,
  931. name = '1m',
  932. mult = 1.0,
  933. }
  934. }, -- What buckets should be used, default 1h and 1month
  935. },
  936. init = reputation_redis_init,
  937. get_token = reputation_redis_get_token,
  938. set_token = reputation_redis_set_token,
  939. },
  940. dns = {
  941. schema = ts.shape{
  942. list = ts.string,
  943. },
  944. config = {
  945. -- list = rep.example.com
  946. },
  947. get_token = reputation_dns_get_token,
  948. -- No set token for DNS
  949. init = reputation_dns_init,
  950. }
  951. }
  952. local function is_rule_applicable(task, rule)
  953. local ip = task:get_from_ip()
  954. if not (rule.selector.config.outbound and rule.selector.config.inbound) then
  955. if rule.selector.config.outbound then
  956. if not (task:get_user() or (ip and ip:is_local())) then
  957. return false
  958. end
  959. elseif rule.selector.config.inbound then
  960. if task:get_user() or (ip and ip:is_local()) then
  961. return false
  962. end
  963. end
  964. end
  965. if rule.config.whitelist_map then
  966. if rule.config.whitelist_map:process(task) then
  967. return false
  968. end
  969. end
  970. return true
  971. end
  972. local function reputation_filter_cb(task, rule)
  973. if (is_rule_applicable(task, rule)) then
  974. rule.selector.filter(task, rule, rule.backend)
  975. end
  976. end
  977. local function reputation_postfilter_cb(task, rule)
  978. if (is_rule_applicable(task, rule)) then
  979. rule.selector.postfilter(task, rule, rule.backend)
  980. end
  981. end
  982. local function reputation_idempotent_cb(task, rule)
  983. if (is_rule_applicable(task, rule)) then
  984. rule.selector.idempotent(task, rule, rule.backend)
  985. end
  986. end
  987. local function callback_gen(cb, rule)
  988. return function(task)
  989. if rule.enabled then
  990. cb(task, rule)
  991. end
  992. end
  993. end
  994. local function parse_rule(name, tbl)
  995. local sel_type,sel_conf = fun.head(tbl.selector)
  996. local selector = selectors[sel_type]
  997. if not selector then
  998. rspamd_logger.errx(rspamd_config, "unknown selector defined for rule %s: %s", name,
  999. sel_type)
  1000. return
  1001. end
  1002. local bk_type,bk_conf = fun.head(tbl.backend)
  1003. local backend = backends[bk_type]
  1004. if not backend then
  1005. rspamd_logger.errx(rspamd_config, "unknown backend defined for rule %s: %s", name,
  1006. tbl.backend.type)
  1007. return
  1008. end
  1009. -- Allow config override
  1010. local rule = {
  1011. selector = lua_util.shallowcopy(selector),
  1012. backend = lua_util.shallowcopy(backend),
  1013. config = {}
  1014. }
  1015. -- Override default config params
  1016. rule.backend.config = lua_util.override_defaults(rule.backend.config, bk_conf)
  1017. if backend.schema then
  1018. local checked,schema_err = backend.schema:transform(rule.backend.config)
  1019. if not checked then
  1020. rspamd_logger.errx(rspamd_config, "cannot parse backend config for %s: %s",
  1021. sel_type, schema_err)
  1022. return
  1023. end
  1024. rule.backend.config = checked
  1025. end
  1026. rule.selector.config = lua_util.override_defaults(rule.selector.config, sel_conf)
  1027. if selector.schema then
  1028. local checked,schema_err = selector.schema:transform(rule.selector.config)
  1029. if not checked then
  1030. rspamd_logger.errx(rspamd_config, "cannot parse selector config for %s: %s (%s)",
  1031. sel_type,
  1032. schema_err, sel_conf)
  1033. return
  1034. end
  1035. rule.selector.config = checked
  1036. end
  1037. -- Generic options
  1038. tbl.selector = nil
  1039. tbl.backend = nil
  1040. rule.config = lua_util.override_defaults(rule.config, tbl)
  1041. if rule.config.whitelist then
  1042. if lua_maps_exprs.schema(rule.config.whitelist) then
  1043. rule.config.whitelist_map = lua_maps_exprs.create(rspamd_config,
  1044. rule.config.whitelist, N)
  1045. elseif lua_maps.map_schema(rule.config.whitelist) then
  1046. local map = lua_maps.map_add_from_ucl(rule.config.whitelist,
  1047. 'radix',
  1048. sel_type .. ' reputation whitelist')
  1049. if not map then
  1050. rspamd_logger.errx(rspamd_config, "cannot parse whitelist map config for %s: (%s)",
  1051. sel_type,
  1052. rule.config.whitelist)
  1053. return
  1054. end
  1055. rule.config.whitelist_map = {
  1056. process = function(_, task)
  1057. -- Hack: we assume that it is an ip whitelist :(
  1058. local ip = task:get_from_ip()
  1059. if ip and map:get_key(ip) then return true end
  1060. return false
  1061. end
  1062. }
  1063. else
  1064. rspamd_logger.errx(rspamd_config, "cannot parse whitelist map config for %s: (%s)",
  1065. sel_type,
  1066. rule.config.whitelist)
  1067. return
  1068. end
  1069. end
  1070. local symbol = rule.selector.config.symbol or name
  1071. if tbl.symbol then
  1072. symbol = tbl.symbol
  1073. end
  1074. rule.symbol = symbol
  1075. rule.enabled = true
  1076. if rule.selector.init then
  1077. rule.enabled = false
  1078. end
  1079. if rule.backend.init then
  1080. rule.enabled = false
  1081. end
  1082. -- Perform additional initialization if needed
  1083. rspamd_config:add_on_load(function(cfg, ev_base, worker)
  1084. if rule.selector.init then
  1085. if not rule.selector.init(rule, cfg, ev_base, worker) then
  1086. rule.enabled = false
  1087. rspamd_logger.errx(rspamd_config, 'Cannot init selector %s (backend %s) for symbol %s',
  1088. sel_type, bk_type, rule.symbol)
  1089. else
  1090. rule.enabled = true
  1091. end
  1092. end
  1093. if rule.backend.init then
  1094. if not rule.backend.init(rule, cfg, ev_base, worker) then
  1095. rule.enabled = false
  1096. rspamd_logger.errx(rspamd_config, 'Cannot init backend (%s) for rule %s for symbol %s',
  1097. bk_type, sel_type, rule.symbol)
  1098. else
  1099. rule.enabled = true
  1100. end
  1101. end
  1102. if rule.enabled then
  1103. rspamd_logger.infox(rspamd_config, 'Enable %s (%s backend) rule for symbol %s (split symbols: %s)',
  1104. sel_type, bk_type, rule.symbol,
  1105. rule.selector.config.split_symbols)
  1106. end
  1107. end)
  1108. -- We now generate symbol for checking
  1109. local rule_type = 'normal'
  1110. if rule.selector.config.split_symbols then
  1111. rule_type = 'callback'
  1112. end
  1113. local id = rspamd_config:register_symbol{
  1114. name = rule.symbol,
  1115. type = rule_type,
  1116. callback = callback_gen(reputation_filter_cb, rule),
  1117. augmentations = {string.format("timeout=%f", redis_params.timeout or 0.0)},
  1118. }
  1119. if rule.selector.config.split_symbols then
  1120. rspamd_config:register_symbol{
  1121. name = rule.symbol .. '_HAM',
  1122. type = 'virtual',
  1123. parent = id,
  1124. }
  1125. rspamd_config:register_symbol{
  1126. name = rule.symbol .. '_SPAM',
  1127. type = 'virtual',
  1128. parent = id,
  1129. }
  1130. end
  1131. if rule.selector.dependencies then
  1132. fun.each(function(d)
  1133. rspamd_config:register_dependency(symbol, d)
  1134. end, rule.selector.dependencies)
  1135. end
  1136. if rule.selector.postfilter then
  1137. -- Also register a postfilter
  1138. rspamd_config:register_symbol{
  1139. name = rule.symbol .. '_POST',
  1140. type = 'postfilter',
  1141. flags = 'nostat,explicit_disable,ignore_passthrough',
  1142. callback = callback_gen(reputation_postfilter_cb, rule),
  1143. augmentations = {string.format("timeout=%f", redis_params.timeout or 0.0)},
  1144. }
  1145. end
  1146. if rule.selector.idempotent then
  1147. -- Has also idempotent component (e.g. saving data to the backend)
  1148. rspamd_config:register_symbol{
  1149. name = rule.symbol .. '_IDEMPOTENT',
  1150. type = 'idempotent',
  1151. flags = 'explicit_disable,ignore_passthrough',
  1152. callback = callback_gen(reputation_idempotent_cb, rule),
  1153. augmentations = {string.format("timeout=%f", redis_params.timeout or 0.0)},
  1154. }
  1155. end
  1156. end
  1157. redis_params = lua_redis.parse_redis_server('reputation')
  1158. local opts = rspamd_config:get_all_opt("reputation")
  1159. -- Initialization part
  1160. if not (opts and type(opts) == 'table') then
  1161. rspamd_logger.infox(rspamd_config, 'Module is unconfigured')
  1162. return
  1163. end
  1164. if opts['rules'] then
  1165. for k,v in pairs(opts['rules']) do
  1166. if not ((v or E).selector) then
  1167. rspamd_logger.errx(rspamd_config, "no selector defined for rule %s", k)
  1168. else
  1169. parse_rule(k, v)
  1170. end
  1171. end
  1172. else
  1173. lua_util.disable_module(N, "config")
  1174. end