From f722c66ce8ff4f888b0660af2ef83a23f789be7a Mon Sep 17 00:00:00 2001 From: Vsevolod Stakhov Date: Sat, 7 Oct 2017 14:48:52 +0100 Subject: [PATCH] [Feature] Add framing for the new reputation generic plugin --- src/plugins/lua/reputation.lua | 254 +++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 src/plugins/lua/reputation.lua diff --git a/src/plugins/lua/reputation.lua b/src/plugins/lua/reputation.lua new file mode 100644 index 000000000..e97a7600f --- /dev/null +++ b/src/plugins/lua/reputation.lua @@ -0,0 +1,254 @@ +--[[ +Copyright (c) 2017, Vsevolod Stakhov + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + +if confighelp then + return +end + +-- A generic plugin for reputation handling + +local E = {} +local N = 'reputation' + +local rspamd_logger = require "rspamd_logger" +local rspamd_util = require "rspamd_util" +local rspamd_lua_utils = require "lua_util" +local fun = require "fun" +local redis_params = nil +local default_expiry = 864000 -- 10 day by default + +-- IP Selector functions + + +-- Selectors are used to extract reputation tokens +local ip_selector = { + config = { + actions = { -- how each action is treated in scoring + ['reject'] = 1.0, + ['add header'] = 0.25, + ['rewrite subject'] = 0.25, + ['no action'] = 1.0 + }, + scores = { -- how each component is evaluated + ['asn'] = 0.4, + ['country'] = 0.01, + ['ipnet'] = 0.5, + ['ip'] = 1.0 + }, + symbol = 'IP_SCORE', -- symbol to be inserted + hash = 'ip_score', -- hash table in redis used for storing scores + asn_suffix = 'a:', -- prefix for ASN hashes + country_suffix = 'c:', -- prefix for country hashes + ipnet_suffix = 'n:', -- prefix for ipnet hashes + lower_bound = 10, -- minimum number of messages to be scored + min_score = nil, + max_score = nil, + score_divisor = 1, + }, + --dependencies = {"ASN"}, -- ASN is a prefilter now... +} + +local selectors = { + ip = ip_selector, +} + +local function reputation_dns_init(rule) + if not rule.backend.config.list then + rspamd_logger.errx(rspamd_config, "rule %s with DNS backend has no `list` parameter defined", + rule.symbol) + return false + end + + return true +end + +local function reputation_dns_get_token(task, token) +end + +local function reputation_redis_get_token(task, token) +end + +local function reputation_redis_set_token(task, token, value) +end + +-- Backends are responsible for getting reputation tokens +local backends = { + redis = { + config = { + expiry = default_expiry + }, + get_token = reputation_redis_get_token, + set_token = reputation_redis_set_token, + }, + dns = { + config = { + }, + get_token = reputation_dns_get_token, + -- No set token for DNS + init = reputation_dns_init, + } +} + +local function reputation_filter_cb(task, rule) + rule.selector.filter(task, rule, rule.backend) +end + +local function reputation_postfilter_cb(task, rule) + rule.selector.postfilter(task, rule, rule.backend) +end + +local function reputation_idempotent_cb(task, rule) + rule.selector.idempotent(task, rule, rule.backend) +end + +local function deepcopy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[deepcopy(orig_key)] = deepcopy(orig_value) + end + setmetatable(copy, deepcopy(getmetatable(orig))) + else -- number, string, boolean, etc + copy = orig + end + return copy +end +local function override_defaults(def, override) + for k,v in pairs(override) do + if def[k] then + if type(v) == 'table' then + override_defaults(def[k], v) + else + def[k] = v + end + else + def[k] = v + end + end +end + +local rules = {} + +local function callback_gen(cb, rule) + return function(task) + cb(task, rule) + end +end + +local function parse_rule(name, tbl) + local selector = selectors[tbl.selector['type']] + + if not selector then + rspamd_logger.errx(rspamd_config, "unknown selector defined for rule %s: %s", name, + tbl.selector.type) + return + end + + local backend = tbl.backend + if not backend or not backend.type then + rspamd_logger.errx(rspamd_config, "no backend defined for rule %s", name) + return + end + + backend = backends[backend.type] + if not backend then + rspamd_logger.errx(rspamd_config, "unknown backend defined for rule %s: %s", name, + tbl.backend.type) + return + end + -- Allow config override + local rule = { + selector = deepcopy(selector), + backend = deepcopy(backend) + } + + -- Override default config params + override_defaults(rule.backend.config, tbl.backend) + override_defaults(rule.selector.config, tbl.selector) + + local symbol = name + if tbl.symbol then + symbol = name + end + + rule.symbol = symbol + + -- Perform additional initialization if needed + if rule.selector.init then + if not rule.selector.init(rule) then + return + end + end + if rule.backend.init then + if not rule.backend.init(rule) then + return + end + end + + -- We now generate symbol for checking + local id = rspamd_config:register_symbol{ + name = symbol, + type = 'normal', + callback = callback_gen(reputation_filter_cb, rule), + } + + if rule.selector.dependencies then + fun.each(function(d) + rspamd_config:register_dependency(id, d) + end, rule.selector.dependencies) + end + + if rule.selector.postfilter then + -- Also register a postfilter + rspamd_config:register_symbol{ + name = symbol .. '_POST', + type = 'postfilter,nostat', + callback = callback_gen(reputation_postfilter_cb, rule), + } + end + + if rule.selector.idempotent then + -- Has also idempotent component (e.g. saving data to the backend) + rspamd_config:register_symbol{ + name = symbol .. '_IDEMPOTENT', + type = 'idempotent', + callback = callback_gen(reputation_idempotent_cb, rule), + } + end + + rules.symbol = rule +end + +redis_params = rspamd_parse_redis_server('reputation') +local opts = rspamd_config:get_all_opt("fann_redis") + +-- Initialization part +if not (opts and type(opts) == 'table') then + rspamd_logger.infox(rspamd_config, 'Module is unconfigured') + return +end + +if opts['rules'] then + for k,v in opts['rules'] do + if not v.selector or not v.selector.type then + rspamd_logger.errx(rspamd_config, "no selector defined for rule %s", k) + else + parse_rule(k, v) + end + end +end -- 2.39.5