mirror of
https://github.com/wahyd4/by-agent-for-agent.git
synced 2026-08-08 21:06:00 +10:00
345 lines
12 KiB
Python
345 lines
12 KiB
Python
#!/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()
|