diff --git a/LAUNCHD_QUICK_START.md b/LAUNCHD_QUICK_START.md new file mode 100644 index 0000000..0269f89 --- /dev/null +++ b/LAUNCHD_QUICK_START.md @@ -0,0 +1,76 @@ +# Quick Start: launchd Service + +This is a quick reference for installing and running PrivaseeAI Security as a macOS background service. + +## Files Created + +1. **com.privaseeai.security.plist** - launchd configuration file +2. **LAUNCHD_SERVICE_GUIDE.md** - Complete installation and management guide +3. **src/privaseeai_security/daemon.py** - Alternative daemon entry point +4. **Orchestrator daemon mode** - Added `__main__` block to orchestrator.py + +## Quick Installation + +```bash +# 1. Install the package +pip install -e . + +# 2. Create required directories +sudo mkdir -p /var/log/privaseeai /opt/privaseeai +sudo chown $(whoami):staff /var/log/privaseeai /opt/privaseeai +chmod 700 /var/log/privaseeai + +# 3. Install the service +cp com.privaseeai.security.plist ~/Library/LaunchAgents/ + +# 4. Load and start +launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + +# 5. Verify it's running +launchctl list | grep com.privaseeai.security +tail -f /var/log/privaseeai/security.log +``` + +## Service Features + +✅ **Auto-start on boot** - Runs automatically when you log in +✅ **Auto-restart on crash** - Keeps running with 60-second minimum restart delay +✅ **Centralized logging** - All output goes to `/var/log/privaseeai/security.log` +✅ **User agent** - Runs as your user, not as root +✅ **Python module execution** - Runs via `python -m privaseeai_security.orchestrator` + +## Quick Commands + +```bash +# Status +launchctl list | grep com.privaseeai.security + +# Logs +tail -f /var/log/privaseeai/security.log + +# Restart +launchctl stop com.privaseeai.security + +# Unload +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist +``` + +## Test the Service + +```bash +# Run the complete test suite +python3 -m pytest tests/unit/test_launchd_service.py -v + +# Test running orchestrator manually +python3 -m privaseeai_security.orchestrator +# Press Ctrl+C to stop +``` + +## For Full Documentation + +See **LAUNCHD_SERVICE_GUIDE.md** for: +- Detailed installation steps +- Configuration options +- Troubleshooting guide +- Advanced customization +- Security considerations diff --git a/LAUNCHD_SERVICE_GUIDE.md b/LAUNCHD_SERVICE_GUIDE.md new file mode 100644 index 0000000..b1a08e5 --- /dev/null +++ b/LAUNCHD_SERVICE_GUIDE.md @@ -0,0 +1,426 @@ +# PrivaseeAI Security - launchd Service Installation Guide + +This guide explains how to install, configure, and manage the PrivaseeAI Security background service using macOS launchd. + +## Overview + +The `com.privaseeai.security.plist` file configures PrivaseeAI Security to run as a persistent background service that: + +- ✅ Runs `python -m privaseeai_security.orchestrator` automatically +- ✅ Starts on boot (user agent) +- ✅ Auto-restarts if crashed (with a minimum restart delay enforced by ThrottleInterval) +- ✅ Logs to `/var/log/privaseeai/security.log` +- ✅ Runs from `/opt/privaseeai` working directory +- ✅ Executes as the current user +- ✅ Prevents rapid restart loops with a fixed 60-second ThrottleInterval between restarts + +## Prerequisites + +1. **Python 3.11+** - Ensure a Python 3.11+ interpreter is available in your PATH (via Homebrew, venv, or system Python) +2. **PrivaseeAI Security** installed as a Python package +3. **Proper permissions** to write to `/var/log/privaseeai/` + +## Installation Steps + +### 1. Install PrivaseeAI Security Package + +First, ensure the package is installed in your Python environment: + +```bash +# From the repository root +pip install -e . + +# Verify installation +python3 -m privaseeai_security --version +``` + +### 2. Create Required Directories + +Create the log directory with proper permissions: + +```bash +# Create log directory +sudo mkdir -p /var/log/privaseeai + +# Set ownership to current user +sudo chown $(whoami):staff /var/log/privaseeai + +# Restrict permissions so only this user can access logs +chmod 700 /var/log/privaseeai +``` + +### 3. Set Up Working Directory + +Create and configure the working directory: + +```bash +# Create working directory +sudo mkdir -p /opt/privaseeai + +# Set ownership to current user +sudo chown $(whoami):staff /opt/privaseeai + +# Copy or link your configuration files +# (Optional - adjust based on your setup) +# cp config.yaml /opt/privaseeai/ +``` + +### 4. Configure Environment Variables (Optional) + +If you need Telegram alerts, set environment variables in the plist or create a configuration file: + +```bash +# Option 1: Edit the plist file to add your tokens +# Edit com.privaseeai.security.plist and add to EnvironmentVariables: +# TELEGRAM_BOT_TOKEN +# your_bot_token_here +# TELEGRAM_CHAT_ID +# your_chat_id_here + +# Option 2: Use a .env file in /opt/privaseeai/ +cat > /opt/privaseeai/.env << 'EOF' +TELEGRAM_BOT_TOKEN=your_bot_token_here +TELEGRAM_CHAT_ID=your_chat_id_here +EOF +chmod 600 /opt/privaseeai/.env +``` + +### 5. Install the launchd Service + +Copy the plist file to the user's LaunchAgents directory: + +```bash +# Copy plist to LaunchAgents +cp com.privaseeai.security.plist ~/Library/LaunchAgents/ + +# Verify the file is in place +ls -l ~/Library/LaunchAgents/com.privaseeai.security.plist +``` + +## Loading and Managing the Service + +### Load the Service + +Start the service immediately and enable auto-start on boot: + +```bash +# Load and start the service +launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + +# Alternative: Bootstrap (recommended on macOS 11+) +launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.privaseeai.security.plist +``` + +### Check Service Status + +Verify the service is running: + +```bash +# List loaded services and check if ours is running +launchctl list | grep com.privaseeai.security + +# Get detailed status +launchctl print gui/$(id -u)/com.privaseeai.security +``` + +Expected output for running service: +``` +12345 0 com.privaseeai.security +``` +(PID, exit code, label) + +### View Service Logs + +Monitor the service logs in real-time: + +```bash +# Tail the log file +tail -f /var/log/privaseeai/security.log + +# View last 100 lines +tail -n 100 /var/log/privaseeai/security.log + +# Search for errors +grep -i error /var/log/privaseeai/security.log +``` + +### Restart the Service + +If you need to restart the service: + +```bash +# Stop the service +launchctl stop com.privaseeai.security + +# Start the service (it will auto-restart due to KeepAlive) +launchctl start com.privaseeai.security + +# Or unload and reload +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist +launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist +``` + +### Stop and Unload the Service + +To completely stop and disable the service: + +```bash +# Unload the service (stops it and disables auto-start) +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist + +# Alternative: Bootout (recommended on macOS 11+) +launchctl bootout gui/$(id -u)/com.privaseeai.security +``` + +## Testing the Service + +### Basic Functionality Test + +1. **Load the service:** + ```bash + launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + ``` + +2. **Check if it's running:** + ```bash + launchctl list | grep com.privaseeai.security + ps aux | grep "privaseeai_security.orchestrator" + ``` + +3. **Verify logs are being written:** + ```bash + tail -f /var/log/privaseeai/security.log + ``` + + You should see startup messages like: + ``` + INFO - Starting PrivaseeAI Security Orchestrator daemon... + INFO - ✅ Orchestrator daemon started + ``` + +### Crash Recovery Test + +Test the auto-restart functionality: + +1. **Find the service PID:** + ```bash + launchctl list | grep com.privaseeai.security + # Note the PID (first column) + ``` + +2. **Kill the process:** + ```bash + kill -9 + ``` + +3. **Wait 60 seconds (ThrottleInterval), then check:** + ```bash + launchctl list | grep com.privaseeai.security + ``` + + The service should have a new PID, indicating it restarted automatically. + +4. **Check logs for restart:** + ```bash + grep -A 5 "Orchestrator daemon started" /var/log/privaseeai/security.log | tail -20 + ``` + +### Boot Persistence Test + +Test auto-start on boot: + +1. **Reboot your system** +2. **After login, check service status:** + ```bash + launchctl list | grep com.privaseeai.security + tail /var/log/privaseeai/security.log + ``` + +### Manual Invocation Test + +Test running the orchestrator manually (without launchd): + +```bash +# Run directly +cd /opt/privaseeai +python3 -m privaseeai_security.orchestrator + +# Should start and show logs in terminal +# Press Ctrl+C to stop +``` + +## Troubleshooting + +### Service Won't Start + +1. **Check Python installation:** + ```bash + which python3 + /usr/bin/python3 --version # Should be 3.11+ + ``` + +2. **Verify package installation:** + ```bash + python3 -c "import privaseeai_security; print(privaseeai_security.__version__)" + ``` + +3. **Check permissions:** + ```bash + ls -la /var/log/privaseeai/ + ls -la /opt/privaseeai/ + ``` + +4. **View system logs:** + ```bash + log show --predicate 'process == "launchd"' --last 5m | grep privaseeai + ``` + +### Service Crashes Immediately + +1. **Check error logs:** + ```bash + tail -50 /var/log/privaseeai/security.log + ``` + +2. **Test manual execution:** + ```bash + cd /opt/privaseeai + python3 -m privaseeai_security.orchestrator + ``` + +3. **Check for missing dependencies:** + ```bash + python3 -c "from privaseeai_security.orchestrator import ThreatOrchestrator" + ``` + +### Logs Not Appearing + +1. **Verify log directory permissions:** + ```bash + ls -la /var/log/privaseeai/ + ``` + +2. **Check if process can write:** + ```bash + sudo -u $(whoami) touch /var/log/privaseeai/test.log + rm /var/log/privaseeai/test.log + ``` + +3. **Ensure PYTHONUNBUFFERED is set** (already in plist) + +## Configuration Reference + +### Key launchd Properties + +- **Label**: `com.privaseeai.security` - Unique service identifier +- **RunAtLoad**: `true` - Start on boot/load +- **KeepAlive**: Configured to restart on any exit +- **ThrottleInterval**: `60` seconds - Prevents rapid restart loops +- **WorkingDirectory**: `/opt/privaseeai` - Where service runs from +- **StandardOutPath/StandardErrorPath**: `/var/log/privaseeai/security.log` - Log location +- **LimitLoadToSessionType**: `Aqua` - User agent (not system daemon) + +### Environment Variables + +The plist includes: +- `PATH`: Standard system PATH +- `PYTHONUNBUFFERED`: `1` - Force unbuffered output for immediate logs + +## Advanced Configuration + +### Custom Python Interpreter + +If you use a virtual environment: + +1. **Edit the plist file:** + ```xml + ProgramArguments + + /path/to/your/venv/bin/python3 + -m + privaseeai_security.orchestrator + + ``` + +2. **Reload the service:** + ```bash + launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist + launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + ``` + +### Custom Backup Path + +Modify the plist to pass backup path as environment variable: + +```xml +EnvironmentVariables + + + PRIVASEE_BACKUP_PATH + /path/to/ios/backups + +``` + +Then update orchestrator.py to read this environment variable. + +### Adjust Throttle Interval + +Change crash recovery timing: + +```xml +ThrottleInterval +120 +``` + +## Uninstallation + +To completely remove the service: + +```bash +# 1. Unload the service +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist + +# 2. Remove the plist file +rm ~/Library/LaunchAgents/com.privaseeai.security.plist + +# 3. (Optional) Remove log files +sudo rm -rf /var/log/privaseeai/ + +# 4. (Optional) Remove working directory +sudo rm -rf /opt/privaseeai/ +``` + +## Security Considerations + +1. **Never store secrets in the plist file** - Use environment files or macOS Keychain +2. **Limit log file permissions** - Only the service user should read/write logs +3. **Regularly rotate logs** - Use `newsyslog` or similar tools +4. **Monitor for unusual behavior** - Check logs regularly for errors + +## Support + +For issues or questions: +- Check logs: `/var/log/privaseeai/security.log` +- GitHub Issues: https://github.com/aurelianware/PrivaseeAI.Security/issues +- Documentation: See README.md and USER_GUIDE.md + +## Quick Reference Card + +```bash +# Install +cp com.privaseeai.security.plist ~/Library/LaunchAgents/ +launchctl load ~/Library/LaunchAgents/com.privaseeai.security.plist + +# Status +launchctl list | grep com.privaseeai.security + +# Logs +tail -f /var/log/privaseeai/security.log + +# Restart +launchctl stop com.privaseeai.security + +# Uninstall +launchctl unload ~/Library/LaunchAgents/com.privaseeai.security.plist +rm ~/Library/LaunchAgents/com.privaseeai.security.plist +``` diff --git a/com.privaseeai.security.plist b/com.privaseeai.security.plist new file mode 100644 index 0000000..aea6044 --- /dev/null +++ b/com.privaseeai.security.plist @@ -0,0 +1,65 @@ + + + + + + Label + com.privaseeai.security + + + ProgramArguments + + /usr/bin/env + python3 + -m + privaseeai_security.orchestrator + + + + WorkingDirectory + /opt/privaseeai + + + RunAtLoad + + + + KeepAlive + + + SuccessfulExit + + + + + + ThrottleInterval + 60 + + + StandardOutPath + /var/log/privaseeai/security.log + + StandardErrorPath + /var/log/privaseeai/security.log + + + EnvironmentVariables + + PATH + /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + PYTHONUNBUFFERED + 1 + + + + Nice + 0 + + + LimitLoadToSessionType + + Aqua + + + diff --git a/pyproject.toml b/pyproject.toml index 8fae5c9..63039b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ markers = [ [tool.coverage.run] source = ["src"] +relative_files = true omit = [ "*/tests/*", "*/__pycache__/*", diff --git a/src/privaseeai_security/daemon.py b/src/privaseeai_security/daemon.py new file mode 100644 index 0000000..a240bdf --- /dev/null +++ b/src/privaseeai_security/daemon.py @@ -0,0 +1,90 @@ +"""Daemon entry point for PrivaseeAI Security orchestrator. + +This module provides a standalone entry point for running the orchestrator +as a background service via launchd or other init systems. + +Usage: + python -m privaseeai_security.daemon +""" + +import asyncio +import signal +import sys +from typing import Optional + +from .orchestrator import ThreatOrchestrator +from .logger import get_logger + + +logger = get_logger(__name__) + + +# Global orchestrator instance for signal handling +_orchestrator: Optional[ThreatOrchestrator] = None + + +async def run_daemon(): + """Run the threat orchestrator as a daemon service.""" + global _orchestrator + + # Create shutdown event inside async context + shutdown_event = asyncio.Event() + + def _signal_handler(signum, frame): + """Handle shutdown signals.""" + logger.info(f"Received signal {signum}, initiating shutdown...") + shutdown_event.set() + + # Setup signal handlers + signal.signal(signal.SIGTERM, _signal_handler) + signal.signal(signal.SIGINT, _signal_handler) + + logger.info("Starting PrivaseeAI Security daemon...") + + # Create orchestrator instance + _orchestrator = ThreatOrchestrator( + backup_path=None, # Auto-detect + telegram_enabled=True, + monitor_interval=30, + scan_backups_on_start=True + ) + + try: + # Start monitoring + await _orchestrator.start() + logger.info("✅ Orchestrator started successfully") + + # Wait for shutdown signal + await shutdown_event.wait() + + except Exception as e: + logger.error(f"Daemon error: {e}", exc_info=True) + raise + finally: + # Clean shutdown + if _orchestrator: + logger.info("Stopping orchestrator...") + await _orchestrator.stop() + logger.info("✅ Orchestrator stopped cleanly") + + +def main(): + """Main entry point for daemon.""" + # Log startup + logger.info("PrivaseeAI Security Daemon starting...") + + try: + # Run the async daemon + asyncio.run(run_daemon()) + except KeyboardInterrupt: + logger.info("Daemon interrupted by user") + except Exception as e: + logger.error(f"Fatal error: {e}", exc_info=True) + sys.exit(1) + + logger.info("Daemon shutdown complete") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/src/privaseeai_security/orchestrator.py b/src/privaseeai_security/orchestrator.py index 6a4f600..86d1e8c 100644 --- a/src/privaseeai_security/orchestrator.py +++ b/src/privaseeai_security/orchestrator.py @@ -372,3 +372,60 @@ async def scan_now(self) -> ThreatSummary: logger.info("Manual scan triggered") await self._scan_backups_once() return self.get_threat_summary() + + +# Daemon entry point when running as module +async def _run_daemon(): + """Run orchestrator as a daemon service.""" + import signal + + shutdown_event = asyncio.Event() + orchestrator = None + + def signal_handler(signum, frame): + """Handle shutdown signals.""" + logger.info(f"Received signal {signum}, shutting down...") + shutdown_event.set() + + # Setup signal handlers + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + try: + # Create and start orchestrator + orchestrator = ThreatOrchestrator( + backup_path=None, # Auto-detect + telegram_enabled=True, + monitor_interval=30, + scan_backups_on_start=True + ) + + await orchestrator.start() + logger.info("✅ Orchestrator daemon started") + + # Wait for shutdown signal + await shutdown_event.wait() + + except Exception as e: + logger.error(f"Orchestrator daemon error: {e}", exc_info=True) + raise + finally: + if orchestrator: + await orchestrator.stop() + logger.info("✅ Orchestrator daemon stopped") + + +# Entry point for python -m privaseeai_security.orchestrator +if __name__ == "__main__": + import sys + + logger.info("Starting PrivaseeAI Security Orchestrator daemon...") + + try: + asyncio.run(_run_daemon()) + except KeyboardInterrupt: + logger.info("Daemon interrupted") + except Exception as e: + logger.error(f"Fatal error: {e}", exc_info=True) + sys.exit(1) + diff --git a/tests/unit/test_launchd_service.py b/tests/unit/test_launchd_service.py new file mode 100644 index 0000000..adfb6ee --- /dev/null +++ b/tests/unit/test_launchd_service.py @@ -0,0 +1,136 @@ +"""Test launchd service and daemon functionality.""" + +import subprocess +import sys +from pathlib import Path +import pytest + + +# Get repository root +REPO_ROOT = Path(__file__).parent.parent.parent + + +def test_plist_file_exists(): + """Test that the plist file exists.""" + plist_path = REPO_ROOT / "com.privaseeai.security.plist" + assert plist_path.exists(), "com.privaseeai.security.plist not found" + + +def test_plist_is_valid_xml(): + """Test that the plist file is valid XML.""" + import xml.etree.ElementTree as ET + + plist_path = REPO_ROOT / "com.privaseeai.security.plist" + tree = ET.parse(plist_path) + root = tree.getroot() + + assert root.tag == "plist", "Root element should be plist" + assert root.attrib.get("version") == "1.0", "plist version should be 1.0" + + +def test_plist_has_required_keys(): + """Test that the plist has all required launchd keys.""" + import xml.etree.ElementTree as ET + + plist_path = REPO_ROOT / "com.privaseeai.security.plist" + tree = ET.parse(plist_path) + + # Check for required keys + required_keys = [ + "Label", + "ProgramArguments", + "RunAtLoad", + "KeepAlive", + "ThrottleInterval", + "StandardOutPath", + "StandardErrorPath", + "WorkingDirectory", + ] + + plist_text = ET.tostring(tree.getroot(), encoding='unicode') + + for key in required_keys: + assert f"{key}" in plist_text, f"Missing required key: {key}" + + +def test_plist_program_arguments(): + """Test that ProgramArguments is correctly configured.""" + import xml.etree.ElementTree as ET + + plist_path = REPO_ROOT / "com.privaseeai.security.plist" + tree = ET.parse(plist_path) + + plist_text = ET.tostring(tree.getroot(), encoding='unicode') + + # Should use python3 -m privaseeai_security.orchestrator + assert "python3" in plist_text, "Should use python3" + assert "privaseeai_security.orchestrator" in plist_text, "Should run orchestrator module" + + +def test_orchestrator_has_main_block(): + """Test that orchestrator.py has __main__ block for daemon mode.""" + from privaseeai_security import orchestrator + + # Check that the module has the main block + import inspect + source = inspect.getsource(orchestrator) + + assert 'if __name__ == "__main__"' in source, "orchestrator.py should have __main__ block" + assert '_run_daemon' in source, "orchestrator.py should have _run_daemon function" + + +def test_orchestrator_can_be_imported(): + """Test that the orchestrator module can be imported.""" + from privaseeai_security.orchestrator import ThreatOrchestrator + + assert ThreatOrchestrator is not None, "Should be able to import ThreatOrchestrator" + + +def test_daemon_module_exists(): + """Test that daemon.py module exists as alternative.""" + from privaseeai_security import daemon + + assert daemon is not None, "daemon module should exist" + assert hasattr(daemon, 'main'), "daemon should have main function" + + +def test_launchd_guide_exists(): + """Test that the LAUNCHD_SERVICE_GUIDE.md exists.""" + guide_path = REPO_ROOT / "LAUNCHD_SERVICE_GUIDE.md" + assert guide_path.exists(), "LAUNCHD_SERVICE_GUIDE.md not found" + + # Check it has key sections + content = guide_path.read_text() + assert "Installation Steps" in content + assert "Loading and Managing the Service" in content + assert "Testing the Service" in content + assert "Troubleshooting" in content + + +@pytest.mark.slow +def test_orchestrator_module_can_run(): + """Test that orchestrator can be run as a module (with quick timeout).""" + # This test tries to run the module but times out quickly + # Just verifying it starts without import errors + + proc = subprocess.Popen( + [sys.executable, "-m", "privaseeai_security.orchestrator"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + try: + # Let it run for 2 seconds + stdout, stderr = proc.communicate(timeout=2) + # If we get here, process exited early (might be an error) + pytest.fail(f"Process exited unexpectedly: {stderr}") + except subprocess.TimeoutExpired: + # This is expected - the daemon should keep running + proc.kill() + stdout, stderr = proc.communicate() + + # Check for import errors or other startup failures in output + combined = (stdout + stderr).lower() + assert "traceback" not in combined and "modulenotfounderror" not in combined, \ + f"Module had errors: {stdout}\n{stderr}"