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.

metadata_exporter.lua 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. --[[
  2. Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
  3. Copyright (c) 2016, Vsevolod Stakhov <vsevolod@highsecure.ru>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]]--
  14. if confighelp then
  15. return
  16. end
  17. -- A plugin that pushes metadata (or whole messages) to external services
  18. local redis_params
  19. local lua_util = require "lua_util"
  20. local rspamd_http = require "rspamd_http"
  21. local rspamd_util = require "rspamd_util"
  22. local rspamd_logger = require "rspamd_logger"
  23. local ucl = require "ucl"
  24. local E = {}
  25. local N = 'metadata_exporter'
  26. local settings = {
  27. pusher_enabled = {},
  28. pusher_format = {},
  29. pusher_select = {},
  30. mime_type = 'text/plain',
  31. defer = false,
  32. mail_from = '',
  33. mail_to = 'postmaster@localhost',
  34. helo = 'rspamd',
  35. email_template = [[From: "Rspamd" <$mail_from>
  36. To: $mail_to
  37. Subject: Spam alert
  38. Date: $date
  39. MIME-Version: 1.0
  40. Message-ID: <$our_message_id>
  41. Content-type: text/plain; charset=utf-8
  42. Content-Transfer-Encoding: 8bit
  43. Authenticated username: $user
  44. IP: $ip
  45. Queue ID: $qid
  46. SMTP FROM: $from
  47. SMTP RCPT: $rcpt
  48. MIME From: $header_from
  49. MIME To: $header_to
  50. MIME Date: $header_date
  51. Subject: $header_subject
  52. Message-ID: $message_id
  53. Action: $action
  54. Score: $score
  55. Symbols: $symbols]],
  56. }
  57. local function get_general_metadata(task, flatten, no_content)
  58. local r = {}
  59. local ip = task:get_from_ip()
  60. if ip and ip:is_valid() then
  61. r.ip = tostring(ip)
  62. else
  63. r.ip = 'unknown'
  64. end
  65. r.user = task:get_user() or 'unknown'
  66. r.qid = task:get_queue_id() or 'unknown'
  67. r.subject = task:get_subject() or 'unknown'
  68. r.action = task:get_metric_action('default')
  69. local s = task:get_metric_score('default')[1]
  70. r.score = flatten and string.format('%.2f', s) or s
  71. local fuzzy = task:get_mempool():get_variable("fuzzy_hashes", "fstrings")
  72. if fuzzy and #fuzzy > 0 then
  73. local fz = {}
  74. for _,h in ipairs(fuzzy) do
  75. table.insert(fz, h)
  76. end
  77. if not flatten then
  78. r.fuzzy = fz
  79. else
  80. r.fuzzy = table.concat(fz, ', ')
  81. end
  82. else
  83. r.fuzzy = 'unknown'
  84. end
  85. local rcpt = task:get_recipients('smtp')
  86. if rcpt then
  87. local l = {}
  88. for _, a in ipairs(rcpt) do
  89. table.insert(l, a['addr'])
  90. end
  91. if not flatten then
  92. r.rcpt = l
  93. else
  94. r.rcpt = table.concat(l, ', ')
  95. end
  96. else
  97. r.rcpt = 'unknown'
  98. end
  99. local from = task:get_from('smtp')
  100. if ((from or E)[1] or E).addr then
  101. r.from = from[1].addr
  102. else
  103. r.from = 'unknown'
  104. end
  105. local syminf = task:get_symbols_all()
  106. if flatten then
  107. local l = {}
  108. for _, sym in ipairs(syminf) do
  109. local txt
  110. if sym.options then
  111. local topt = table.concat(sym.options, ', ')
  112. txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')' .. ' [' .. topt .. ']'
  113. else
  114. txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')'
  115. end
  116. table.insert(l, txt)
  117. end
  118. r.symbols = table.concat(l, '\n\t')
  119. else
  120. r.symbols = syminf
  121. end
  122. local function process_header(name)
  123. local hdr = task:get_header_full(name)
  124. if hdr then
  125. local l = {}
  126. for _, h in ipairs(hdr) do
  127. table.insert(l, h.decoded)
  128. end
  129. if not flatten then
  130. return l
  131. else
  132. return table.concat(l, '\n')
  133. end
  134. else
  135. return 'unknown'
  136. end
  137. end
  138. if not no_content then
  139. r.header_from = process_header('from')
  140. r.header_to = process_header('to')
  141. r.header_subject = process_header('subject')
  142. r.header_date = process_header('date')
  143. r.message_id = task:get_message_id()
  144. end
  145. return r
  146. end
  147. local formatters = {
  148. default = function(task)
  149. return task:get_content(), {}
  150. end,
  151. email_alert = function(task, rule, extra)
  152. local meta = get_general_metadata(task, true)
  153. local display_emails = {}
  154. local mail_targets = {}
  155. meta.mail_from = rule.mail_from or settings.mail_from
  156. local mail_rcpt = rule.mail_to or settings.mail_to
  157. if type(mail_rcpt) ~= 'table' then
  158. table.insert(display_emails, string.format('<%s>', mail_rcpt))
  159. table.insert(mail_targets, mail_rcpt)
  160. else
  161. for _, e in ipairs(mail_rcpt) do
  162. table.insert(display_emails, string.format('<%s>', e))
  163. table.insert(mail_targets, mail_rcpt)
  164. end
  165. end
  166. if rule.email_alert_sender then
  167. local x = task:get_from('smtp')
  168. if x and string.len(x[1].addr) > 0 then
  169. table.insert(mail_targets, x)
  170. table.insert(display_emails, string.format('<%s>', x[1].addr))
  171. end
  172. end
  173. if rule.email_alert_user then
  174. local x = task:get_user()
  175. if x then
  176. table.insert(mail_targets, x)
  177. table.insert(display_emails, string.format('<%s>', x))
  178. end
  179. end
  180. if rule.email_alert_recipients then
  181. local x = task:get_recipients('smtp')
  182. if x then
  183. for _, e in ipairs(x) do
  184. if string.len(e.addr) > 0 then
  185. table.insert(mail_targets, e.addr)
  186. table.insert(display_emails, string.format('<%s>', e.addr))
  187. end
  188. end
  189. end
  190. end
  191. meta.mail_to = table.concat(display_emails, ', ')
  192. meta.our_message_id = rspamd_util.random_hex(12) .. '@rspamd'
  193. meta.date = rspamd_util.time_to_string(rspamd_util.get_time())
  194. return lua_util.template(rule.email_template or settings.email_template, meta), { mail_targets = mail_targets}
  195. end,
  196. json = function(task)
  197. return ucl.to_format(get_general_metadata(task), 'json-compact')
  198. end
  199. }
  200. local function is_spam(action)
  201. return (action == 'reject' or action == 'add header' or action == 'rewrite subject')
  202. end
  203. local selectors = {
  204. default = function(task)
  205. return true
  206. end,
  207. is_spam = function(task)
  208. local action = task:get_metric_action('default')
  209. return is_spam(action)
  210. end,
  211. is_spam_authed = function(task)
  212. if not task:get_user() then
  213. return false
  214. end
  215. local action = task:get_metric_action('default')
  216. return is_spam(action)
  217. end,
  218. is_reject = function(task)
  219. local action = task:get_metric_action('default')
  220. return (action == 'reject')
  221. end,
  222. is_reject_authed = function(task)
  223. if not task:get_user() then
  224. return false
  225. end
  226. local action = task:get_metric_action('default')
  227. return (action == 'reject')
  228. end,
  229. }
  230. local function maybe_defer(task, rule)
  231. if rule.defer then
  232. rspamd_logger.warnx(task, 'deferring message')
  233. task:set_pre_result('soft reject', 'deferred', N)
  234. end
  235. end
  236. local pushers = {
  237. redis_pubsub = function(task, formatted, rule)
  238. local _,ret,upstream
  239. local function redis_pub_cb(err)
  240. if err then
  241. rspamd_logger.errx(task, 'got error %s when publishing on server %s',
  242. err, upstream:get_addr())
  243. return maybe_defer(task, rule)
  244. end
  245. return true
  246. end
  247. ret,_,upstream = rspamd_redis_make_request(task,
  248. redis_params, -- connect params
  249. nil, -- hash key
  250. true, -- is write
  251. redis_pub_cb, --callback
  252. 'PUBLISH', -- command
  253. {rule.channel, formatted} -- arguments
  254. )
  255. if not ret then
  256. rspamd_logger.errx(task, 'error connecting to redis')
  257. maybe_defer(task, rule)
  258. end
  259. end,
  260. http = function(task, formatted, rule)
  261. local function http_callback(err, code)
  262. if err then
  263. rspamd_logger.errx(task, 'got error %s in http callback', err)
  264. return maybe_defer(task, rule)
  265. end
  266. if code ~= 200 then
  267. rspamd_logger.errx(task, 'got unexpected http status: %s', code)
  268. return maybe_defer(task, rule)
  269. end
  270. return true
  271. end
  272. local hdrs = {}
  273. if rule.meta_headers then
  274. local gm = get_general_metadata(task, false, true)
  275. local pfx = rule.meta_header_prefix or 'X-Rspamd-'
  276. for k, v in pairs(gm) do
  277. if type(v) == 'table' then
  278. hdrs[pfx .. k] = ucl.to_format(v, 'json-compact')
  279. else
  280. hdrs[pfx .. k] = v
  281. end
  282. end
  283. end
  284. rspamd_http.request({
  285. task=task,
  286. url=rule.url,
  287. body=formatted,
  288. callback=http_callback,
  289. mime_type=rule.mime_type or settings.mime_type,
  290. headers=hdrs,
  291. })
  292. end,
  293. send_mail = function(task, formatted, rule, extra)
  294. local lua_smtp = require "lua_smtp"
  295. local function sendmail_cb(ret, err)
  296. if not ret then
  297. rspamd_logger.errx(task, 'SMTP export error: %s', err)
  298. maybe_defer(task, rule)
  299. end
  300. end
  301. lua_smtp.sendmail({
  302. task = task,
  303. host = rule.smtp,
  304. port = rule.smtp_port or settings.smtp_port or 25,
  305. from = rule.mail_from or settings.mail_from,
  306. recipients = extra.mail_targets or rule.mail_to or settings.mail_to,
  307. helo = rule.helo or settings.helo,
  308. timeout = rule.timeout or settings.timeout,
  309. }, formatted, sendmail_cb)
  310. end,
  311. }
  312. local opts = rspamd_config:get_all_opt(N)
  313. if not opts then return end
  314. local process_settings = {
  315. select = function(val)
  316. selectors.custom = assert(load(val))()
  317. end,
  318. format = function(val)
  319. formatters.custom = assert(load(val))()
  320. end,
  321. push = function(val)
  322. pushers.custom = assert(load(val))()
  323. end,
  324. custom_push = function(val)
  325. if type(val) == 'table' then
  326. for k, v in pairs(val) do
  327. pushers[k] = assert(load(v))()
  328. end
  329. end
  330. end,
  331. custom_select = function(val)
  332. if type(val) == 'table' then
  333. for k, v in pairs(val) do
  334. selectors[k] = assert(load(v))()
  335. end
  336. end
  337. end,
  338. custom_format = function(val)
  339. if type(val) == 'table' then
  340. for k, v in pairs(val) do
  341. formatters[k] = assert(load(v))()
  342. end
  343. end
  344. end,
  345. pusher_enabled = function(val)
  346. if type(val) == 'string' then
  347. if pushers[val] then
  348. settings.pusher_enabled[val] = true
  349. else
  350. rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
  351. end
  352. elseif type(val) == 'table' then
  353. for _, v in ipairs(val) do
  354. if pushers[v] then
  355. settings.pusher_enabled[v] = true
  356. else
  357. rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
  358. end
  359. end
  360. end
  361. end,
  362. }
  363. for k, v in pairs(opts) do
  364. local f = process_settings[k]
  365. if f then
  366. f(opts[k])
  367. else
  368. settings[k] = v
  369. end
  370. end
  371. if type(settings.rules) ~= 'table' then
  372. -- Legacy config
  373. settings.rules = {}
  374. if not next(settings.pusher_enabled) then
  375. if pushers.custom then
  376. rspamd_logger.infox(rspamd_config, 'Custom pusher implicitly enabled')
  377. settings.pusher_enabled.custom = true
  378. else
  379. -- Check legacy options
  380. if settings.url then
  381. rspamd_logger.warnx(rspamd_config, 'HTTP pusher implicitly enabled')
  382. settings.pusher_enabled.http = true
  383. end
  384. if settings.channel then
  385. rspamd_logger.warnx(rspamd_config, 'Redis Pubsub pusher implicitly enabled')
  386. settings.pusher_enabled.redis_pubsub = true
  387. end
  388. if settings.smtp and settings.mail_to then
  389. rspamd_logger.warnx(rspamd_config, 'SMTP pusher implicitly enabled')
  390. settings.pusher_enabled.send_mail = true
  391. end
  392. end
  393. end
  394. if not next(settings.pusher_enabled) then
  395. rspamd_logger.errx(rspamd_config, 'No push backend enabled')
  396. return
  397. end
  398. if settings.formatter then
  399. settings.format = formatters[settings.formatter]
  400. if not settings.format then
  401. rspamd_logger.errx(rspamd_config, 'No such formatter: %s', settings.formatter)
  402. return
  403. end
  404. end
  405. if settings.selector then
  406. settings.select = selectors[settings.selector]
  407. if not settings.select then
  408. rspamd_logger.errx(rspamd_config, 'No such selector: %s', settings.selector)
  409. return
  410. end
  411. end
  412. for k in pairs(settings.pusher_enabled) do
  413. local formatter = settings.pusher_format[k]
  414. local selector = settings.pusher_select[k]
  415. if not formatter then
  416. settings.pusher_format[k] = settings.formatter or 'default'
  417. rspamd_logger.infox(rspamd_config, 'Using default formatter for %s pusher', k)
  418. else
  419. if not formatters[formatter] then
  420. rspamd_logger.errx(rspamd_config, 'No such formatter: %s - disabling %s', formatter, k)
  421. settings.pusher_enabled.k = nil
  422. end
  423. end
  424. if not selector then
  425. settings.pusher_select[k] = settings.selector or 'default'
  426. rspamd_logger.infox(rspamd_config, 'Using default selector for %s pusher', k)
  427. else
  428. if not selectors[selector] then
  429. rspamd_logger.errx(rspamd_config, 'No such selector: %s - disabling %s', selector, k)
  430. settings.pusher_enabled.k = nil
  431. end
  432. end
  433. end
  434. if settings.pusher_enabled.redis_pubsub then
  435. redis_params = rspamd_parse_redis_server(N)
  436. if not redis_params then
  437. rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
  438. settings.pusher_enabled.redis_pubsub = nil
  439. else
  440. local r = {}
  441. r.backend = 'redis_pubsub'
  442. r.channel = settings.channel
  443. r.defer = settings.defer
  444. r.selector = settings.pusher_select.redis_pubsub
  445. r.formatter = settings.pusher_format.redis_pubsub
  446. settings.rules[r.backend:upper()] = r
  447. end
  448. end
  449. if settings.pusher_enabled.http then
  450. if not settings.url then
  451. rspamd_logger.errx(rspamd_config, 'No URL is specified')
  452. settings.pusher_enabled.http = nil
  453. else
  454. local r = {}
  455. r.backend = 'http'
  456. r.url = settings.url
  457. r.mime_type = settings.mime_type
  458. r.defer = settings.defer
  459. r.selector = settings.pusher_select.http
  460. r.formatter = settings.pusher_format.http
  461. settings.rules[r.backend:upper()] = r
  462. end
  463. end
  464. if settings.pusher_enabled.send_mail then
  465. if not (settings.mail_to and settings.smtp) then
  466. rspamd_logger.errx(rspamd_config, 'No mail_to and/or smtp setting is specified')
  467. settings.pusher_enabled.send_mail = nil
  468. else
  469. local r = {}
  470. r.backend = 'send_mail'
  471. r.mail_to = settings.mail_to
  472. r.mail_from = settings.mail_from
  473. r.helo = settings.hello
  474. r.smtp = settings.smtp
  475. r.smtp_port = settings.smtp_port
  476. r.email_template = settings.email_template
  477. r.defer = settings.defer
  478. r.selector = settings.pusher_select.send_mail
  479. r.formatter = settings.pusher_format.send_mail
  480. settings.rules[r.backend:upper()] = r
  481. end
  482. end
  483. if not next(settings.pusher_enabled) then
  484. rspamd_logger.errx(rspamd_config, 'No push backend enabled')
  485. return
  486. end
  487. elseif not next(settings.rules) then
  488. lua_util.debugm(N, rspamd_config, 'No rules enabled')
  489. return
  490. end
  491. if not settings.rules or not next(settings.rules) then
  492. rspamd_logger.errx(rspamd_config, 'No rules enabled')
  493. return
  494. end
  495. local backend_required_elements = {
  496. http = {
  497. 'url',
  498. },
  499. smtp = {
  500. 'mail_to',
  501. 'smtp',
  502. },
  503. redis_pubsub = {
  504. 'channel',
  505. },
  506. }
  507. local check_element = {
  508. selector = function(k, v)
  509. if not selectors[v] then
  510. rspamd_logger.errx(rspamd_config, 'Rule %s has invalid selector %s', k, v)
  511. return false
  512. else
  513. return true
  514. end
  515. end,
  516. formatter = function(k, v)
  517. if not formatters[v] then
  518. rspamd_logger.errx(rspamd_config, 'Rule %s has invalid formatter %s', k, v)
  519. return false
  520. else
  521. return true
  522. end
  523. end,
  524. }
  525. local backend_check = {
  526. default = function(k, rule)
  527. local reqset = backend_required_elements[rule.backend]
  528. if reqset then
  529. for _, e in ipairs(reqset) do
  530. if not rule[e] then
  531. rspamd_logger.errx(rspamd_config, 'Rule %s misses required setting %s', k, e)
  532. settings.rules[k] = nil
  533. end
  534. end
  535. end
  536. for sett, v in pairs(rule) do
  537. local f = check_element[sett]
  538. if f then
  539. if not f(sett, v) then
  540. settings.rules[k] = nil
  541. end
  542. end
  543. end
  544. end,
  545. }
  546. backend_check.redis_pubsub = function(k, rule)
  547. if not redis_params then
  548. redis_params = rspamd_parse_redis_server(N)
  549. end
  550. if not redis_params then
  551. rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
  552. settings.rules[k] = nil
  553. else
  554. backend_check.default(k, rule)
  555. end
  556. end
  557. setmetatable(backend_check, {
  558. __index = function()
  559. return backend_check.default
  560. end,
  561. })
  562. for k, v in pairs(settings.rules) do
  563. if type(v) == 'table' then
  564. local backend = v.backend
  565. if not backend then
  566. rspamd_logger.errx(rspamd_config, 'Rule %s has no backend', k)
  567. settings.rules[k] = nil
  568. elseif not pushers[backend] then
  569. rspamd_logger.errx(rspamd_config, 'Rule %s has invalid backend %s', k, backend)
  570. settings.rules[k] = nil
  571. else
  572. local f = backend_check[backend]
  573. f(k, v)
  574. end
  575. else
  576. rspamd_logger.errx(rspamd_config, 'Rule %s has bad type: %s', k, type(v))
  577. settings.rules[k] = nil
  578. end
  579. end
  580. local function gen_exporter(rule)
  581. return function (task)
  582. if task:has_flag('skip') then return end
  583. local selector = rule.selector or 'default'
  584. local selected = selectors[selector](task)
  585. if selected then
  586. lua_util.debugm(N, task, 'Message selected for processing')
  587. local formatter = rule.formatter or 'default'
  588. local formatted, extra = formatters[formatter](task, rule)
  589. if formatted then
  590. pushers[rule.backend](task, formatted, rule, extra)
  591. else
  592. lua_util.debugm(N, task, 'Formatter [%s] returned non-truthy value [%s]', formatter, formatted)
  593. end
  594. else
  595. lua_util.debugm(N, task, 'Selector [%s] returned non-truthy value [%s]', selector, selected)
  596. end
  597. end
  598. end
  599. if not next(settings.rules) then
  600. rspamd_logger.errx(rspamd_config, 'No rules enabled')
  601. lua_util.disable_module(N, "config")
  602. end
  603. for k, r in pairs(settings.rules) do
  604. rspamd_config:register_symbol({
  605. name = 'EXPORT_METADATA_' .. k,
  606. type = 'idempotent',
  607. callback = gen_exporter(r),
  608. priority = 10,
  609. flags = 'empty,explicit_disable,ignore_passthrough',
  610. })
  611. end