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.

lua_cfg_transform.lua 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. --[[
  2. Copyright (c) 2017, Vsevolod Stakhov <vsevolod@highsecure.ru>
  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. local logger = require "rspamd_logger"
  14. local lua_util = require "lua_util"
  15. local rspamd_util = require "rspamd_util"
  16. local fun = require "fun"
  17. local function is_implicit(t)
  18. local mt = getmetatable(t)
  19. return mt and mt.class and mt.class == 'ucl.type.impl_array'
  20. end
  21. local function metric_pairs(t)
  22. -- collect the keys
  23. local keys = {}
  24. local implicit_array = is_implicit(t)
  25. local function gen_keys(tbl)
  26. if implicit_array then
  27. for _,v in ipairs(tbl) do
  28. if v.name then
  29. table.insert(keys, {v.name, v})
  30. v.name = nil
  31. else
  32. -- Very tricky to distinguish:
  33. -- group {name = "foo" ... } + group "blah" { ... }
  34. for gr_name,gr in pairs(v) do
  35. if type(gr_name) ~= 'number' then
  36. -- We can also have implicit arrays here
  37. local gr_implicit = is_implicit(gr)
  38. if gr_implicit then
  39. for _,gr_elt in ipairs(gr) do
  40. table.insert(keys, {gr_name, gr_elt})
  41. end
  42. else
  43. table.insert(keys, {gr_name, gr})
  44. end
  45. end
  46. end
  47. end
  48. end
  49. else
  50. if tbl.name then
  51. table.insert(keys, {tbl.name, tbl})
  52. tbl.name = nil
  53. else
  54. for k,v in pairs(tbl) do
  55. if type(k) ~= 'number' then
  56. -- We can also have implicit arrays here
  57. local sym_implicit = is_implicit(v)
  58. if sym_implicit then
  59. for _,elt in ipairs(v) do
  60. table.insert(keys, {k, elt})
  61. end
  62. else
  63. table.insert(keys, {k, v})
  64. end
  65. end
  66. end
  67. end
  68. end
  69. end
  70. gen_keys(t)
  71. -- return the iterator function
  72. local i = 0
  73. return function()
  74. i = i + 1
  75. if keys[i] then
  76. return keys[i][1], keys[i][2]
  77. end
  78. end
  79. end
  80. local function group_transform(cfg, k, v)
  81. if v.name then k = v.name end
  82. local new_group = {
  83. symbols = {}
  84. }
  85. if v.enabled then new_group.enabled = v.enabled end
  86. if v.disabled then new_group.disabled = v.disabled end
  87. if v.max_score then new_group.max_score = v.max_score end
  88. if v.symbol then
  89. for sk,sv in metric_pairs(v.symbol) do
  90. if sv.name then
  91. sk = sv.name
  92. sv.name = nil -- Remove field
  93. end
  94. new_group.symbols[sk] = sv
  95. end
  96. end
  97. if not cfg.group then cfg.group = {} end
  98. if cfg.group[k] then
  99. cfg.group[k] = lua_util.override_defaults(cfg.group[k], new_group)
  100. else
  101. cfg.group[k] = new_group
  102. end
  103. logger.infox("overriding group %s from the legacy metric settings", k)
  104. end
  105. local function symbol_transform(cfg, k, v)
  106. -- first try to find any group where there is a definition of this symbol
  107. for gr_n, gr in pairs(cfg.group) do
  108. if gr.symbols and gr.symbols[k] then
  109. -- We override group symbol with ungrouped symbol
  110. logger.infox("overriding group symbol %s in the group %s", k, gr_n)
  111. gr.symbols[k] = lua_util.override_defaults(gr.symbols[k], v)
  112. return
  113. end
  114. end
  115. -- Now check what Rspamd knows about this symbol
  116. local sym = rspamd_config:get_metric_symbol(k)
  117. if not sym or not sym.group then
  118. -- Otherwise we just use group 'ungrouped'
  119. if not cfg.group.ungrouped then
  120. cfg.group.ungrouped = {
  121. symbols = {}
  122. }
  123. end
  124. cfg.group.ungrouped.symbols[k] = v
  125. logger.debugx("adding symbol %s to the group 'ungrouped'", k)
  126. end
  127. end
  128. local function test_groups(groups)
  129. for gr_name, gr in pairs(groups) do
  130. if not gr.symbols then
  131. local cnt = 0
  132. for _,_ in pairs(gr) do cnt = cnt + 1 end
  133. if cnt == 0 then
  134. logger.debugx('group %s is empty', gr_name)
  135. else
  136. logger.infox('group %s has no symbols', gr_name)
  137. end
  138. end
  139. end
  140. end
  141. local function convert_metric(cfg, metric)
  142. if metric.actions then
  143. cfg.actions = lua_util.override_defaults(cfg.actions, metric.actions)
  144. logger.infox("overriding actions from the legacy metric settings")
  145. end
  146. if metric.unknown_weight then
  147. cfg.actions.unknown_weight = metric.unknown_weight
  148. end
  149. if metric.subject then
  150. logger.infox("overriding subject from the legacy metric settings")
  151. cfg.actions.subject = metric.subject
  152. end
  153. if metric.group then
  154. for k, v in metric_pairs(metric.group) do
  155. group_transform(cfg, k, v)
  156. end
  157. else
  158. if not cfg.group then
  159. cfg.group = {
  160. ungrouped = {
  161. symbols = {}
  162. }
  163. }
  164. end
  165. end
  166. if metric.symbol then
  167. for k, v in metric_pairs(metric.symbol) do
  168. symbol_transform(cfg, k, v)
  169. end
  170. end
  171. return cfg
  172. end
  173. -- Converts a table of groups indexed by number (implicit array) to a
  174. -- merged group definition
  175. local function merge_groups(groups)
  176. local ret = {}
  177. for k,gr in pairs(groups) do
  178. if type(k) == 'number' then
  179. for key,sec in pairs(gr) do
  180. ret[key] = sec
  181. end
  182. else
  183. ret[k] = gr
  184. end
  185. end
  186. return ret
  187. end
  188. -- Checks configuration files for statistics
  189. local function check_statistics_sanity()
  190. local local_conf = rspamd_paths['LOCAL_CONFDIR']
  191. local local_stat = string.format('%s/local.d/%s', local_conf,
  192. 'statistic.conf')
  193. local local_bayes = string.format('%s/local.d/%s', local_conf,
  194. 'classifier-bayes.conf')
  195. if rspamd_util.file_exists(local_stat) and
  196. rspamd_util.file_exists(local_bayes) then
  197. logger.warnx(rspamd_config, 'conflicting files %s and %s are found: '..
  198. 'Rspamd classifier configuration might be broken!', local_stat, local_bayes)
  199. end
  200. end
  201. -- Converts surbl module config to rbl module
  202. local function surbl_section_convert(cfg, section)
  203. local rbl_section = cfg.rbl.rbls
  204. local wl = section.whitelist
  205. for name,value in pairs(section.rules or {}) do
  206. if rbl_section[name] then
  207. logger.warnx(rspamd_config, 'conflicting names in surbl and rbl rules: %s, prefer surbl rule!',
  208. name)
  209. end
  210. local converted = {
  211. urls = true,
  212. ignore_defaults = true,
  213. }
  214. if wl then
  215. converted.whitelist = wl
  216. end
  217. for k,v in pairs(value) do
  218. local skip = false
  219. -- Rename
  220. if k == 'suffix' then k = 'rbl' end
  221. if k == 'ips' then k = 'returncodes' end
  222. if k == 'bits' then k = 'returnbits' end
  223. if k == 'noip' then k = 'no_ip' end
  224. -- Crappy legacy
  225. if k == 'options' then
  226. if v == 'noip' or v == 'no_ip' then
  227. converted.no_ip = true
  228. skip = true
  229. end
  230. end
  231. if k:match('check_') then
  232. local n = k:match('check_(.*)')
  233. k = n
  234. end
  235. if k == 'dkim' and v then
  236. converted.dkim_domainonly = false
  237. converted.dkim_match_from = true
  238. end
  239. if k == 'emails' and v then
  240. -- To match surbl behaviour
  241. converted.emails_domainonly = true
  242. end
  243. if not skip then
  244. converted[k] = lua_util.deepcopy(v)
  245. end
  246. end
  247. rbl_section[name] = lua_util.override_defaults(rbl_section[name], converted)
  248. end
  249. end
  250. -- Converts surbl module config to rbl module
  251. local function emails_section_convert(cfg, section)
  252. local rbl_section = cfg.rbl.rbls
  253. local wl = section.whitelist
  254. for name,value in pairs(section.rules or {}) do
  255. if rbl_section[name] then
  256. logger.warnx(rspamd_config, 'conflicting names in emails and rbl rules: %s, prefer emails rule!',
  257. name)
  258. end
  259. local converted = {
  260. emails = true,
  261. ignore_defaults = true,
  262. }
  263. if wl then
  264. converted.whitelist = wl
  265. end
  266. for k,v in pairs(value) do
  267. local skip = false
  268. -- Rename
  269. if k == 'dnsbl' then k = 'rbl' end
  270. if k == 'check_replyto' then k = 'replyto' end
  271. if k == 'hashlen' then k = 'hash_len' end
  272. if k == 'encoding' then k = 'hash_format' end
  273. if k == 'domain_only' then k = 'emails_domainonly' end
  274. if k == 'delimiter' then k = 'emails_delimiter' end
  275. if k == 'skip_body' then
  276. skip = true
  277. if v then
  278. -- Hack
  279. converted.emails = false
  280. converted.replyto = true
  281. else
  282. converted.emails = true
  283. end
  284. end
  285. if k == 'expect_ip' then
  286. -- Another stupid hack
  287. if not converted.return_codes then
  288. converted.returncodes = {}
  289. end
  290. local symbol = value.symbol or name
  291. converted.returncodes[symbol] = { v }
  292. skip = true
  293. end
  294. if not skip then
  295. converted[k] = lua_util.deepcopy(v)
  296. end
  297. end
  298. rbl_section[name] = lua_util.override_defaults(rbl_section[name], converted)
  299. end
  300. end
  301. return function(cfg)
  302. local ret = false
  303. if cfg['metric'] then
  304. for _, v in metric_pairs(cfg.metric) do
  305. cfg = convert_metric(cfg, v)
  306. end
  307. ret = true
  308. end
  309. if cfg.symbols then
  310. for k, v in metric_pairs(cfg.symbols) do
  311. symbol_transform(cfg, k, v)
  312. end
  313. end
  314. check_statistics_sanity()
  315. if not cfg.actions then
  316. logger.errx('no actions defined')
  317. else
  318. -- Perform sanity check for actions
  319. local actions_defs = {'no action', 'no_action', -- In case if that's added
  320. 'greylist', 'add header', 'add_header',
  321. 'rewrite subject', 'rewrite_subject', 'quarantine',
  322. 'reject', 'discard'}
  323. if not cfg.actions['no action'] and not cfg.actions['no_action'] and
  324. not cfg.actions['accept'] then
  325. for _,d in ipairs(actions_defs) do
  326. if cfg.actions[d] then
  327. local action_score = nil
  328. if type(cfg.actions[d]) == 'number' then
  329. action_score = cfg.actions[d]
  330. elseif type(cfg.actions[d]) == 'table' and cfg.actions[d]['score'] then
  331. action_score = cfg.actions[d]['score']
  332. end
  333. if type(cfg.actions[d]) ~= 'table' and not action_score then
  334. cfg.actions[d] = nil
  335. elseif type(action_score) == 'number' and action_score < 0 then
  336. cfg.actions['no_action'] = cfg.actions[d] - 0.001
  337. logger.infox(rspamd_config, 'set no_action score to: %s, as action %s has negative score',
  338. cfg.actions['no_action'], d)
  339. break
  340. end
  341. end
  342. end
  343. end
  344. local actions_set = lua_util.list_to_hash(actions_defs)
  345. -- Now check actions section for garbadge
  346. actions_set['unknown_weight'] = true
  347. actions_set['grow_factor'] = true
  348. actions_set['subject'] = true
  349. for k,_ in pairs(cfg.actions) do
  350. if not actions_set[k] then
  351. logger.warnx(rspamd_config, 'unknown element in actions section: %s', k)
  352. end
  353. end
  354. -- Performs thresholds sanity
  355. -- We exclude greylist here as it can be set to whatever threshold in practice
  356. local actions_order = {
  357. 'no_action',
  358. 'add_header',
  359. 'rewrite_subject',
  360. 'quarantine',
  361. 'reject',
  362. 'discard'
  363. }
  364. for i=1,(#actions_order - 1) do
  365. local act = actions_order[i]
  366. if cfg.actions[act] and type(cfg.actions[act]) == 'number' then
  367. local score = cfg.actions[act]
  368. for j=i+1,#actions_order do
  369. local next_act = actions_order[j]
  370. if cfg.actions[next_act] and type(cfg.actions[next_act]) == 'number' then
  371. local next_score = cfg.actions[next_act]
  372. if next_score <= score then
  373. logger.errx(rspamd_config, 'invalid actions thresholds order: action %s (%s) must have lower '..
  374. 'score than action %s (%s)', act, score, next_act, next_score)
  375. ret = false
  376. end
  377. end
  378. end
  379. end
  380. end
  381. end
  382. if not cfg.group then
  383. logger.errx('no symbol groups defined')
  384. else
  385. if cfg.group[1] then
  386. -- We need to merge groups
  387. cfg.group = merge_groups(cfg.group)
  388. ret = true
  389. end
  390. test_groups(cfg.group)
  391. end
  392. -- Deal with dkim settings
  393. if not cfg.dkim then
  394. cfg.dkim = {}
  395. else
  396. if cfg.dkim.sign_condition then
  397. -- We have an obsoleted sign condition, so we need to either add dkim_signing and move it
  398. -- there or just move sign condition there...
  399. if not cfg.dkim_signing then
  400. logger.warnx('obsoleted DKIM signing method used, converting it to "dkim_signing" module')
  401. cfg.dkim_signing = {
  402. sign_condition = cfg.dkim.sign_condition
  403. }
  404. else
  405. if not cfg.dkim_signing.sign_condition then
  406. logger.warnx('obsoleted DKIM signing method used, move it to "dkim_signing" module')
  407. cfg.dkim_signing.sign_condition = cfg.dkim.sign_condition
  408. else
  409. logger.warnx('obsoleted DKIM signing method used, ignore it as "dkim_signing" also defines condition!')
  410. end
  411. end
  412. end
  413. end
  414. -- Again: legacy stuff :(
  415. if not cfg.dkim.sign_headers then
  416. local sec = cfg.dkim_signing
  417. if sec and sec[1] then sec = cfg.dkim_signing[1] end
  418. if sec and sec.sign_headers then
  419. cfg.dkim.sign_headers = sec.sign_headers
  420. end
  421. end
  422. -- DKIM signing/ARC legacy
  423. for _, mod in ipairs({'dkim_signing', 'arc'}) do
  424. if cfg[mod] then
  425. if cfg[mod].auth_only ~= nil then
  426. if cfg[mod].sign_authenticated ~= nil then
  427. logger.warnx(rspamd_config,
  428. 'both auth_only (%s) and sign_authenticated (%s) for %s are specified, prefer auth_only',
  429. cfg[mod].auth_only, cfg[mod].sign_authenticated, mod)
  430. end
  431. cfg[mod].sign_authenticated = cfg[mod].auth_only
  432. end
  433. end
  434. end
  435. if cfg.dkim and cfg.dkim.sign_headers and type(cfg.dkim.sign_headers) == 'table' then
  436. -- Flatten
  437. cfg.dkim.sign_headers = table.concat(cfg.dkim.sign_headers, ':')
  438. end
  439. -- Try to find some obvious issues with configuration
  440. for k,v in pairs(cfg) do
  441. if type(v) == 'table' and v[k] and type (v[k]) == 'table' then
  442. logger.errx('nested section: %s { %s { ... } }, it is likely a configuration error',
  443. k, k)
  444. end
  445. end
  446. -- If neural network is enabled we MUST have `check_all_filters` flag
  447. if cfg.neural then
  448. if not cfg.options then
  449. cfg.options = {}
  450. end
  451. if not cfg.options.check_all_filters then
  452. logger.infox(rspamd_config, 'enable `options.check_all_filters` for neural network')
  453. cfg.options.check_all_filters = true
  454. end
  455. end
  456. -- Deal with IP_SCORE
  457. if cfg.ip_score and (cfg.ip_score.servers or cfg.redis.servers) then
  458. logger.warnx(rspamd_config, 'ip_score module is deprecated in honor of reputation module!')
  459. if not cfg.reputation then
  460. cfg.reputation = {
  461. rules = {}
  462. }
  463. end
  464. if not cfg.reputation.rules then cfg.reputation.rules = {} end
  465. if not fun.any(function(_, v) return v.selector and v.selector.ip end,
  466. cfg.reputation.rules) then
  467. logger.infox(rspamd_config, 'attach ip reputation element to use it')
  468. cfg.reputation.rules.ip_score = {
  469. selector = {
  470. ip = {},
  471. },
  472. backend = {
  473. redis = {},
  474. }
  475. }
  476. if cfg.ip_score.servers then
  477. cfg.reputation.rules.ip_score.backend.redis.servers = cfg.ip_score.servers
  478. end
  479. if cfg.symbols and cfg.symbols['IP_SCORE'] then
  480. local t = cfg.symbols['IP_SCORE']
  481. if not cfg.symbols['SENDER_REP_SPAM'] then
  482. cfg.symbols['SENDER_REP_SPAM'] = t
  483. cfg.symbols['SENDER_REP_HAM'] = t
  484. cfg.symbols['SENDER_REP_HAM'].weight = -(t.weight or 0)
  485. end
  486. end
  487. else
  488. logger.infox(rspamd_config, 'ip reputation already exists, do not do any IP_SCORE transforms')
  489. end
  490. end
  491. if cfg.surbl then
  492. if not cfg.rbl then
  493. cfg.rbl = {
  494. rbls = {}
  495. }
  496. end
  497. if not cfg.rbl.rbls then
  498. cfg.rbl.rbls = {}
  499. end
  500. surbl_section_convert(cfg, cfg.surbl)
  501. logger.infox(rspamd_config, 'converted surbl rules to rbl rules')
  502. cfg.surbl = {}
  503. end
  504. if cfg.emails then
  505. if not cfg.rbl then
  506. cfg.rbl = {
  507. rbls = {}
  508. }
  509. end
  510. if not cfg.rbl.rbls then
  511. cfg.rbl.rbls = {}
  512. end
  513. emails_section_convert(cfg, cfg.emails)
  514. logger.infox(rspamd_config, 'converted emails rules to rbl rules')
  515. cfg.emails = {}
  516. end
  517. return ret, cfg
  518. end