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

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