aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AUTHORS.md4
-rw-r--r--conf/modules.d/contextal.conf21
-rw-r--r--conf/modules.d/phishing.conf2
-rw-r--r--interface/js/app/config.js10
-rw-r--r--lualib/lua_maps.lua87
-rw-r--r--lualib/lua_scanners/cloudmark.lua49
-rw-r--r--lualib/lua_util.lua51
-rw-r--r--src/controller.c116
-rw-r--r--src/libserver/maps/map.c73
-rw-r--r--src/libserver/maps/map_private.h36
-rw-r--r--src/libserver/symcache/symcache_impl.cxx4
-rw-r--r--src/lua/lua_config.c4
-rw-r--r--src/lua/lua_map.c7
-rw-r--r--src/plugins/lua/contextal.lua332
-rw-r--r--src/plugins/lua/gpt.lua13
-rw-r--r--src/plugins/lua/hfilter.lua13
-rw-r--r--src/plugins/lua/phishing.lua2
-rw-r--r--test/functional/cases/001_merged/102_multimap.robot10
-rw-r--r--test/functional/configs/merged-override.conf8
19 files changed, 708 insertions, 134 deletions
diff --git a/AUTHORS.md b/AUTHORS.md
index a07906645..86cc3dfa0 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,10 +1,10 @@
# Project Authors and Contributors
-Rspamd was created by [Vsevolod Stakhov](https://github.com/vstakov).
+Rspamd was created by [Vsevolod Stakhov](https://github.com/vstakhov).
## Authors
-* [Vsevolod Stakhov](https://github.com/vstakov)
+* [Vsevolod Stakhov](https://github.com/vstakhov)
## Developers
diff --git a/conf/modules.d/contextal.conf b/conf/modules.d/contextal.conf
new file mode 100644
index 000000000..da61b2cd8
--- /dev/null
+++ b/conf/modules.d/contextal.conf
@@ -0,0 +1,21 @@
+# Please don't modify this file as your changes might be overwritten with
+# the next update.
+#
+# You can modify 'local.d/contextal.conf' to add and merge
+# parameters defined inside this section
+#
+# You can modify 'override.d/contextal.conf' to strictly override all
+# parameters defined inside this section
+#
+# See https://rspamd.com/doc/faq.html#what-are-the-locald-and-overrided-directories
+# for details
+#
+# Module documentation can be found at https://rspamd.com/doc/modules/contextal.html
+
+contextal {
+ enabled = false;
+
+ .include(try=true,priority=5) "${DBDIR}/dynamic/contextal.conf"
+ .include(try=true,priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/contextal.conf"
+ .include(try=true,priority=10) "$LOCAL_CONFDIR/override.d/contextal.conf"
+}
diff --git a/conf/modules.d/phishing.conf b/conf/modules.d/phishing.conf
index a6531e689..076f5964f 100644
--- a/conf/modules.d/phishing.conf
+++ b/conf/modules.d/phishing.conf
@@ -17,7 +17,7 @@ phishing {
# Disabled by default
openphish_enabled = false;
openphish_premium = false;
- openphish_map = "https://www.openphish.com/feed.txt";
+ openphish_map = "https://raw.githubusercontent.com/openphish/public_feed/refs/heads/main/feed.txt";
# Phishtank is disabled by default in the module, so let's enable it here explicitly
phishtank_enabled = true;
diff --git a/interface/js/app/config.js b/interface/js/app/config.js
index 037dabfdd..0f35e3107 100644
--- a/interface/js/app/config.js
+++ b/interface/js/app/config.js
@@ -116,7 +116,6 @@ define(["jquery", "app/common"],
success: function (json) {
const [{data}] = json;
$listmaps.empty();
- $("#modalBody").empty();
const $tbody = $("<tbody>");
$.each(data, (i, item) => {
@@ -126,8 +125,7 @@ define(["jquery", "app/common"],
}
const $tr = $("<tr>").append($td);
- const $span = $('<span class="map-link" data-bs-toggle="modal" data-bs-target="#modalDialog">' +
- item.uri + "</span>").data("item", item);
+ const $span = $('<span class="map-link">' + item.uri + "</span>").data("item", item);
$span.wrap("<td>").parent().appendTo($tr);
$("<td>" + item.description + "</td>").appendTo($tr);
$tr.appendTo($tbody);
@@ -157,7 +155,7 @@ define(["jquery", "app/common"],
let mode = "advanced";
// Modal form for maps
- $(document).on("click", "[data-bs-toggle=\"modal\"]", function () {
+ $(document).on("click", ".map-link", function () {
const item = $(this).data("item");
common.query("getmap", {
headers: {
@@ -167,6 +165,7 @@ define(["jquery", "app/common"],
// Highlighting a large amount of text is unresponsive
mode = (new Blob([data[0].data]).size > 5120) ? "basic" : $("input[name=editorMode]:checked").val();
+ $("#modalBody").empty();
$("<" + editor[mode].elt + ' id="editor" class="' + editor[mode].class + '" data-id="' + item.map +
'"></' + editor[mode].elt + ">").appendTo("#modalBody");
@@ -198,10 +197,9 @@ define(["jquery", "app/common"],
errorMessage: "Cannot receive maps data",
server: common.getServer()
});
- return false;
});
$("#modalDialog").on("hidden.bs.modal", () => {
- if (editor[mode].codejar) {
+ if (editor[mode].codejar && jar && typeof jar.destroy === "function") {
jar.destroy();
$(".codejar-wrap").remove();
} else {
diff --git a/lualib/lua_maps.lua b/lualib/lua_maps.lua
index 6dad3b6ad..c45b51b97 100644
--- a/lualib/lua_maps.lua
+++ b/lualib/lua_maps.lua
@@ -88,16 +88,64 @@ end
local external_map_schema = ts.shape {
external = ts.equivalent(true), -- must be true
- backend = ts.string, -- where to get data, required
- method = ts.one_of { "body", "header", "query" }, -- how to pass input
+ backend = ts.string:is_optional(), -- where to get data, required for HTTP
+ cdb = ts.string:is_optional(), -- path to CDB file, required for CDB
+ method = ts.one_of { "body", "header", "query" }:is_optional(), -- how to pass input
encode = ts.one_of { "json", "messagepack" }:is_optional(), -- how to encode input (if relevant)
timeout = (ts.number + ts.string / lua_util.parse_time_interval):is_optional(),
}
+-- Storage for CDB instances
+local cdb_maps = {}
+local cdb_finisher_set = false
+
local rspamd_http = require "rspamd_http"
local ucl = require "ucl"
+-- Function to handle CDB maps
+local function handle_cdb_map(map_config, key, callback, task)
+ local rspamd_cdb = require "rspamd_cdb"
+ local hash_key = map_config.cdb
+
+ -- Check if we need to open the CDB file
+ if not cdb_maps[hash_key] then
+ local cdb_file = map_config.cdb
+ -- Provide ev_base to monitor changes
+ local cdb_handle = rspamd_cdb.open(cdb_file, task:get_ev_base())
+
+ if not cdb_handle then
+ local err_msg = string.format("Failed to open CDB file: %s", cdb_file)
+ rspamd_logger.errx(task, err_msg)
+ if callback then
+ callback(false, err_msg, 500, task)
+ end
+ return nil
+ else
+ cdb_maps[hash_key] = cdb_handle
+ end
+ end
+
+ -- Look up the key in CDB
+ local result = cdb_maps[hash_key]:find(key)
+
+ if callback then
+ if result then
+ callback(true, result, 200, task)
+ else
+ callback(false, 'not found', 404, task)
+ end
+ return nil
+ end
+
+ return result
+end
+
local function query_external_map(map_config, upstreams, key, callback, task)
+ -- Check if this is a CDB map
+ if map_config.cdb then
+ return handle_cdb_map(map_config, key, callback, task)
+ end
+ -- Fallback to HTTP
local http_method = (map_config.method == 'body' or map_config.method == 'form') and 'POST' or 'GET'
local upstream = upstreams:get_upstream_round_robin()
local http_headers = {
@@ -138,7 +186,8 @@ local function query_external_map(map_config, upstreams, key, callback, task)
local params_table = {}
for k, v in pairs(key) do
if type(v) == 'string' then
- table.insert(params_table, string.format('%s=%s', lua_util.url_encode_string(k), lua_util.url_encode_string(v)))
+ table.insert(params_table,
+ string.format('%s=%s', lua_util.url_encode_string(k), lua_util.url_encode_string(v)))
end
end
url = string.format('%s?%s', url, table.concat(params_table, '&'))
@@ -305,7 +354,7 @@ local function rspamd_map_add_from_ucl(opt, mtype, description, callback)
if string.find(opt[1], '^%d') then
-- List of numeric stuff (hope it's ipnets definitions)
- local map = rspamd_config:radix_from_ucl(opt)
+ local map = rspamd_config:radix_from_ucl(opt, description)
if map then
ret.__data = map
@@ -448,17 +497,39 @@ local function rspamd_map_add_from_ucl(opt, mtype, description, callback)
local parse_res, parse_err = external_map_schema(opt)
if parse_res then
- ret.__upstreams = lua_util.http_upstreams_by_url(rspamd_config:get_mempool(), opt.backend)
- if ret.__upstreams then
+ if opt.cdb then
ret.__data = opt
ret.__external = true
setmetatable(ret, ret_mt)
maybe_register_selector()
+ if not cdb_finisher_set then
+ -- Register a finalize script to close all CDB handles when Rspamd stops
+ rspamd_config:register_finish_script(function()
+ for path, _ in pairs(cdb_maps) do
+ rspamd_logger.infox(rspamd_config, 'closing CDB map: %s', path)
+ cdb_maps[path] = nil
+ end
+ end)
+ cdb_finisher_set = true
+ end
+
return ret
+ elseif opt.backend then
+ ret.__upstreams = lua_util.http_upstreams_by_url(rspamd_config:get_mempool(), opt.backend)
+ if ret.__upstreams then
+ ret.__data = opt
+ ret.__external = true
+ setmetatable(ret, ret_mt)
+ maybe_register_selector()
+
+ return ret
+ else
+ rspamd_logger.errx(rspamd_config, 'cannot parse external map upstreams: %s',
+ opt.backend)
+ end
else
- rspamd_logger.errx(rspamd_config, 'cannot parse external map upstreams: %s',
- opt.backend)
+ rspamd_logger.errx(rspamd_config, 'external map requires either "cdb" or "backend" parameter')
end
else
rspamd_logger.errx(rspamd_config, 'cannot parse external map: %s',
diff --git a/lualib/lua_scanners/cloudmark.lua b/lualib/lua_scanners/cloudmark.lua
index 26a3bf9c4..12a60abf1 100644
--- a/lualib/lua_scanners/cloudmark.lua
+++ b/lualib/lua_scanners/cloudmark.lua
@@ -173,53 +173,6 @@ local function cloudmark_config(opts)
return nil
end
--- Converts a key-value map to the table representing multipart body, with the following values:
--- `data`: data of the part
--- `filename`: optional filename
--- `content-type`: content type of the element (optional)
--- `content-transfer-encoding`: optional CTE header
-local function table_to_multipart_body(tbl, boundary)
- local seen_data = false
- local out = {}
-
- for k, v in pairs(tbl) do
- if v.data then
- seen_data = true
- table.insert(out, string.format('--%s\r\n', boundary))
- if v.filename then
- table.insert(out,
- string.format('Content-Disposition: form-data; name="%s"; filename="%s"\r\n',
- k, v.filename))
- else
- table.insert(out,
- string.format('Content-Disposition: form-data; name="%s"\r\n', k))
- end
- if v['content-type'] then
- table.insert(out,
- string.format('Content-Type: %s\r\n', v['content-type']))
- else
- table.insert(out, 'Content-Type: text/plain\r\n')
- end
- if v['content-transfer-encoding'] then
- table.insert(out,
- string.format('Content-Transfer-Encoding: %s\r\n',
- v['content-transfer-encoding']))
- else
- table.insert(out, 'Content-Transfer-Encoding: binary\r\n')
- end
- table.insert(out, '\r\n')
- table.insert(out, v.data)
- table.insert(out, '\r\n')
- end
- end
-
- if seen_data then
- table.insert(out, string.format('--%s--\r\n', boundary))
- end
-
- return out
-end
-
local function get_specific_symbol(scores_symbols, score)
local selected
local sel_thr = -1
@@ -359,7 +312,7 @@ local function cloudmark_check(task, content, digest, rule, maybe_part)
local request_data = {
task = task,
url = url,
- body = table_to_multipart_body(request, static_boundary),
+ body = lua_util.table_to_multipart_body(request, static_boundary),
headers = {
['Content-Type'] = string.format('multipart/form-data; boundary="%s"', static_boundary)
},
diff --git a/lualib/lua_util.lua b/lualib/lua_util.lua
index 62b38c87e..636212b1f 100644
--- a/lualib/lua_util.lua
+++ b/lualib/lua_util.lua
@@ -1805,4 +1805,55 @@ exports.symbols_priorities = {
low = 0,
}
+---[[[
+-- @function lua_util.table_to_multipart_body(tbl, boundary)
+-- Converts a key-value map to the table representing multipart body, with the following values:
+-- `data`: data of the part
+-- `filename`: optional filename
+-- `content-type`: content type of the element (optional)
+-- `content-transfer-encoding`: optional CTE header
+local function table_to_multipart_body(tbl, boundary)
+ local seen_data = false
+ local out = {}
+
+ for k, v in pairs(tbl) do
+ if v.data then
+ seen_data = true
+ table.insert(out, string.format('--%s\r\n', boundary))
+ if v.filename then
+ table.insert(out,
+ string.format('Content-Disposition: form-data; name="%s"; filename="%s"\r\n',
+ k, v.filename))
+ else
+ table.insert(out,
+ string.format('Content-Disposition: form-data; name="%s"\r\n', k))
+ end
+ if v['content-type'] then
+ table.insert(out,
+ string.format('Content-Type: %s\r\n', v['content-type']))
+ else
+ table.insert(out, 'Content-Type: text/plain\r\n')
+ end
+ if v['content-transfer-encoding'] then
+ table.insert(out,
+ string.format('Content-Transfer-Encoding: %s\r\n',
+ v['content-transfer-encoding']))
+ else
+ table.insert(out, 'Content-Transfer-Encoding: binary\r\n')
+ end
+ table.insert(out, '\r\n')
+ table.insert(out, v.data)
+ table.insert(out, '\r\n')
+ end
+ end
+
+ if seen_data then
+ table.insert(out, string.format('--%s--\r\n', boundary))
+ end
+
+ return out
+end
+
+exports.table_to_multipart_body = table_to_multipart_body
+
return exports
diff --git a/src/controller.c b/src/controller.c
index 386448f93..895611589 100644
--- a/src/controller.c
+++ b/src/controller.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Vsevolod Stakhov
+ * Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -979,12 +979,6 @@ rspamd_controller_handle_maps(struct rspamd_http_connection_entry *conn_ent,
if (bk->protocol == MAP_PROTO_FILE) {
editable = rspamd_controller_can_edit_map(bk);
-
- if (!editable && access(bk->uri, R_OK) == -1) {
- /* Skip unreadable and non-existing maps */
- continue;
- }
-
obj = ucl_object_typed_new(UCL_OBJECT);
ucl_object_insert_key(obj, ucl_object_fromint(bk->id),
"map", 0, false);
@@ -994,8 +988,34 @@ rspamd_controller_handle_maps(struct rspamd_http_connection_entry *conn_ent,
}
ucl_object_insert_key(obj, ucl_object_fromstring(bk->uri),
"uri", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromstring("file"),
+ "type", 0, false);
ucl_object_insert_key(obj, ucl_object_frombool(editable),
"editable", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(map->shared->loaded),
+ "loaded", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(map->shared->cached),
+ "cached", 0, false);
+ ucl_array_append(top, obj);
+ }
+ else {
+ obj = ucl_object_typed_new(UCL_OBJECT);
+ ucl_object_insert_key(obj, ucl_object_fromint(bk->id),
+ "map", 0, false);
+ if (map->description) {
+ ucl_object_insert_key(obj, ucl_object_fromstring(map->description),
+ "description", 0, false);
+ }
+ ucl_object_insert_key(obj, ucl_object_fromstring(bk->uri),
+ "uri", 0, false);
+ ucl_object_insert_key(obj, ucl_object_fromstring(rspamd_map_fetch_protocol_name(bk->protocol)),
+ "type", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(false),
+ "editable", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(map->shared->loaded),
+ "loaded", 0, false);
+ ucl_object_insert_key(obj, ucl_object_frombool(map->shared->cached),
+ "cached", 0, false);
ucl_array_append(top, obj);
}
}
@@ -1008,6 +1028,21 @@ rspamd_controller_handle_maps(struct rspamd_http_connection_entry *conn_ent,
return 0;
}
+gboolean
+rspamd_controller_map_traverse_callback(gconstpointer key, gconstpointer value, gsize _hits, gpointer ud)
+{
+ rspamd_fstring_t **target = (rspamd_fstring_t **) ud;
+
+ *target = rspamd_fstring_append(*target, key, strlen(key));
+
+ if (value) {
+ *target = rspamd_fstring_append(*target, " ", 1);
+ *target = rspamd_fstring_append(*target, value, strlen(value));
+ }
+ *target = rspamd_fstring_append(*target, "\n", 1);
+
+ return TRUE;
+}
/*
* Get map command handler:
* request: /getmap
@@ -1020,7 +1055,7 @@ rspamd_controller_handle_get_map(struct rspamd_http_connection_entry *conn_ent,
{
struct rspamd_controller_session *session = conn_ent->ud;
GList *cur;
- struct rspamd_map *map;
+ struct rspamd_map *map = NULL;
struct rspamd_map_backend *bk = NULL;
const rspamd_ftok_t *idstr;
struct stat st;
@@ -1054,7 +1089,7 @@ rspamd_controller_handle_get_map(struct rspamd_http_connection_entry *conn_ent,
PTR_ARRAY_FOREACH(map->backends, i, bk)
{
- if (bk->id == id && bk->protocol == MAP_PROTO_FILE) {
+ if (bk->id == id) {
found = TRUE;
break;
}
@@ -1069,32 +1104,53 @@ rspamd_controller_handle_get_map(struct rspamd_http_connection_entry *conn_ent,
return 0;
}
- if (stat(bk->uri, &st) == -1 || (fd = open(bk->uri, O_RDONLY)) == -1) {
+ if (bk->protocol == MAP_PROTO_FILE) {
+ if (stat(bk->uri, &st) == -1 || (fd = open(bk->uri, O_RDONLY)) == -1) {
+ reply = rspamd_http_new_message(HTTP_RESPONSE);
+ reply->date = time(NULL);
+ reply->code = 200;
+ }
+ else {
+
+ reply = rspamd_http_new_message(HTTP_RESPONSE);
+ reply->date = time(NULL);
+ reply->code = 200;
+
+ if (st.st_size > 0) {
+ if (!rspamd_http_message_set_body_from_fd(reply, fd)) {
+ close(fd);
+ rspamd_http_message_unref(reply);
+ msg_err_session("cannot read map %s: %s", bk->uri, strerror(errno));
+ rspamd_controller_send_error(conn_ent, 500, "Map read error");
+ return 0;
+ }
+ }
+ else {
+ rspamd_fstring_t *empty_body = rspamd_fstring_new_init("", 0);
+ rspamd_http_message_set_body_from_fstring_steal(reply, empty_body);
+ }
+
+ close(fd);
+ }
+ }
+ else if (bk->protocol == MAP_PROTO_STATIC) {
+ /* We can just traverse map and form reply */
reply = rspamd_http_new_message(HTTP_RESPONSE);
- reply->date = time(NULL);
reply->code = 200;
+ rspamd_fstring_t *map_body = rspamd_fstring_new();
+ rspamd_map_traverse(bk->map, rspamd_controller_map_traverse_callback, &map_body, FALSE);
+ rspamd_http_message_set_body_from_fstring_steal(reply, map_body);
}
- else {
-
+ else if (map->shared->loaded) {
reply = rspamd_http_new_message(HTTP_RESPONSE);
- reply->date = time(NULL);
reply->code = 200;
-
- if (st.st_size > 0) {
- if (!rspamd_http_message_set_body_from_fd(reply, fd)) {
- close(fd);
- rspamd_http_message_unref(reply);
- msg_err_session("cannot read map %s: %s", bk->uri, strerror(errno));
- rspamd_controller_send_error(conn_ent, 500, "Map read error");
- return 0;
- }
- }
- else {
- rspamd_fstring_t *empty_body = rspamd_fstring_new_init("", 0);
- rspamd_http_message_set_body_from_fstring_steal(reply, empty_body);
- }
-
- close(fd);
+ rspamd_fstring_t *map_body = rspamd_fstring_new();
+ rspamd_map_traverse(bk->map, rspamd_controller_map_traverse_callback, &map_body, FALSE);
+ rspamd_http_message_set_body_from_fstring_steal(reply, map_body);
+ }
+ else {
+ reply = rspamd_http_new_message(HTTP_RESPONSE);
+ reply->code = 404;
}
rspamd_http_connection_reset(conn_ent->conn);
diff --git a/src/libserver/maps/map.c b/src/libserver/maps/map.c
index 97130ad7c..76d639a69 100644
--- a/src/libserver/maps/map.c
+++ b/src/libserver/maps/map.c
@@ -339,6 +339,7 @@ http_map_finish(struct rspamd_http_connection *conn,
cbd->periodic->cur_backend = 0;
/* Reset cache, old cached data will be cleaned on timeout */
g_atomic_int_set(&data->cache->available, 0);
+ g_atomic_int_set(&map->shared->loaded, 0);
data->cur_cache_cbd = NULL;
rspamd_map_process_periodic(cbd->periodic);
@@ -424,6 +425,8 @@ http_map_finish(struct rspamd_http_connection *conn,
* We know that a map is in the locked state
*/
g_atomic_int_set(&data->cache->available, 1);
+ g_atomic_int_set(&map->shared->loaded, 1);
+ g_atomic_int_set(&map->shared->cached, 0);
/* Store cached data */
rspamd_strlcpy(data->cache->shmem_name, cbd->shmem_data->shm_name,
sizeof(data->cache->shmem_name));
@@ -919,6 +922,8 @@ read_map_file(struct rspamd_map *map, struct file_map_data *data,
map->read_callback(NULL, 0, &periodic->cbdata, TRUE);
}
+ g_atomic_int_set(&map->shared->loaded, 1);
+
return TRUE;
}
@@ -1003,6 +1008,7 @@ read_map_static(struct rspamd_map *map, struct static_map_data *data,
}
data->processed = TRUE;
+ g_atomic_int_set(&map->shared->loaded, 1);
return TRUE;
}
@@ -1028,7 +1034,7 @@ rspamd_map_periodic_dtor(struct map_periodic_cbdata *periodic)
}
if (periodic->locked) {
- g_atomic_int_set(periodic->map->locked, 0);
+ g_atomic_int_set(&periodic->map->shared->locked, 0);
msg_debug_map("unlocked map %s", periodic->map->name);
if (periodic->map->wrk->state == rspamd_worker_state_running) {
@@ -1438,6 +1444,9 @@ rspamd_map_read_cached(struct rspamd_map *map, struct rspamd_map_backend *bk,
map->read_callback(in, len, &periodic->cbdata, TRUE);
}
+ g_atomic_int_set(&map->shared->loaded, 1);
+ g_atomic_int_set(&map->shared->cached, 1);
+
munmap(in, mmap_len);
return TRUE;
@@ -1727,6 +1736,8 @@ rspamd_map_read_http_cached_file(struct rspamd_map *map,
struct tm tm;
char ncheck_buf[32], lm_buf[32];
+ g_atomic_int_set(&map->shared->loaded, 1);
+ g_atomic_int_set(&map->shared->cached, 1);
rspamd_localtime(map->next_check, &tm);
strftime(ncheck_buf, sizeof(ncheck_buf) - 1, "%Y-%m-%d %H:%M:%S", &tm);
rspamd_localtime(htdata->last_modified, &tm);
@@ -2028,7 +2039,7 @@ rspamd_map_process_periodic(struct map_periodic_cbdata *cbd)
map->scheduled_check = NULL;
if (!map->file_only && !cbd->locked) {
- if (!g_atomic_int_compare_and_exchange(cbd->map->locked,
+ if (!g_atomic_int_compare_and_exchange(&cbd->map->shared->locked,
0, 1)) {
msg_debug_map(
"don't try to reread map %s as it is locked by other process, "
@@ -2050,7 +2061,7 @@ rspamd_map_process_periodic(struct map_periodic_cbdata *cbd)
rspamd_map_schedule_periodic(cbd->map, RSPAMD_MAP_SCHEDULE_ERROR);
if (cbd->locked) {
- g_atomic_int_set(cbd->map->locked, 0);
+ g_atomic_int_set(&cbd->map->shared->locked, 0);
cbd->locked = FALSE;
}
@@ -2781,10 +2792,6 @@ rspamd_map_parse_backend(struct rspamd_config *cfg, const char *map_line)
bk->data.sd = sdata;
}
- bk->id = rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_T1HA,
- bk->uri, strlen(bk->uri),
- 0xdeadbabe);
-
return bk;
err:
@@ -2815,6 +2822,13 @@ rspamd_map_calculate_hash(struct rspamd_map *map)
rspamd_cryptobox_hash_init(&st, NULL, 0);
+ if (map->name) {
+ rspamd_cryptobox_hash_update(&st, map->name, strlen(map->name));
+ }
+ if (map->description) {
+ rspamd_cryptobox_hash_update(&st, map->description, strlen(map->description));
+ }
+
for (i = 0; i < map->backends->len; i++) {
bk = g_ptr_array_index(map->backends, i);
rspamd_cryptobox_hash_update(&st, bk->uri, strlen(bk->uri));
@@ -2823,6 +2837,26 @@ rspamd_map_calculate_hash(struct rspamd_map *map)
rspamd_cryptobox_hash_final(&st, cksum);
cksum_encoded = rspamd_encode_base32(cksum, sizeof(cksum), RSPAMD_BASE32_DEFAULT);
rspamd_strlcpy(map->tag, cksum_encoded, sizeof(map->tag));
+
+ for (i = 0; i < map->backends->len; i++) {
+ bk = g_ptr_array_index(map->backends, i);
+
+ /* Also update each backend */
+ rspamd_cryptobox_fast_hash_state_t hst;
+ rspamd_cryptobox_fast_hash_init(&hst, 0);
+ rspamd_cryptobox_fast_hash_update(&hst, bk->uri, strlen(bk->uri));
+ rspamd_cryptobox_fast_hash_update(&hst, map->tag, sizeof(map->tag));
+
+ if (bk->protocol == MAP_PROTO_STATIC) {
+ /* Static maps content is pre-defined */
+ rspamd_cryptobox_fast_hash_update(&hst, bk->data.sd->data,
+ bk->data.sd->len);
+ }
+
+ /* We use only 52 bits to be compatible with other numbers representation */
+ bk->id = rspamd_cryptobox_fast_hash_final(&hst) & ~(0xFFFULL << 52);
+ }
+
g_free(cksum_encoded);
}
@@ -2888,8 +2922,8 @@ rspamd_map_add(struct rspamd_config *cfg,
map->user_data = user_data;
map->cfg = cfg;
map->id = rspamd_random_uint64_fast();
- map->locked =
- rspamd_mempool_alloc0_shared(cfg->cfg_pool, sizeof(int));
+ map->shared =
+ rspamd_mempool_alloc0_shared(cfg->cfg_pool, sizeof(struct rspamd_map_shared_data));
map->backends = g_ptr_array_sized_new(1);
map->wrk = worker;
rspamd_mempool_add_destructor(cfg->cfg_pool, rspamd_ptr_array_free_hard,
@@ -2988,8 +3022,8 @@ rspamd_map_add_from_ucl(struct rspamd_config *cfg,
map->user_data = user_data;
map->cfg = cfg;
map->id = rspamd_random_uint64_fast();
- map->locked =
- rspamd_mempool_alloc0_shared(cfg->cfg_pool, sizeof(int));
+ map->shared =
+ rspamd_mempool_alloc0_shared(cfg->cfg_pool, sizeof(struct rspamd_map_shared_data));
map->backends = g_ptr_array_new();
map->wrk = worker;
map->no_file_read = (flags & RSPAMD_MAP_FILE_NO_READ);
@@ -3108,7 +3142,7 @@ rspamd_map_add_from_ucl(struct rspamd_config *cfg,
goto err;
}
- gboolean all_local = TRUE;
+ gboolean all_local = TRUE, all_loaded = TRUE;
PTR_ARRAY_FOREACH(map->backends, i, bk)
{
@@ -3127,9 +3161,8 @@ rspamd_map_add_from_ucl(struct rspamd_config *cfg,
map_data = g_string_sized_new(32);
if (rspamd_map_add_static_string(cfg, elt, map_data)) {
- bk->data.sd->data = map_data->str;
bk->data.sd->len = map_data->len;
- g_string_free(map_data, FALSE);
+ bk->data.sd->data = (unsigned char *) g_string_free(map_data, FALSE);
}
else {
g_string_free(map_data, TRUE);
@@ -3152,13 +3185,16 @@ rspamd_map_add_from_ucl(struct rspamd_config *cfg,
}
ucl_object_iterate_free(it);
- bk->data.sd->data = map_data->str;
bk->data.sd->len = map_data->len;
- g_string_free(map_data, FALSE);
+ bk->data.sd->data = (unsigned char *) g_string_free(map_data, FALSE);
}
}
else if (bk->protocol != MAP_PROTO_FILE) {
all_local = FALSE;
+ all_loaded = FALSE; /* Will be loaded later */
+ }
+ else {
+ all_loaded = FALSE; /* Will be loaded later (even for files) */
}
}
@@ -3167,6 +3203,11 @@ rspamd_map_add_from_ucl(struct rspamd_config *cfg,
cfg->map_file_watch_multiplier);
}
+ if (all_loaded) {
+ /* Static map */
+ g_atomic_int_set(&map->shared->loaded, 1);
+ }
+
rspamd_map_calculate_hash(map);
msg_debug_map("added map from ucl");
diff --git a/src/libserver/maps/map_private.h b/src/libserver/maps/map_private.h
index d0b22fe36..0a912a5da 100644
--- a/src/libserver/maps/map_private.h
+++ b/src/libserver/maps/map_private.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Vsevolod Stakhov
+ * Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -54,6 +54,23 @@ enum fetch_proto {
MAP_PROTO_STATIC
};
+static const char *
+rspamd_map_fetch_protocol_name(enum fetch_proto proto)
+{
+ switch (proto) {
+ case MAP_PROTO_FILE:
+ return "file";
+ case MAP_PROTO_HTTP:
+ return "http";
+ case MAP_PROTO_HTTPS:
+ return "https";
+ case MAP_PROTO_STATIC:
+ return "static";
+ default:
+ return "unknown";
+ }
+}
+
/**
* Data specific to file maps
*/
@@ -76,7 +93,7 @@ struct rspamd_http_map_cached_cbdata {
time_t last_checked;
};
-struct rspamd_map_cachepoint {
+struct rspamd_http_map_cache {
int available;
gsize len;
time_t last_modified;
@@ -88,7 +105,7 @@ struct rspamd_map_cachepoint {
*/
struct http_map_data {
/* Shared cache data */
- struct rspamd_map_cachepoint *cache;
+ struct rspamd_http_map_cache *cache;
/* Non-shared for cache owner, used to cleanup cache */
struct rspamd_http_map_cached_cbdata *cur_cache_cbd;
char *userinfo;
@@ -124,7 +141,7 @@ struct rspamd_map_backend {
gboolean is_fallback;
struct rspamd_map *map;
struct ev_loop *event_loop;
- uint32_t id;
+ uint64_t id;
struct rspamd_cryptobox_pubkey *trusted_pubkey;
union rspamd_map_backend_data data;
char *uri;
@@ -133,6 +150,15 @@ struct rspamd_map_backend {
struct map_periodic_cbdata;
+/*
+ * Shared between workers
+ */
+struct rspamd_map_shared_data {
+ int locked;
+ int loaded;
+ int cached;
+};
+
struct rspamd_map {
struct rspamd_dns_resolver *r;
struct rspamd_config *cfg;
@@ -168,7 +194,7 @@ struct rspamd_map {
bool no_file_read; /* Do not read files */
bool seen; /* This map has already been watched or pre-loaded */
/* Shared lock for temporary disabling of map reading (e.g. when this map is written by UI) */
- int *locked;
+ struct rspamd_map_shared_data *shared;
char tag[MEMPOOL_UID_LEN];
};
diff --git a/src/libserver/symcache/symcache_impl.cxx b/src/libserver/symcache/symcache_impl.cxx
index 4d17348c2..c0278cfc1 100644
--- a/src/libserver/symcache/symcache_impl.cxx
+++ b/src/libserver/symcache/symcache_impl.cxx
@@ -126,7 +126,7 @@ auto symcache::init() -> bool
}
else {
msg_err_cache("cannot register delayed dependency %s -> %s: "
- "destionation %s is missing",
+ "destination %s is missing",
delayed_dep.from.data(),
delayed_dep.to.data(), delayed_dep.to.data());
}
@@ -1338,4 +1338,4 @@ auto symcache::get_max_timeout(std::vector<std::pair<double, const cache_item *>
return accumulated_timeout;
}
-}// namespace rspamd::symcache \ No newline at end of file
+}// namespace rspamd::symcache
diff --git a/src/lua/lua_config.c b/src/lua/lua_config.c
index 07ed58ad5..f52eae44f 100644
--- a/src/lua/lua_config.c
+++ b/src/lua/lua_config.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Vsevolod Stakhov
+ * Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -118,7 +118,7 @@ local function foo(task)
end
*/
/***
-* @method rspamd_config:radix_from_ucl(obj)
+* @method rspamd_config:radix_from_ucl(obj, description)
* Creates new embedded map of IP/mask addresses from object.
* @param {ucl} obj object
* @return {map} radix tree object
diff --git a/src/lua/lua_map.c b/src/lua/lua_map.c
index 062613bd7..5f55ece06 100644
--- a/src/lua/lua_map.c
+++ b/src/lua/lua_map.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Vsevolod Stakhov
+ * Copyright 2025 Vsevolod Stakhov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -319,6 +319,11 @@ int lua_config_radix_from_ucl(lua_State *L)
ucl_object_insert_key(fake_obj, ucl_object_fromstring("static"),
"url", 0, false);
+ if (lua_type(L, 3) == LUA_TSTRING) {
+ ucl_object_insert_key(fake_obj, ucl_object_fromstring(lua_tostring(L, 3)),
+ "description", 0, false);
+ }
+
if ((m = rspamd_map_add_from_ucl(cfg, fake_obj, "static radix map",
rspamd_radix_read,
rspamd_radix_fin,
diff --git a/src/plugins/lua/contextal.lua b/src/plugins/lua/contextal.lua
new file mode 100644
index 000000000..f6202781a
--- /dev/null
+++ b/src/plugins/lua/contextal.lua
@@ -0,0 +1,332 @@
+--[[
+Copyright (c) 2025, Vsevolod Stakhov <vsevolod@rspamd.com>
+
+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.
+]]--
+
+local E = {}
+local N = 'contextal'
+
+if confighelp then
+ return
+end
+
+local opts = rspamd_config:get_all_opt(N)
+if not opts then
+ return
+end
+
+local lua_redis = require "lua_redis"
+local lua_util = require "lua_util"
+local redis_cache = require "lua_cache"
+local rspamd_http = require "rspamd_http"
+local rspamd_logger = require "rspamd_logger"
+local rspamd_util = require "rspamd_util"
+local ts = require("tableshape").types
+local ucl = require "ucl"
+
+local cache_context, redis_params
+
+local contextal_actions = {
+ ['ALERT'] = true,
+ ['ALLOW'] = true,
+ ['BLOCK'] = true,
+ ['QUARANTINE'] = true,
+ ['SPAM'] = true,
+}
+
+local config_schema = lua_redis.enrich_schema {
+ action_symbol_prefix = ts.string:is_optional(),
+ base_url = ts.string:is_optional(),
+ cache_prefix = ts.string:is_optional(),
+ cache_timeout = ts.number:is_optional(),
+ cache_ttl = ts.number:is_optional(),
+ custom_actions = ts.array_of(ts.string):is_optional(),
+ defer_if_no_result = ts.boolean:is_optional(),
+ defer_message = ts.string:is_optional(),
+ enabled = ts.boolean:is_optional(),
+ http_timeout = ts.number:is_optional(),
+ request_ttl = ts.number:is_optional(),
+ submission_symbol = ts.string:is_optional(),
+}
+
+local settings = {
+ action_symbol_prefix = 'CONTEXTAL_ACTION',
+ base_url = 'http://localhost:8080',
+ cache_prefix = 'CXAL',
+ cache_timeout = 5,
+ cache_ttl = 3600,
+ custom_actions = {},
+ defer_if_no_result = false,
+ defer_message = 'Awaiting deep scan - try again later',
+ http_timeout = 2,
+ request_ttl = 4,
+ submission_symbol = 'CONTEXTAL_SUBMIT',
+}
+
+local static_boundary = rspamd_util.random_hex(32)
+local use_request_ttl = true
+
+local function maybe_defer(task, obj)
+ if settings.defer_if_no_result and not ((obj or E)[1] or E).actions then
+ task:set_pre_result('soft reject', settings.defer_message)
+ end
+end
+
+local function process_actions(task, obj, is_cached)
+ for _, match in ipairs((obj[1] or E).actions or E) do
+ local act = match.action
+ local scenario = match.scenario
+ if not (act and scenario) then
+ rspamd_logger.err(task, 'bad result: %s', match)
+ elseif contextal_actions[act] then
+ task:insert_result(settings.action_symbol_prefix .. '_' .. act, 1.0, scenario)
+ else
+ rspamd_logger.err(task, 'unknown action: %s', act)
+ end
+ end
+
+ if not cache_context or is_cached then
+ maybe_defer(task, obj)
+ return
+ end
+
+ local cache_obj
+ if (obj[1] or E).actions then
+ cache_obj = {[1] = {["actions"] = obj[1].actions}}
+ else
+ local work_id = task:get_mempool():get_variable('contextal_work_id', 'string')
+ if work_id then
+ cache_obj = {[1] = {["work_id"] = work_id}}
+ else
+ rspamd_logger.err(task, 'no work id found in mempool')
+ return
+ end
+ end
+
+ redis_cache.cache_set(task,
+ task:get_digest(),
+ cache_obj,
+ cache_context)
+
+ maybe_defer(task, obj)
+end
+
+local function process_cached(task, obj)
+ if (obj[1] or E).actions then
+ task:disable_symbol(settings.action_symbol_prefix)
+ return process_actions(task, obj, true)
+ elseif (obj[1] or E).work_id then
+ task:get_mempool():set_variable('contextal_work_id', obj[1].work_id)
+ else
+ rspamd_logger.err(task, 'bad result (cached): %s', obj)
+ end
+end
+
+local function action_cb(task)
+ local work_id = task:get_mempool():get_variable('contextal_work_id', 'string')
+ if not work_id then
+ rspamd_logger.err(task, 'no work id found in mempool')
+ return
+ end
+
+ local function http_callback(err, code, body, hdrs)
+ if err then
+ rspamd_logger.err(task, 'http error: %s', err)
+ maybe_defer(task)
+ return
+ end
+ if code ~= 200 then
+ rspamd_logger.err(task, 'bad http code: %s', code)
+ maybe_defer(task)
+ return
+ end
+ local parser = ucl.parser()
+ local _, parse_err = parser:parse_string(body)
+ if parse_err then
+ rspamd_logger.err(task, 'cannot parse JSON: %s', err)
+ maybe_defer(task)
+ return
+ end
+ local obj = parser:get_object()
+ return process_actions(task, obj, false)
+ end
+
+ rspamd_http.request({
+ task = task,
+ url = settings.actions_url .. work_id,
+ callback = http_callback,
+ timeout = settings.http_timeout,
+ gzip = settings.gzip,
+ keepalive = settings.keepalive,
+ no_ssl_verify = settings.no_ssl_verify,
+ })
+end
+
+local function submit(task)
+
+ local function http_callback(err, code, body, hdrs)
+ if err then
+ rspamd_logger.err(task, 'http error: %s', err)
+ maybe_defer(task)
+ return
+ end
+ if code ~= 201 then
+ rspamd_logger.err(task, 'bad http code: %s', code)
+ maybe_defer(task)
+ return
+ end
+ local parser = ucl.parser()
+ local _, parse_err = parser:parse_string(body)
+ if parse_err then
+ rspamd_logger.err(task, 'cannot parse JSON: %s', err)
+ maybe_defer(task)
+ return
+ end
+ local obj = parser:get_object()
+ local work_id = obj.work_id
+ if work_id then
+ task:get_mempool():set_variable('contextal_work_id', work_id)
+ end
+ task:insert_result(settings.submission_symbol, 1.0,
+ string.format('work_id=%s', work_id or 'nil'))
+ task:add_timer(settings.request_ttl, action_cb)
+ end
+
+ local req = {
+ object_data = {['data'] = task:get_content()},
+ }
+ if settings.request_ttl then
+ req.ttl = {['data'] = tostring(settings.request_ttl)}
+ end
+ if settings.max_recursion then
+ req.maxrec = {['data'] = tostring(settings.max_recursion)}
+ end
+ rspamd_http.request({
+ task = task,
+ url = settings.submit_url,
+ body = lua_util.table_to_multipart_body(req, static_boundary),
+ callback = http_callback,
+ headers = {
+ ['Content-Type'] = string.format('multipart/form-data; boundary="%s"', static_boundary)
+ },
+ timeout = settings.http_timeout,
+ gzip = settings.gzip,
+ keepalive = settings.keepalive,
+ no_ssl_verify = settings.no_ssl_verify,
+ })
+end
+
+local function cache_hit(task, err, data)
+ if err then
+ rspamd_logger.err(task, 'error getting cache: %s', err)
+ else
+ process_cached(task, data)
+ end
+end
+
+local function submit_cb(task)
+ if cache_context then
+ redis_cache.cache_get(task,
+ task:get_digest(),
+ cache_context,
+ settings.cache_timeout,
+ submit,
+ cache_hit
+ )
+ else
+ submit(task)
+ end
+end
+
+local function set_url_path(base, path)
+ local slash = base:sub(#base) == '/' and '' or '/'
+ return base .. slash .. path
+end
+
+settings = lua_util.override_defaults(settings, opts)
+
+local res, err = config_schema:transform(settings)
+if not res then
+ rspamd_logger.warnx(rspamd_config, 'plugin %s is misconfigured: %s', N, err)
+ local err_msg = string.format("schema error: %s", res)
+ lua_util.config_utils.push_config_error(N, err_msg)
+ lua_util.disable_module(N, "failed", err_msg)
+ return
+end
+
+for _, k in ipairs(settings.custom_actions) do
+ contextal_actions[k] = true
+end
+
+if not settings.base_url then
+ if not (settings.submit_url and settings.actions_url) then
+ rspamd_logger.err(rspamd_config, 'no URL configured for contextal')
+ lua_util.disable_module(N, 'config')
+ return
+ end
+else
+ if not settings.submit_url then
+ settings.submit_url = set_url_path(settings.base_url, 'api/v1/submit')
+ end
+ if not settings.actions_url then
+ settings.actions_url = set_url_path(settings.base_url, 'api/v1/actions/')
+ end
+end
+
+redis_params = lua_redis.parse_redis_server(N)
+if redis_params then
+ cache_context = redis_cache.create_cache_context(redis_params, {
+ cache_prefix = settings.cache_prefix,
+ cache_ttl = settings.cache_ttl,
+ cache_format = 'json',
+ cache_use_hashing = false
+ })
+end
+
+local submission_id = rspamd_config:register_symbol({
+ name = settings.submission_symbol,
+ type = 'normal',
+ group = N,
+ callback = submit_cb
+})
+
+local top_options = rspamd_config:get_all_opt('options')
+if settings.request_ttl and settings.request_ttl >= (top_options.task_timeout * 0.8) then
+ rspamd_logger.warn(rspamd_config, [[request ttl is >= 80% of task timeout, won't wait on processing]])
+ use_request_ttl = false
+elseif not settings.request_ttl then
+ use_request_ttl = false
+end
+
+local parent_id
+if use_request_ttl then
+ parent_id = submission_id
+else
+ parent_id = rspamd_config:register_symbol({
+ name = settings.action_symbol_prefix,
+ type = 'postfilter',
+ priority = lua_util.symbols_priorities.high - 1,
+ group = N,
+ callback = action_cb
+ })
+end
+
+for k in pairs(contextal_actions) do
+ rspamd_config:register_symbol({
+ name = settings.action_symbol_prefix .. '_' .. k,
+ parent = parent_id,
+ type = 'virtual',
+ group = N,
+ })
+end
diff --git a/src/plugins/lua/gpt.lua b/src/plugins/lua/gpt.lua
index 98a3e38ee..5d1cf5e06 100644
--- a/src/plugins/lua/gpt.lua
+++ b/src/plugins/lua/gpt.lua
@@ -494,6 +494,7 @@ local function insert_results(task, result, sel_part)
rspamd_logger.errx(task, 'no probability in result')
return
end
+
if result.probability > 0.5 then
task:insert_result('GPT_SPAM', (result.probability - 0.5) * 2, tostring(result.probability))
if settings.autolearn then
@@ -504,10 +505,6 @@ local function insert_results(task, result, sel_part)
process_categories(task, result.categories)
end
else
- if result.reason and settings.reason_header then
- lua_mime.modify_headers(task,
- { add = { [settings.reason_header] = { value = 'value', order = 1 } } })
- end
task:insert_result('GPT_HAM', (0.5 - result.probability) * 2, tostring(result.probability))
if settings.autolearn then
task:set_flag("learn_ham")
@@ -516,6 +513,10 @@ local function insert_results(task, result, sel_part)
process_categories(task, result.categories)
end
end
+ if result.reason and settings.reason_header then
+ lua_mime.modify_headers(task,
+ { add = { [settings.reason_header] = { value = tostring(result.reason), order = 1 } } })
+ end
if cache_context then
lua_cache.cache_set(task, redis_cache_key(sel_part), result, cache_context)
@@ -958,14 +959,14 @@ if opts then
"FROM and url domains. Evaluate spam probability (0-1). " ..
"Output ONLY 3 lines:\n" ..
"1. Numeric score (0.00-1.00)\n" ..
- "2. One-sentence reason citing strongest red flag\n" ..
+ "2. One-sentence reason citing whether it is spam, the strongest red flag, or why it is ham\n" ..
"3. Primary concern category if found from the list: " .. table.concat(lua_util.keys(categories_map), ', ')
else
settings.prompt = "Analyze this email strictly as a spam detector given the email message, subject, " ..
"FROM and url domains. Evaluate spam probability (0-1). " ..
"Output ONLY 2 lines:\n" ..
"1. Numeric score (0.00-1.00)\n" ..
- "2. One-sentence reason citing strongest red flag\n"
+ "2. One-sentence reason citing whether it is spam, the strongest red flag, or why it is ham\n"
end
end
end
diff --git a/src/plugins/lua/hfilter.lua b/src/plugins/lua/hfilter.lua
index 6bc011b83..a783565ab 100644
--- a/src/plugins/lua/hfilter.lua
+++ b/src/plugins/lua/hfilter.lua
@@ -199,9 +199,10 @@ local function check_regexp(str, regexp_text)
return re:match(str)
end
-local function add_static_map(data)
+local function add_static_map(data, description)
return rspamd_config:add_map {
type = 'regexp_multi',
+ description = description,
url = {
upstreams = 'static',
data = data,
@@ -568,16 +569,16 @@ local function append_t(t, a)
end
end
if config['helo_enabled'] then
- checks_hello_bareip_map = add_static_map(checks_hello_bareip)
- checks_hello_badip_map = add_static_map(checks_hello_badip)
- checks_hellohost_map = add_static_map(checks_hellohost)
- checks_hello_map = add_static_map(checks_hello)
+ checks_hello_bareip_map = add_static_map(checks_hello_bareip, 'Hfilter: HELO bare ip')
+ checks_hello_badip_map = add_static_map(checks_hello_badip, 'Hfilter: HELO bad ip')
+ checks_hellohost_map = add_static_map(checks_hellohost, 'Hfilter: HELO host')
+ checks_hello_map = add_static_map(checks_hello, 'Hfilter: HELO')
append_t(symbols_enabled, symbols_helo)
timeout = math.max(timeout, rspamd_config:get_dns_timeout() * 3)
end
if config['hostname_enabled'] then
if not checks_hellohost_map then
- checks_hellohost_map = add_static_map(checks_hellohost)
+ checks_hellohost_map = add_static_map(checks_hellohost, 'Hfilter: HOSTNAME')
end
append_t(symbols_enabled, symbols_hostname)
timeout = math.max(timeout, rspamd_config:get_dns_timeout())
diff --git a/src/plugins/lua/phishing.lua b/src/plugins/lua/phishing.lua
index 3f5c9e634..4dc3fd924 100644
--- a/src/plugins/lua/phishing.lua
+++ b/src/plugins/lua/phishing.lua
@@ -39,7 +39,7 @@ local anchor_exceptions_maps = {}
local strict_domains_maps = {}
local phishing_feed_exclusion_map = nil
local generic_service_map = nil
-local openphish_map = 'https://www.openphish.com/feed.txt'
+local openphish_map = 'https://raw.githubusercontent.com/openphish/public_feed/refs/heads/main/feed.txt'
local phishtank_suffix = 'phishtank.rspamd.com'
-- Not enabled by default as their feed is quite large
local openphish_premium = false
diff --git a/test/functional/cases/001_merged/102_multimap.robot b/test/functional/cases/001_merged/102_multimap.robot
index 50d1af6b6..a16d0e5c4 100644
--- a/test/functional/cases/001_merged/102_multimap.robot
+++ b/test/functional/cases/001_merged/102_multimap.robot
@@ -418,6 +418,16 @@ MAP - EXTERNAL MISS
... Settings={symbols_enabled = [EXTERNAL_MULTIMAP]}
Do Not Expect Symbol EXTERNAL_MULTIMAP
+MAP - EXTERNAL CDB
+ Scan File ${MESSAGE} IP=127.0.0.1 Hostname=example.com
+ ... Settings={symbols_enabled = [EXTERNAL_MULTIMAP_CDB]}
+ Expect Symbol EXTERNAL_MULTIMAP_CDB
+
+MAP - EXTERNAL CDB MISS
+ Scan File ${MESSAGE} IP=127.0.0.1 Hostname=example.com.bg
+ ... Settings={symbols_enabled = [EXTERNAL_MULTIMAP_CDB]}
+ Do Not Expect Symbol EXTERNAL_MULTIMAP_CDB
+
MAP - DYNAMIC SYMBOLS - SYM1
Scan File ${MESSAGE} IP=127.0.0.1 Hostname=foo
... Settings={symbols_enabled = [DYN_TEST1,DYN_TEST2,DYN_MULTIMAP]}
diff --git a/test/functional/configs/merged-override.conf b/test/functional/configs/merged-override.conf
index 344e30786..e302e88fc 100644
--- a/test/functional/configs/merged-override.conf
+++ b/test/functional/configs/merged-override.conf
@@ -254,6 +254,14 @@ multimap {
}
}
+ EXTERNAL_MULTIMAP_CDB {
+ type = "hostname";
+ map = {
+ external = true;
+ cdb = "{= env.TESTDIR =}/configs/maps/domains.cdb";
+ }
+ }
+
DYN_MULTIMAP {
type = "hostname";
map = "{= env.TESTDIR =}/configs/maps/dynamic_symbols.map";