diff --git a/README.md b/README.md index 76f0203..ae9ef27 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,20 @@ sudo mkdir -p /usr/local/inspamity # Clone the repository (or copy your files) sudo git clone https://github.com/dtump/inspamity.git /usr/local/inspamity -# Set proper permissions -sudo chmod +x /usr/local/inspamity/email_ai_interface.py -sudo chmod +x /usr/local/inspamity/cli_toolbox.py +# Set executable permissions +sudo chmod 0755 /usr/local/inspamity/email_ai_interface.py +sudo chmod 0755 /usr/local/inspamity/cli_toolbox.py # Create a virtual environment and install dependencies sudo python3 -m venv /usr/local/inspamity/.venv sudo /usr/local/inspamity/.venv/bin/pip install /usr/local/inspamity + +# Create system config and private debug locations +sudo install -d -o root -g _rspamd -m 0750 /etc/inspamity +sudo install -d -o _rspamd -g _rspamd -m 0700 /var/local/inspamity +sudo cp /usr/local/inspamity/config.ini.default /etc/inspamity/config.ini +sudo chown root:_rspamd /etc/inspamity/config.ini +sudo chmod 0640 /etc/inspamity/config.ini ``` ### Step 2: Install the rspamd integration @@ -65,7 +72,8 @@ sudo cp /usr/local/inspamity/rspamd/external_ai_test.lua /etc/rspamd/plugins.d/ # Create /etc/rspamd/modules.d/external_ai_test.conf echo -e 'external_ai_test {\n enabled = true;\n}' | sudo tee /etc/rspamd/modules.d/external_ai_test.conf -# Restart rspamd to apply changes +# Verify rspamd config, then restart rspamd to apply changes +sudo rspamadm configtest sudo systemctl restart rspamd ``` @@ -81,24 +89,32 @@ sudo tail -f /var/log/rspamd/rspamd.log ### Step 4: Enable debugging -```bash -# Make directory for debugging data -mkdir /var/local/inspamity +Debug mode can save raw email, processed email, AI output, and error logs. Treat these files as private mail data. -# Make _rspamd owner of this directory -chown _rspamd: /var/local/inspamity +```bash +# The installation step above creates this directory; these commands are safe to re-run. +sudo install -d -o _rspamd -g _rspamd -m 0700 /var/local/inspamity ``` -After this, enable debugging in the config.ini +After this, set `debug_mode = true` in `/etc/inspamity/config.ini` if you really need debug artifacts. Inspamity creates new debug files with mode `0600`. ## ⚙️ Configuration -Copy `config.ini.default` to `config.ini` (or `/etc/inspamity/config.ini` for system-wide) and edit it: +Copy `config.ini.default` to `/etc/inspamity/config.ini` for system-wide production use and edit it. Keep it readable only by root and the rspamd runtime user because it contains provider API keys: + +```bash +sudo chown root:_rspamd /etc/inspamity/config.ini +sudo chmod 0640 /etc/inspamity/config.ini +sudo -u _rspamd test -r /etc/inspamity/config.ini +``` ```ini [settings] # AI provider: anthropic or openai provider = anthropic +# Save private debug artifacts under debug_directory when true +debug_mode = false +debug_directory = /var/local/inspamity [anthropic] api_key = your_api_key_here @@ -130,7 +146,7 @@ By default, the rspamd Lua script is configured to: - Apply a score based on the AI's confidence level - Log detailed information for debugging -You can adjust these settings by editing `/etc/rspamd/local.d/external_ai_test.lua` to fit your needs. +You can adjust these settings by editing the installed plugin at `/etc/rspamd/plugins.d/external_ai_test.lua` to fit your needs. ## 📊 How It Works @@ -144,7 +160,10 @@ You can adjust these settings by editing `/etc/rspamd/local.d/external_ai_test.l - Check rspamd logs for errors: `sudo tail -f /var/log/rspamd/rspamd.log` - Test email processing directly: `/usr/local/inspamity/.venv/bin/python3 /usr/local/inspamity/email_ai_interface.py email.eml` -- Verify permissions on all scripts and directories +- Verify permissions on all scripts and directories: + - `/etc/inspamity` should be `0750 root:_rspamd` + - `/etc/inspamity/config.ini` should be `0640 root:_rspamd` + - `/var/local/inspamity` should be `0700 _rspamd:_rspamd` - Ensure all dependencies are properly installed ## 📜 License diff --git a/config.ini.default b/config.ini.default index 4e1be1a..581f200 100644 --- a/config.ini.default +++ b/config.ini.default @@ -1,9 +1,14 @@ +# Production config is normally /etc/inspamity/config.ini. +# Recommended permissions: /etc/inspamity 0750 root:_rspamd, config.ini 0640 root:_rspamd. + [settings] # AI provider to use: anthropic or openai provider = anthropic # Enable debug mode to save copies of processed emails and AI output debug_mode = false -# Directory where debug files will be stored +# Directory where debug files will be stored. +# Treat this as private mail data; recommended permissions: 0700 _rspamd:_rspamd. +# New debug files are created with mode 0600. debug_directory = /var/local/inspamity [anthropic] diff --git a/email_ai_interface.py b/email_ai_interface.py index 86510a3..16630bc 100755 --- a/email_ai_interface.py +++ b/email_ai_interface.py @@ -1,14 +1,55 @@ #!/usr/bin/env python3 import json +import os import sys import traceback from datetime import datetime from pathlib import Path +from typing import Any from email_utils.ai_spam_check import check_spam_with_ai from email_utils.config import load_config from email_utils.process_email import format_email_content, get_email_content +DEBUG_DIR_MODE = 0o700 +DEBUG_FILE_MODE = 0o600 + + +def prepare_debug_directory(debug_dir: str) -> Path: + """Create and lock down the debug directory for private mail artifacts.""" + debug_path = Path(debug_dir) + debug_path.mkdir(parents=True, mode=DEBUG_DIR_MODE, exist_ok=True) + debug_path.chmod(DEBUG_DIR_MODE) + return debug_path + + +def write_private_text(path: Path, content: str) -> None: + """Write a debug text file without allowing group/other access.""" + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + fd = os.open(path, flags, DEBUG_FILE_MODE) + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + fd = -1 + f.write(content) + finally: + if fd != -1: + os.close(fd) + path.chmod(DEBUG_FILE_MODE) + + +def write_private_json(path: Path, payload: dict[str, Any]) -> None: + """Write a debug JSON file without allowing group/other access.""" + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + fd = os.open(path, flags, DEBUG_FILE_MODE) + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + fd = -1 + json.dump(payload, f, indent=2) + finally: + if fd != -1: + os.close(fd) + path.chmod(DEBUG_FILE_MODE) + def main() -> None: try: @@ -32,12 +73,10 @@ def main() -> None: raise ValueError("Empty email input received") if debug_mode: - debug_path = Path(debug_dir) - debug_path.mkdir(parents=True, exist_ok=True) + debug_path = prepare_debug_directory(debug_dir) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") - with open(debug_path / f"raw_email_{timestamp}.eml", "w", encoding="utf-8") as f: - f.write(email_content) + write_private_text(debug_path / f"raw_email_{timestamp}.eml", email_content) # Process the email processed_email = get_email_content(email_content, is_string=True) @@ -47,11 +86,8 @@ def main() -> None: ai_result = check_spam_with_ai(formatted_output) if debug_mode: - with open(debug_path / f"processed_email_{timestamp}.txt", "w", encoding="utf-8") as f: - f.write(formatted_output) - - with open(debug_path / f"ai_output_{timestamp}.json", "w", encoding="utf-8") as f: - json.dump(ai_result, f, indent=2) + write_private_text(debug_path / f"processed_email_{timestamp}.txt", formatted_output) + write_private_json(debug_path / f"ai_output_{timestamp}.json", ai_result) print(json.dumps(ai_result)) sys.exit(0) @@ -59,9 +95,10 @@ def main() -> None: except Exception as e: if "debug_mode" in locals() and debug_mode and "debug_path" in locals(): error_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") - with open(debug_path / f"error_{error_timestamp}.log", "w", encoding="utf-8") as f: - f.write(f"Error: {e}\n\n") - f.write(traceback.format_exc()) + write_private_text( + debug_path / f"error_{error_timestamp}.log", + f"Error: {e}\n\n{traceback.format_exc()}", + ) error_json = {"error": True, "message": str(e), "timestamp": datetime.now().isoformat()} print(json.dumps(error_json), file=sys.stderr) diff --git a/tests/test_email_ai_interface.py b/tests/test_email_ai_interface.py new file mode 100644 index 0000000..6b10e06 --- /dev/null +++ b/tests/test_email_ai_interface.py @@ -0,0 +1,44 @@ +import json +import stat + +from email_ai_interface import prepare_debug_directory, write_private_json, write_private_text + + +def mode(path): + return stat.S_IMODE(path.stat().st_mode) + + +def test_prepare_debug_directory_uses_private_mode(tmp_path): + debug_dir = tmp_path / "debug" + + prepare_debug_directory(str(debug_dir)) + + assert mode(debug_dir) == 0o700 + + +def test_prepare_debug_directory_tightens_existing_mode(tmp_path): + debug_dir = tmp_path / "debug" + debug_dir.mkdir(mode=0o755) + + prepare_debug_directory(str(debug_dir)) + + assert mode(debug_dir) == 0o700 + + +def test_write_private_text_uses_private_mode(tmp_path): + debug_file = tmp_path / "raw_email_test.eml" + + write_private_text(debug_file, "private mail") + + assert debug_file.read_text() == "private mail" + assert mode(debug_file) == 0o600 + + +def test_write_private_json_uses_private_mode(tmp_path): + debug_file = tmp_path / "ai_output_test.json" + payload = {"is_spam": "no", "confidence": 99, "reason": "test"} + + write_private_json(debug_file, payload) + + assert json.loads(debug_file.read_text()) == payload + assert mode(debug_file) == 0o600