]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Feature] Add framing for the new reputation generic plugin
authorVsevolod Stakhov <vsevolod@highsecure.ru>
Sat, 7 Oct 2017 13:48:52 +0000 (14:48 +0100)
committerVsevolod Stakhov <vsevolod@highsecure.ru>
Sat, 7 Oct 2017 13:48:52 +0000 (14:48 +0100)
src/plugins/lua/reputation.lua [new file with mode: 0644]

diff --git a/src/plugins/lua/reputation.lua b/src/plugins/lua/reputation.lua
new file mode 100644 (file)
index 0000000..e97a760
--- /dev/null
@@ -0,0 +1,254 @@
+--[[
+Copyright (c) 2017, Vsevolod Stakhov <vsevolod@highsecure.ru>
+
+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