diff --git a/agent360/plugins/rspamd.py b/agent360/plugins/rspamd.py new file mode 100644 index 0000000..dedb9be --- /dev/null +++ b/agent360/plugins/rspamd.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""agent360 Rspamd monitor plugin. + +Returned metric groups: +- Health: available, auth_ok, read_only, version, uptime_seconds +- Volume: scanned, learned +- Mail classification: spam_count, ham_count +- Actions: reject_count, soft_reject_count, greylist_count, + add_header_count, rewrite_subject_count, no_action_count +- Ratios: spam_ratio_pct, reject_ratio_pct, greylist_ratio_pct +- Performance: scan_time_avg_sec, scan_time_last_sec + +Configuration section example: +[rspamd] +enabled = yes + +Notes: +- This plugin intentionally omits low-value/raw internals to keep payloads + compact for monitoring. +""" + +from __future__ import print_function, unicode_literals + +import json +import os +import re +import subprocess + +import plugins + + +class Plugin(plugins.BasePlugin): + __name__ = 'rspamd' + + @staticmethod + def _which(command): + # Compatible replacement for shutil.which for older Python versions. + path = os.environ.get('PATH', '') + if not path: + return None + + is_windows = os.name == 'nt' + exts = [''] + if is_windows: + pathext = os.environ.get('PATHEXT', '.EXE;.BAT;.CMD;.COM') + exts = [ext.lower() for ext in pathext.split(os.pathsep) if ext] + + for folder in path.split(os.pathsep): + folder = folder.strip('"') + if not folder: + continue + + candidate = os.path.join(folder, command) + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + + if is_windows and not os.path.splitext(command)[1]: + for ext in exts: + candidate_ext = candidate + ext + if os.path.isfile(candidate_ext) and os.access(candidate_ext, os.X_OK): + return candidate_ext + return None + + def _command(self, subcommand): + rspamc_path = self._which('rspamc') + if not rspamc_path: + return None + + cmd = [rspamc_path] + cmd.extend([subcommand, '-j']) + return cmd + + @staticmethod + def _to_text(raw_value): + if isinstance(raw_value, bytes): + try: + return raw_value.decode('utf-8') + except Exception: + return raw_value.decode('utf-8', 'replace') + return raw_value or '' + + @staticmethod + def _extract_json_text(text): + if not text: + return None + + start = text.find('{') + end = text.rfind('}') + if start == -1 or end == -1 or end < start: + return None + + return text[start:end + 1] + + @staticmethod + def _sanitize_json_text(text): + if not text: + return text + + # rspamc may emit non-JSON float tokens such as lowercase nan/inf. + return re.sub(r'(?