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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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, ignore rbl rule!',
  208. name)
  209. rbl_section[name] = {}
  210. end
  211. local converted = {
  212. urls = true,
  213. ignore_defaults = true,
  214. }
  215. if wl then
  216. converted.whitelist = wl
  217. end
  218. for k,v in pairs(value) do
  219. local skip = false
  220. -- Rename
  221. if k == 'suffix' then k = 'rbl' end
  222. if k == 'ips' then k = 'returncodes' end
  223. if k == 'bits' then k = 'returnbits' 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] = 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, ignore rbl rule!',
  257. name)
  258. rbl_section[name] = {}
  259. end
  260. local converted = {
  261. emails = true,
  262. ignore_defaults = true,
  263. }
  264. if wl then
  265. converted.whitelist = wl
  266. end
  267. for k,v in pairs(value) do
  268. local skip = false
  269. -- Rename
  270. if k == 'dnsbl' then k = 'rbl' end
  271. if k == 'check_replyto' then k = 'replyto' end
  272. if k == 'hashlen' then k = 'hash_len' end
  273. if k == 'encoding' then k = 'hash_format' end
  274. if k == 'domain_only' then k = 'emails_domainonly' end
  275. if k == 'delimiter' then k = 'emails_delimiter' end
  276. if k == 'skip_body' then
  277. if v then
  278. -- Hack
  279. converted.emails = false
  280. converted.replyto = true
  281. skip = true
  282. end
  283. end
  284. if k == 'expect_ip' then
  285. -- Another stupid hack
  286. if not converted.return_codes then
  287. converted.returncodes = {}
  288. end
  289. local symbol = value.symbol or name
  290. converted.returncodes[symbol] = { v }
  291. skip = true
  292. end
  293. if not skip then
  294. converted[k] = lua_util.deepcopy(v)
  295. end
  296. end
  297. rbl_section[name] = converted
  298. end
  299. end
  300. return function(cfg)
  301. local ret = false
  302. if cfg['metric'] then
  303. for _, v in metric_pairs(cfg.metric) do
  304. cfg = convert_metric(cfg, v)
  305. end
  306. ret = true
  307. end
  308. if cfg.symbols then
  309. for k, v in metric_pairs(cfg.symbols) do
  310. symbol_transform(cfg, k, v)
  311. end
  312. end
  313. check_statistics_sanity()
  314. if not cfg.actions then
  315. logger.errx('no actions defined')
  316. else
  317. -- Perform sanity check for actions
  318. local actions_defs = {'no action', 'no_action', -- In case if that's added
  319. 'greylist', 'add header', 'add_header',
  320. 'rewrite subject', 'rewrite_subject', 'reject'}
  321. if not cfg.actions['no action'] and not cfg.actions['no_action'] and
  322. not cfg.actions['accept'] then
  323. for _,d in ipairs(actions_defs) do
  324. if cfg.actions[d] and type(cfg.actions[d]) == 'number' then
  325. if cfg.actions[d] < 0 then
  326. cfg.actions['no action'] = cfg.actions[d] - 0.001
  327. logger.infox('set no action score to: %s, as action %s has negative score',
  328. cfg.actions['no action'], d)
  329. break
  330. end
  331. end
  332. end
  333. end
  334. local actions_set = {}
  335. for _,d in ipairs(actions_defs) do
  336. actions_set[d] = true
  337. end
  338. -- Now check actions section for garbadge
  339. actions_set['unknown_weight'] = true
  340. actions_set['grow_factor'] = true
  341. actions_set['subject'] = true
  342. for k,_ in pairs(cfg.actions) do
  343. if not actions_set[k] then
  344. logger.warnx('unknown element in actions section: %s', k)
  345. end
  346. end
  347. end
  348. if not cfg.group then
  349. logger.errx('no symbol groups defined')
  350. else
  351. if cfg.group[1] then
  352. -- We need to merge groups
  353. cfg.group = merge_groups(cfg.group)
  354. ret = true
  355. end
  356. test_groups(cfg.group)
  357. end
  358. -- Deal with dkim settings
  359. if not cfg.dkim then
  360. cfg.dkim = {}
  361. else
  362. if cfg.dkim.sign_condition then
  363. -- We have an obsoleted sign condition, so we need to either add dkim_signing and move it
  364. -- there or just move sign condition there...
  365. if not cfg.dkim_signing then
  366. logger.warnx('obsoleted DKIM signing method used, converting it to "dkim_signing" module')
  367. cfg.dkim_signing = {
  368. sign_condition = cfg.dkim.sign_condition
  369. }
  370. else
  371. if not cfg.dkim_signing.sign_condition then
  372. logger.warnx('obsoleted DKIM signing method used, move it to "dkim_signing" module')
  373. cfg.dkim_signing.sign_condition = cfg.dkim.sign_condition
  374. else
  375. logger.warnx('obsoleted DKIM signing method used, ignore it as "dkim_signing" also defines condition!')
  376. end
  377. end
  378. end
  379. end
  380. -- Again: legacy stuff :(
  381. if not cfg.dkim.sign_headers then
  382. local sec = cfg.dkim_signing
  383. if sec and sec[1] then sec = cfg.dkim_signing[1] end
  384. if sec and sec.sign_headers then
  385. cfg.dkim.sign_headers = sec.sign_headers
  386. end
  387. end
  388. if cfg.dkim and cfg.dkim.sign_headers and type(cfg.dkim.sign_headers) == 'table' then
  389. -- Flatten
  390. cfg.dkim.sign_headers = table.concat(cfg.dkim.sign_headers, ':')
  391. end
  392. -- Try to find some obvious issues with configuration
  393. for k,v in pairs(cfg) do
  394. if type(v) == 'table' and v[k] and type (v[k]) == 'table' then
  395. logger.errx('nested section: %s { %s { ... } }, it is likely a configuration error',
  396. k, k)
  397. end
  398. end
  399. -- If neural network is enabled we MUST have `check_all_filters` flag
  400. if cfg.neural then
  401. if not cfg.options then
  402. cfg.options = {}
  403. end
  404. if not cfg.options.check_all_filters then
  405. logger.infox(rspamd_config, 'enable `options.check_all_filters` for neural network')
  406. cfg.options.check_all_filters = true
  407. end
  408. end
  409. -- Deal with IP_SCORE
  410. if cfg.ip_score and (cfg.ip_score.servers or cfg.redis.servers) then
  411. logger.warnx(rspamd_config, 'ip_score module is deprecated in honor of reputation module!')
  412. if not cfg.reputation then
  413. cfg.reputation = {
  414. rules = {}
  415. }
  416. end
  417. if not cfg.reputation.rules then cfg.reputation.rules = {} end
  418. if not fun.any(function(_, v) return v.selector and v.selector.ip end,
  419. cfg.reputation.rules) then
  420. logger.infox(rspamd_config, 'attach ip reputation element to use it')
  421. cfg.reputation.rules.ip_score = {
  422. selector = {
  423. ip = {},
  424. },
  425. backend = {
  426. redis = {},
  427. }
  428. }
  429. if cfg.ip_score.servers then
  430. cfg.reputation.rules.ip_score.backend.redis.servers = cfg.ip_score.servers
  431. end
  432. if cfg.symbols and cfg.symbols['IP_SCORE'] then
  433. local t = cfg.symbols['IP_SCORE']
  434. if not cfg.symbols['SENDER_REP_SPAM'] then
  435. cfg.symbols['SENDER_REP_SPAM'] = t
  436. cfg.symbols['SENDER_REP_HAM'] = t
  437. cfg.symbols['SENDER_REP_HAM'].weight = -(t.weight or 0)
  438. end
  439. end
  440. else
  441. logger.infox(rspamd_config, 'ip reputation already exists, do not do any IP_SCORE transforms')
  442. end
  443. end
  444. if cfg.surbl then
  445. if not cfg.rbl then
  446. cfg.rbl = {
  447. rbls = {}
  448. }
  449. end
  450. if not cfg.rbl.rbls then
  451. cfg.rbl.rbls = {}
  452. end
  453. surbl_section_convert(cfg, cfg.surbl)
  454. logger.infox(rspamd_config, 'converted surbl rules to rbl rules')
  455. cfg.surbl = {}
  456. end
  457. if cfg.emails then
  458. if not cfg.rbl then
  459. cfg.rbl = {
  460. rbls = {}
  461. }
  462. end
  463. if not cfg.rbl.rbls then
  464. cfg.rbl.rbls = {}
  465. end
  466. emails_section_convert(cfg, cfg.emails)
  467. logger.infox(rspamd_config, 'converted emails rules to rbl rules')
  468. cfg.emails = {}
  469. end
  470. return ret, cfg
  471. end