#!/usr/bin/env python3 """ Security Audit Script for Clawdbot Checks system configuration for security issues. """ import os import sys import json import stat import argparse from pathlib import Path from typing import Dict, List, Tuple class SecurityAudit: def __init__(self): self.issues: List[Dict] = [] self.warnings: List[Dict] = [] self.passed: List[str] = [] def check_file_permissions(self, path: str, expected_mode: int, description: str) -> bool: """Check if file has correct permissions.""" p = Path(path).expanduser() if not p.exists(): self.warnings.append({ 'check': description, 'status': 'skip', 'message': f'{path} does not exist' }) return True current_mode = stat.S_IMODE(p.stat().st_mode) if current_mode != expected_mode: self.issues.append({ 'check': description, 'status': 'fail', 'message': f'{path} has mode {oct(current_mode)}, expected {oct(expected_mode)}', 'fix': f'chmod {oct(expected_mode)[2:]} {path}' }) return False self.passed.append(f'{description}: {path} = {oct(current_mode)}') return True def check_dir_permissions(self, path: str, expected_mode: int, description: str) -> bool: """Check if directory has correct permissions.""" return self.check_file_permissions(path, expected_mode, description) def check_config_not_in_sync(self) -> bool: """Check that config isn't in cloud sync folders.""" clawdbot_path = Path('~/.clawdbot').expanduser().resolve() sync_folders = [ Path('~/Library/Mobile Documents').expanduser(), # iCloud Path('~/Dropbox').expanduser(), Path('~/Google Drive').expanduser(), Path('~/OneDrive').expanduser(), ] for sync_folder in sync_folders: if sync_folder.exists(): try: if clawdbot_path.is_relative_to(sync_folder): self.issues.append({ 'check': 'Config not in cloud sync', 'status': 'fail', 'message': f'~/.clawdbot is inside {sync_folder}!', 'fix': 'Move ~/.clawdbot outside of cloud sync folders' }) return False except ValueError: pass # Not relative, which is good self.passed.append('Config not in cloud sync folders') return True def check_gateway_config(self) -> bool: """Check gateway security settings.""" config_path = Path('~/.clawdbot/clawdbot.json').expanduser() if not config_path.exists(): self.warnings.append({ 'check': 'Gateway config', 'status': 'skip', 'message': 'clawdbot.json not found' }) return True try: with open(config_path) as f: config = json.load(f) except Exception as e: self.issues.append({ 'check': 'Gateway config', 'status': 'fail', 'message': f'Cannot parse config: {e}' }) return False gateway = config.get('gateway', {}) all_good = True # Check bind bind = gateway.get('bind', 'loopback') if bind not in ['loopback', '127.0.0.1', 'localhost']: if bind in ['0.0.0.0', '::']: self.issues.append({ 'check': 'Gateway bind', 'status': 'fail', 'message': f'Gateway bound to {bind} (publicly accessible!)', 'fix': 'Set gateway.bind to "loopback" in config' }) all_good = False else: self.warnings.append({ 'check': 'Gateway bind', 'status': 'warn', 'message': f'Gateway bound to {bind}' }) else: self.passed.append(f'Gateway bind: {bind} (local only)') # Check auth auth = gateway.get('auth', {}) auth_mode = auth.get('mode', 'none') if auth_mode == 'none': self.issues.append({ 'check': 'Gateway auth', 'status': 'fail', 'message': 'Gateway has no authentication!', 'fix': 'Set gateway.auth.mode to "token" in config' }) all_good = False else: self.passed.append(f'Gateway auth: {auth_mode}') return all_good def check_telegram_policy(self) -> bool: """Check Telegram DM/group policies.""" config_path = Path('~/.clawdbot/clawdbot.json').expanduser() if not config_path.exists(): return True try: with open(config_path) as f: config = json.load(f) except: return True telegram = config.get('channels', {}).get('telegram', {}) if not telegram.get('enabled', False): return True all_good = True # Check DM policy dm_policy = telegram.get('dmPolicy', 'open') if dm_policy == 'open': self.warnings.append({ 'check': 'Telegram DM policy', 'status': 'warn', 'message': 'DM policy is "open" - anyone can DM the bot' }) else: self.passed.append(f'Telegram DM policy: {dm_policy}') # Check group policy group_policy = telegram.get('groupPolicy', 'closed') if group_policy == 'open': self.warnings.append({ 'check': 'Telegram group policy', 'status': 'warn', 'message': 'Group policy is "open" - anyone in groups can command the bot' }) else: self.passed.append(f'Telegram group policy: {group_policy}') return all_good def check_ssh_config(self) -> bool: """Check SSH hardening (if sshd_config exists).""" sshd_config = Path('/etc/ssh/sshd_config') if not sshd_config.exists(): return True # Not a server, skip try: content = sshd_config.read_text() except PermissionError: self.warnings.append({ 'check': 'SSH config', 'status': 'skip', 'message': 'Cannot read sshd_config (need sudo)' }) return True all_good = True # Check PasswordAuthentication if 'PasswordAuthentication yes' in content: self.issues.append({ 'check': 'SSH password auth', 'status': 'fail', 'message': 'SSH password authentication is enabled!', 'fix': 'Set PasswordAuthentication no in /etc/ssh/sshd_config' }) all_good = False elif 'PasswordAuthentication no' in content: self.passed.append('SSH password auth: disabled') # Check PermitRootLogin if 'PermitRootLogin yes' in content: self.issues.append({ 'check': 'SSH root login', 'status': 'fail', 'message': 'SSH root login is enabled!', 'fix': 'Set PermitRootLogin no in /etc/ssh/sshd_config' }) all_good = False elif 'PermitRootLogin no' in content: self.passed.append('SSH root login: disabled') return all_good def run_audit(self, quick: bool = False) -> Tuple[int, int, int]: """Run all security checks.""" # File permissions self.check_dir_permissions('~/.clawdbot', 0o700, 'Clawdbot directory permissions') self.check_file_permissions('~/.clawdbot/clawdbot.json', 0o600, 'Config file permissions') # Config location if not quick: self.check_config_not_in_sync() # Gateway config self.check_gateway_config() # Telegram policies self.check_telegram_policy() # SSH (if server) if not quick: self.check_ssh_config() return len(self.issues), len(self.warnings), len(self.passed) def fix_issues(self) -> int: """Attempt to auto-fix issues.""" fixed = 0 for issue in self.issues: fix = issue.get('fix', '') if fix.startswith('chmod'): try: parts = fix.split() mode = int(parts[1], 8) path = Path(parts[2]).expanduser() os.chmod(path, mode) print(f"✅ Fixed: {fix}") fixed += 1 except Exception as e: print(f"❌ Cannot fix: {fix} ({e})") else: print(f"⚠️ Manual fix needed: {fix}") return fixed def print_report(self, verbose: bool = False): """Print audit report.""" print("=" * 60) print("🛡️ CLAWDBOT SECURITY AUDIT") print("=" * 60) if self.issues: print(f"\n🚨 ISSUES ({len(self.issues)})") for issue in self.issues: print(f" ❌ {issue['check']}") print(f" {issue['message']}") if issue.get('fix'): print(f" Fix: {issue['fix']}") if self.warnings: print(f"\n⚠️ WARNINGS ({len(self.warnings)})") for warn in self.warnings: print(f" ⚠️ {warn['check']}") print(f" {warn['message']}") if verbose and self.passed: print(f"\n✅ PASSED ({len(self.passed)})") for p in self.passed: print(f" ✅ {p}") print("\n" + "=" * 60) total = len(self.issues) + len(self.warnings) + len(self.passed) if self.issues: print(f"❌ {len(self.issues)} issues need attention") elif self.warnings: print(f"⚠️ {len(self.warnings)} warnings, but no critical issues") else: print(f"✅ All {total} checks passed!") print("=" * 60) def main(): parser = argparse.ArgumentParser(description='Clawdbot Security Audit') parser.add_argument('--quick', action='store_true', help='Quick audit (skip slow checks)') parser.add_argument('--fix', action='store_true', help='Attempt to auto-fix issues') parser.add_argument('--json', action='store_true', help='Output as JSON') parser.add_argument('--verbose', '-v', action='store_true', help='Show passed checks') args = parser.parse_args() audit = SecurityAudit() issues, warnings, passed = audit.run_audit(quick=args.quick) if args.fix: print("\n🔧 Attempting to fix issues...\n") fixed = audit.fix_issues() print(f"\nFixed {fixed} issues.") # Re-run audit audit = SecurityAudit() audit.run_audit(quick=args.quick) if args.json: result = { 'issues': audit.issues, 'warnings': audit.warnings, 'passed': audit.passed, 'summary': { 'issues': len(audit.issues), 'warnings': len(audit.warnings), 'passed': len(audit.passed), } } print(json.dumps(result, indent=2, ensure_ascii=False)) else: audit.print_report(verbose=args.verbose) sys.exit(1 if audit.issues else 0) if __name__ == '__main__': main()