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

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