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 17KB

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