Training an ANN on dense provider features (fasttext_embed, text_hash)
could silently produce a degenerate model: with the historical
learning_rate=0.01 default, RMSprop drives the net into tanh saturation
depending on weight init luck - the loss freezes, yet the constant
all-one-class model is saved and classifies every message as spam (or
ham) until the next retrain. On a real corpus this happened in roughly
one of three weight inits.
Fixes:
* use the embedding (funnel) architecture for any rule with dense
feature providers, not only LLM ones: the simple symbol architecture
applies ReLU directly to the input, clipping the negative half of the
embedding space, and is the least stable option on such vectors
(it is also less accurate; layernorm in the funnel fixes the
conditioning)
* resolve the learning_rate default by input type: 0.01 for symbol
vectors as before, 0.001 for dense embeddings, which converges
reliably with equal accuracy; an explicit config value still wins
* add a quality gate to the training child: a model with constant or
single-class output on its own training set is rejected instead of
saved; the lock is released and training retries on the next cycle
with a different weight init, which converges in practice
* return an explicit msgpack rejection marker from the training child
instead of nil on the gate/NaN paths: a nil return used to deadlock
the controller against the training subprocess (see the lua_worker
fix) and stalled training forever
* [Feature] mx_check: three-layer cache rewrite
This is the comprehensive implementation behind issue #6032. The single-
layer cache from previous shape is replaced by a three-layer Redis design
(d:<domain> / m:<mxhost> / i:<ip>) under <key_prefix>:. Short-code wire
formats minimise Redis footprint; per-layer validators
(is_valid_cache_value) treat unrecognised entries as a cache miss;
the resolve / probe path that follows then issues a fresh cache_set at
the same key, overwriting the bad entry in place.
Probe coordination
- SET NX EX claims the i:<ip> probe lock; a post-claim GET disambiguates
held lock, already-published verdict, and corrupted-value-needing-heal
cases. A separate force_claim_probe_lock path overwrites corrupted
values to break the SET NX loop without leaking refcounts.
- Redis errors during the lock claim surface as MX_REDIS_ERROR; lock held
by another worker surfaces as MX_INFLIGHT and skips duplicated TCP
connections which under high-load would result in DoS like activity
from the target side and most likely will negatively impact Rspamd's
user IP/ASN/Org reputation.
DNS / probe model
- Dual-stack via probe_ipv4 / probe_ipv6 / prefer_ipv6 with family-tagged
cache values (v4: / v6: / v64:) and coverage checks so flipping the
probe-family set re-resolves only as needed.
- Real DNS path failures (SERVFAIL / REFUSED / timeout) are distinguished
from authoritative NXDOMAIN / NOREC via is_dns_real_failure; the former
surface as MX_DNS_FAIL (cached as 'df') so a recovered resolver path
can be re-tried promptly. NXDOMAIN/NOREC collapse into MX_NONE.
- step3 partitions resolved IPs into PUBLIC / LOCAL (RFC1918 / CGNAT /
ULA) / BOGON (loopback, TEST-NET, multicast, link-local, etc.). Only
PUBLIC IPs reach the TCP probe. MX_LOCAL_ONLY / MX_LOCAL_MIX /
MX_BOGON_ONLY / MX_BOGON_MIX fire with the offending IPs as options.
test_mode lifts loopback out of the bogon set so the probe path can be
exercised against 127.0.0.1.
Symbol surface
- Multi-source: check_from / check_mime_from / check_reply_to with
envelope > reply-to > mime-from priority dedup if same domain is hitting
MX checks from different sources. Per-source prefixes
(symbol_prefix_from / symbol_prefix_mime_from / symbol_prefix_reply_to)
fan every MX_* symbol across the three sources at registration time.
- A-fallback path (no MX RR, A used as implicit MX per RFC 5321 §5.1)
has its own MX_A_* symbol family so operators can score it
independently of the MX-RR path.
- Per-outcome greylist and reject gates (greylist_invalid /
greylist_none / greylist_broken / ..., reject_null_mx with
reject_authorized / reject_local kill switches); null-MX domains can
now trigger a real set_pre_result. reject_nxdomain_mx removed
as bad option to serve, practically nxdomain reject would be good only
on eTLD+1.
- Probe-outcome symbols (MX_GOOD / MX_TIMEOUT_* / MX_REFUSED /
MX_INVALID / MX_ERROR / MX_INFLIGHT) populate the option field with
the MX hostname; IP-class symbols still carry IPs since that's where
IP information is the point. MX_REDIS_ERROR has no option (it's a
module-internal signal).
- New punishment maps: bad_mxs (glob on MX hostnames) and bad_ips
(radix on resolved IPs). Any hit short-circuits with MX_BAD /
MX_IP_BAD before any TCP probe runs which allows to punish
domains which shares same MX infra.
Scoring
- set_metric_all_sources ships sensible defaults for every symbol.
Operators can tune any weight through the new "mx" group in
conf/groups.conf via local.d/mx_group.conf or override.d/
mx_group.conf without touching the module.
Functional tests
- 167_mx_check.robot refreshed for the new symbol set; MX_NONE replaces
MX_NXDOMAIN/MX_MISSING, MX_A_REFUSED covers the closed-port
A-fallback case, and MX_BAD / MX_IP_BAD have dedicated assertions.
- 168_mx_check_greeting.robot covers verify_greeting=true /
send_quit=false: silent listener -> MX_TIMEOUT_READ; continuation
220- with no follow-up held past read_timeout -> MX_GOOD (a
regression that re-queued reads under send_quit=false would surface
as MX_TIMEOUT_READ); 5xx greeting -> MX_ERROR; non-SMTP line ->
MX_INVALID.
- 169_mx_check_greeting_quit.robot covers verify_greeting=true /
send_quit=true: proper multi-line timing -> MX_GOOD plus dummy
status file QUIT_AFTER_FINAL (catches a regression where QUIT is
sent before the final 220 line, which rspamd's verdict alone cannot
detect); slow second line -> MX_TIMEOUT_READ.
- util/dummy_smtp.py mock with silent / error / messy / greeting_single
/ greeting_multi modes and a --status-file argument for out-of-band
timing verification.
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
* [Feature] mx_check: optional per-entry weight multiplier for bad_mxs / bad_ips
Both bad_mxs (glob) and bad_ips (radix) entries can now carry an optional numeric second token that is read as a weight multiplier on top of the MX_BAD / MX_IP_BAD group score. Examples: `trapmx.example.com 3` triples the weight; `1.2.3.4 0.5` halves it. Default multiplier is 1.0 (no value or non-numeric value). Lets operators tier confidence within a single map without maintaining several.
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
* [Fix] Use static parent callback in mx_check module
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
* [Fix] Add missing executable flag on dummy_smtp python script
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
* [Chore] Add group to parent mx_check symbol
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
* [Fix] change rspamd_config:add_map to lua_maps so inline maps works too, adjust autotests so they survive parallelism
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
---------
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
Co-authored-by: Vsevolod Stakhov <vsevolod@rspamd.com>
Switch the default "rspamd.com" rule from a hardcoded round-robin host
list to SRV-based discovery. "service=fuzzy+rspamd.com" makes the
upstream parser resolve the _fuzzy._tcp.rspamd.com SRV record, so
backends and ports are managed entirely in DNS with no client-side
config change.
The legacy fuzzy1/fuzzy2 hostnames keep resolving to every live
backend, so existing installs that pinned the old round-robin string
are unaffected. See rspamd/dns#8.
Phase C of #6032 (IPv6 probing deferred):
IP-class classification. Resolved MX-target IPs are partitioned into
PUBLIC / LOCAL / BOGON against fixed RFC range sets. LOCAL (RFC1918,
CGNAT, ULA) is unprobeable from our vantage point; BOGON (loopback,
link-local, TEST-NET, multicast, reserved) has no legitimate meaning as
an MX target and is a packet-injection footgun. Only PUBLIC addresses
are probed; the rest emit MX_LOCAL_ONLY/MIX and MX_BOGON_ONLY/MIX. The
range sets are a correctness invariant and are not operator-tunable.
Per-layer trust/skip maps. exclude_mxs is a glob map of trusted MX
hostnames; a hit short-circuits the whole check with MX_WHITE. exclude_ips
is a radix map of IPs dropped from the probe set; if it empties the set,
MX_SKIP fires.
Run-scope toggles. check_authorized and check_local (both default false)
control whether authenticated and local-network senders are checked,
replacing the previous hardcoded skip.
test_mode (testing only) lifts loopback out of the bogon set so the probe
path stays exercisable against a local listener; functional tests use it.
The IPv4-mapped range ::ffff:0:0/96 is intentionally excluded from the
bogon set: rspamd's radix stores IPv4 as its v4-mapped form, so listing
that prefix would classify all IPv4 traffic as bogon.
Refs #6032.
Fix three defects found in review of the Phase A rework:
- step2/step3: a non-working probe verdict for one MX host ended the
whole lookup instead of trying the remaining MX records. Domains with
a refused/timed-out primary MX and a reachable backup MX were scored
MX_INVALID instead of MX_GOOD. step3 now hands its verdict to a
continuation; step2 walks the MX list in priority order and only
emits a failure after every selected host fails. Also stop caching a
broken-MX domain under d: as 'nxd' (it would later be misreported as
NXDOMAIN).
- A-fallback: a NODATA/empty A response was cached and reported as
NXDOMAIN. nxdomain is now returned only for a genuine DNS_ERR_NXDOMAIN;
domains that exist but publish neither MX nor A emit a missing/invalid
outcome and write no d: cache entry.
- Legacy aliases: the shipped modules.d/mx_check.conf set connect_timeout
and verify_greeting, so the merged config always carried them and the
`timeout`/`wait_for_greeting` aliases were silently ignored. Drop those
keys from the shipped file (kept as documented comments); warn when a
legacy key and its replacement are both set.
Add a functional test for the NODATA case.
Refs #6032.
Replaces the single domain-keyed cache with three namespaces — `<key_prefix>`
for the per-domain MX/A-fallback verdict, `<key_prefix>Ⓜ️` for per-MX-host A
records, and `<key_prefix>` for per-IP probe verdicts. Two domains pointing
at a shared MX host (every G-Suite / M365 tenant, every ESP customer) now share
the m-layer and i-layer entries, so the second domain hits cache at every step
and emits its symbol with zero new DNS or TCP work.
Splits the probe into two clean shapes — pure connect-only and full SMTP banner
validation — using the new `lua_tcp` options merged in #6034. `verify_greeting`
+ `send_quit` replace the conflated `wait_for_greeting`; banner parsing
honours multi-line greetings (RFC 5321 §4.2.1), validates the reply code, and
distinguishes 220 success, 4xx/5xx rejection (real SMTP, `MX_ERROR`), and
non-SMTP listeners (`MX_INVALID`).
Adds informational symbols at score 0: `MX_REFUSED`, `MX_TIMEOUT_CONNECT`,
`MX_TIMEOUT_READ`, `MX_ERROR`, `MX_NXDOMAIN`, `MX_NULL` (RFC 7505 detection),
`MX_BROKEN` (every MX RR points at an unresolvable host). Primary symbols
(`MX_GOOD` / `MX_INVALID` / `MX_MISSING` / `MX_WHITE`) keep today's scores —
operator-visible behaviour is preserved, the new symbols are emitted alongside
for tuning data ahead of Phase B's two-path matrix.
Legacy keys are honoured with deprecation warnings: `timeout` maps to
`connect_timeout`, `wait_for_greeting` maps to `verify_greeting`. Adds a `port`
setting (default 25) so the module is testable on non-privileged ports.
Functional tests in test/functional/cases/167_mx_check.robot cover Null MX,
NXDOMAIN, broken-reference MX, connect-refused, and the A-fallback path.
Refs #6032.
* [Feature] elastic: log Reply-To, received IPs, URL metadata, and pre-result module
- reply_to_user / reply_to_domain: parsed from Reply-To via
rspamd_util.parse_mail_address, mirroring the from / mime_from split.
- received_ips: list of IPs from Received headers
- urls and urls_cta with the new collect_urls config block: per-URL
records {url, etld, host, protocol, flags, count} plus aggregate
metrics {total, unique, max_repeats, repeat_ratio}. CTA URLs are
collected via text_part:get_cta_urls({original=true}) and walked via
:get_redirected so url_redirector-resolved hops are captured, then
either kept inline at the top of urls (sorted ahead of non-CTA so
they survive max_urls truncation) or emitted into a dedicated
urls_cta when separate_cta is on
- action_forced: the module name from task:has_pre_result(), so logs
show which prefilter short-circuited the pipeline (or 'no force').
Renames get_received_delay to get_received_info (returns delay + ips
in one pass over the received chain) and replaces the local
merge_settings helper with lua_util.override_defaults — the two are
functionally equivalent recursive deep-merges, but override_defaults
is the project-wide maintained helper.
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
* [Fix] elastic: reset queue counters on pop drain which prevents the indices from accumulating monotonically over the worker's lifetime
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
* [Fix] elastic: address review feedback on PR #6018
- Drop tostring() around url:get_text() (already a Lua string) in
url_to_record and url_key.
- Drop tostring() around url:get_flags_num() (.. coerces numbers).
- Replace tostring(url) in CTA dedup key with url:get_text() to avoid
the __tostring metamethod's percent-encoding two-pass walk.
- Drop `or nil` no-op after url:get_redirected().
- Cache url:get_host() once in url_to_record (was called twice).
- Remove dead `if on then` guard on url:get_flags() — only set bits
are inserted, so every value is true.
- Cache tostring(real_ip) in get_received_info and tostring(ip_addr) /
tostring(origin_ip) in get_general_metadata; refactor to one call.
- In build_urls_metadata, compute url_key(u, false) once per URL and
reuse for the CTA lookup; only recompute when full_urls is true.
- Drop sort=true from task:get_urls() — the C-level qsort doesn't
survive: results are rehashed for dedup and re-sorted by count.
Also remove the misleading "deterministic order, stable dedup"
comment (table.sort is unstable in standard Lua).
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
* [Fix] elastic: drop dead `or {}` after task:get_urls() and other functions that always provide table
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
---------
Signed-off-by: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
Call rspamd_worker_check_and_adjust_timeout during configtest so
misconfigured plugin timeouts are reported at configuration validation
time. Elevate the diagnostic from info to warning level.
Fix rspamd_symcache_add_symbol_augmentation to parse "key=value" format
in augmentation strings, allowing numeric timeout augmentations from
Lua plugins to be stored and compared correctly.
Clarify mx_check timeout comment to explain that the effective symbol
timeout includes dns.timeout in addition to the configured value.
settings.timeout (greylisting period, 5 min) was being picked up by
lua_redis as the Redis connection timeout, inflating the symbol's
augmentation timeout to 300s. Add redis_timeout (default 1.0s) and
explicitly set redis_params.timeout after parse_redis_server.
Both bl.score.senderscore.com and score.senderscore.com require
a registered MyValidity account to function. Unregistered IPs
receive 127.255.255.255 (blocked) for all queries, making the
RBLs non-functional without prior account setup regardless of
query volume.
Disable senderscore_reputation (score.senderscore.com) by default
and update the senderscore (bl.score.senderscore.com) comment to
reflect the actual registration requirement. Users must register
their querying IPs at https://my.validity.com before enabling
either RBL.
Convert hardcoded suspicious TLDs list to a proper map file following
rspamd's standard map loading pattern with fallback support.
Changes:
- Add conf/maps.d/suspicious_tlds.inc with default TLDs (.tk, .ml, .ga, .cf, .gq)
- Update url_suspect.conf to use fallback+file:// pattern for user overrides
- Update url_suspect.lua to load TLDs via rspamd_map_add_from_ucl()
Users can now:
- Override entirely: create local.d/maps.d/suspicious_tlds.inc
- Extend defaults: create local.d/maps.d/suspicious_tlds.inc.local
- Disable: set suspicious_tlds_map = null in local.d/url_suspect.conf
Supersedes #5864 - the map-based approach inherently handles nil/missing
config gracefully, making the type check unnecessary.
Add integrated autolearn system for neural networks with LLM providers:
- New lua_neural_learn library with guards system and rspamd_expression
support for complex conditions
- Expression-based conditions: spam_condition, ham_condition using
rspamd_expression syntax (e.g., "BAYES_SPAM & DMARC_POLICY_REJECT")
- Score, action, and symbol-based thresholds
- Pluggable guards via rspamd_plugins['neural'].autolearn hooks
- Mempool-based flag passing (no double scanning)
- Probabilistic sampling for training volume control
Also includes contrib/neural-embedding-service with a FastEmbed-based
Python service for CPU-optimized embedding inference, compatible with
both Ollama and OpenAI API formats.
Configuration example:
autolearn {
enabled = true;
spam_score = 15.0;
spam_condition = "BAYES_SPAM & (DMARC_POLICY_REJECT | RBL_SPAMHAUS)";
ham_condition = "BAYES_HAM & DKIM_VALID_AU & SPF_PASS";
}
The option name max_score was confusing as it doesn't refer to the
symbol score but rather the number of fuzzy hash hits at which the
normalized score reaches ~1.0 (formula: tanh(e * hits / hits_limit)).
- Rename max_score -> hits_limit in fuzzy_check.c and default config
- Add backward compatibility: max_score is still accepted as an alias
- Add lua_cfg_transform to handle legacy configs (max_score overrides
hits_limit to ensure local.d overrides work correctly)
- Add explanatory comments in config and documentation
Fixes#5768: Settings lookup was broken for subaddressed recipients
(e.g., user+folder@example.com) because the aliases plugin was
disabled by default after it was moved from rules/misc.lua in 3.14.
This restores the pre-3.14 behavior where plus-tags are stripped
and virtual recipients are created for settings matching.
Performance improvements for messages with many URLs:
1. O(1) TLD lookups: Convert builtin_suspicious list to hash set on init,
eliminates O(n*m) iteration (500k+ checks for 100k URLs × 5 TLDs)
2. Use rspamd_text for URL checks: get_text(true) returns opaque rspamd_text
without string copying, use text:find() for RTL detection
3. Use rspamd_ip API: parse_addr() + is_local() for IP checks instead of
pattern matching
4. Add max_urls limit (10000) for DoS protection
These optimizations significantly reduce memory allocation and CPU usage.
The url_suspect plugin had multiple critical issues:
1. R_SUSPICIOUS_URL triggered on every message with URLs, adding 25 points
due to incorrect dynamic score usage (5.0 * 5.0 instead of 1.0 * 5.0)
2. Broken compat_mode inserted R_SUSPICIOUS_URL without URL info whenever
ANY url check triggered, making it impossible to debug
3. Symbol names were unnecessarily configurable, adding complexity
4. url_suspect_group.conf was not included in groups.conf, so scores
were not loaded at all
Fixed by:
- Removed R_SUSPICIOUS_URL and compat_mode completely
- Fixed all insert_result() calls to use 1.0 dynamic weight
- Made symbol names hardcoded constants
- Added url group to groups.conf with max_score = 9.0
- Cleaned up score configuration parameters
- Changed comments from 'Uncomment to enable' to 'To enable, add in local.d/url_suspect.conf:'
- Users should not edit shipped config files directly
- Follow Rspamd convention: use local.d/override.d for user customizations
- Updated all map parameter comments for consistency
- Clearer path structure: use local.d/maps/ subdirectory
- Removed all use_pattern_map, use_range_map, use_tld_map, etc. flags
- Maps are now implicitly enabled if configured (not nil)
- Cleaner configuration: just uncomment the map parameter to enable
- Updated init_maps() to check map existence instead of enable flags
- Updated check functions to use maps if configured
- Simpler, more intuitive configuration approach
This commit implements a two-level URL processing system that addresses
issue #5731 and provides flexible URL analysis with multiple specific symbols.
Core changes:
* Modified src/libserver/url.c to handle oversized user fields (fixes#5731)
* Added lualib/lua_url_filter.lua - Fast library filter during parsing
* Added src/plugins/lua/url_suspect.lua - Deep inspection plugin
* Added conf/modules.d/url_suspect.conf - Plugin configuration
* Added conf/scores.d/url_suspect_group.conf - Symbol scores
Key features:
* No new C flags - uses existing URL flags (has_user, numeric, obscured, etc.)
* Works without maps - built-in logic for common cases
* 15+ specific symbols instead of generic R_SUSPICIOUS_URL
* Backward compatible - keeps R_SUSPICIOUS_URL working
* User extensible - custom filters and checks supported
Optional features:
* Example map files for advanced customization (disabled by default)
* Whitelist, pattern matching, TLD lists
Issue: #5731
- Replace manual Redis operations with lua_cache API for better consistency
- Use messagepack serialization and automatic key hashing
- Fix Leta Mullvad API URL to /search/__data.json endpoint
- Add search_engine parameter support
- Remove redundant 'or DEFAULTS.xxx' patterns (opts already has defaults merged)
- Add proper debug_module propagation throughout call chain
- Improve JSON parsing to handle Leta Mullvad's nested pointer structure
- Use local N = 'llm_search_context' idiom instead of constant string reuse
- Replace rspamd_logger.debugm with lua_util.debugm (rspamd_logger has no debugm method)
- Use extract_specific_urls instead of task:get_urls()
- Add task parameter to query_search_api for proper logging and HTTP requests
- Remove retry logic using non-existent rspamd_config:add_delayed_callback
- Simplify to single HTTP attempt with graceful failure
- Remove retry_count and retry_delay options from config
- New module llm_search_context.lua: extracts domains from email URLs and queries search API
- Integrated into gpt.lua with parallel context fetching (user + search)
- Redis caching with configurable TTL (default 1 hour)
- Retry logic with exponential backoff for search API failures
- Disabled by default for backward compatibility
- Configuration options in gpt.conf for customization
Add support for HTML structure fuzzy hashing in fuzzy_check plugin:
Core integration:
- Add FUZZY_CMD_FLAG_HTML flag and FUZZY_RESULT_HTML result type
- Add html_shingles, min_html_tags, html_weight options to fuzzy_rule
- Implement fuzzy_cmd_from_html_part() to generate HTML fuzzy commands
- Integrate into fuzzy_generate_commands() for automatic hash generation
- Handle HTML results with configurable weight multiplier
Configuration:
- html_shingles: enable/disable HTML fuzzy hashing per rule
- min_html_tags: minimum HTML tags threshold (default 10)
- html_weight: score multiplier for HTML matches (default 1.0)
Use cases:
1. Brand protection: detect phishing with copied HTML but fake CTA
2. Spam campaigns: group messages by HTML structure
3. Template detection: identify newsletters/notifications
4. Phishing: text match + HTML CTA mismatch = suspicious
Files added:
- lualib/lua_fuzzy_html.lua: helper functions for mismatch detection
- conf/modules.d/fuzzy_check_html.conf: configuration examples
- test/functional/configs/fuzzy_html_test.conf: test configuration
- rules/fuzzy_html_phishing.lua: phishing detection rules
HTML fuzzy works alongside text fuzzy:
- Both hashes generated and sent to storage
- Separate result types allow different handling
- CTA domain verification prevents false positives
Next steps:
- Performance testing on real email corpus
- Fine-tune weights and thresholds
- Collect legitimate brand templates for whitelisting
Implemented a category-based symbol system for hash lookup antivirus
scanners (MetaDefender and VirusTotal) to replace dynamic scoring:
- Added 4 symbol categories: CLEAN (-0.5), LOW (2.0), MEDIUM (5.0), HIGH (8.0)
- Replaced full_score_engines with threshold-based categorization (low_category, medium_category)
- Fixed symbol registration in antivirus.lua to use rule instead of config
- Updated cache format to preserve symbol category across requests
- Added backward compatibility for old cache format
- Added symbols registration and metric score assignment
- Updated configuration documentation with examples
The new system provides:
- Clear threat categorization instead of linear interpolation
- Proper symbol weights applied automatically
- Consistent behavior between MetaDefender and VirusTotal
- Cache that preserves symbol categories
Configuration example:
metadefender {
apikey = "KEY";
type = "metadefender";
minimum_engines = 3;
low_category = 5;
medium_category = 10;
}
* remove max_size as it was looking to rows elements count, not strings size in total, such check will be too much compute intensive
* increase default errors max_fail as usually elastic not recover so quickly and needs a bit more time