#!/usr/bin/env python3 """Ransomware canary file deployment and monitoring agent. Deploys decoy files across critical directories and monitors them using watchdog for real-time filesystem event detection. Any interaction with canary files triggers alerts via email, Slack, and syslog. """ import os import sys import json import time import hashlib import logging import smtplib import argparse import platform from pathlib import Path from email.mime.text import MIMEText from datetime import datetime, timezone try: from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler HAS_WATCHDOG = True except ImportError: HAS_WATCHDOG = False try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False try: import psutil HAS_PSUTIL = True except ImportError: HAS_PSUTIL = False logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.StreamHandler(), logging.FileHandler("canary_monitor.log"), ], ) logger = logging.getLogger(__name__) CANARY_FILE_TEMPLATES = { "Passwords.xlsx": b"PK\x03\x04" + b"\x00" * 26 + b"[Content_Types].xml" + os.urandom(512), "Financial_Report_2026.docx": b"PK\x03\x04" + b"\x00" * 26 + b"word/document.xml" + os.urandom(512), "backup_credentials.csv": ( b"hostname,username,password,last_rotated\n" b"dc01.corp.local,svc_backup,R3st0re$ecur3!2026,2026-01-15\n" b"sql-prod-01,sa,Pr0d_DB#Access!,2026-02-01\n" b"vpn-gateway,admin,VPN@dm1n_2026!,2026-03-01\n" b"nas-backup,root,B4ckup_N4S!2026,2025-12-20\n" ), "Employee_SSN_List.xlsx": b"PK\x03\x04" + b"\x00" * 26 + b"xl/worksheets/sheet1.xml" + os.urandom(512), "tax_returns_2025.pdf": b"%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\n" + os.urandom(256), "bitcoin_wallet_seed.txt": ( b"BIP39 Mnemonic Seed Phrase (DO NOT SHARE)\n" b"abandon ability able about above absent absorb abstract absurd abuse\n" b"access accident account accuse achieve acid acoustic acquire across act\n" b"Wallet Address: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh\n" ), "database_export_prod.sql": ( b"-- MySQL dump 10.13 Distrib 8.0.36\n" b"-- Host: db-prod-01.internal Database: customer_data\n" b"CREATE TABLE customers (\n" b" id INT PRIMARY KEY AUTO_INCREMENT,\n" b" ssn VARCHAR(11) NOT NULL,\n" b" credit_card VARCHAR(19),\n" b" balance DECIMAL(10,2)\n" b");\n" ), "AWS_Access_Keys.csv": ( b"User Name,Access Key ID,Secret Access Key\n" b"svc-prod-deploy,AKIAIOSFODNN7EXAMPLE,wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n" b"svc-backup,AKIAI44QH8DHBEXAMPLE,je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY\n" ), } def compute_sha256(filepath): """Compute SHA-256 hash of a file.""" sha256 = hashlib.sha256() try: with open(filepath, "rb") as f: for chunk in iter(lambda: f.read(65536), b""): sha256.update(chunk) return sha256.hexdigest() except (FileNotFoundError, PermissionError): return "file_not_accessible" def compute_entropy(filepath): """Calculate Shannon entropy of file content to detect encryption.""" try: with open(filepath, "rb") as f: data = f.read() except (FileNotFoundError, PermissionError): return 0.0 if not data: return 0.0 from collections import Counter import math byte_counts = Counter(data) length = len(data) entropy = -sum( (count / length) * math.log2(count / length) for count in byte_counts.values() if count > 0 ) return round(entropy, 4) def get_process_info(): """Get information about processes that may have accessed canary files.""" if not HAS_PSUTIL: return {"error": "psutil not installed"} suspicious = [] for proc in psutil.process_iter(["pid", "name", "username", "cmdline", "create_time"]): try: info = proc.info if info["create_time"] and (time.time() - info["create_time"]) < 30: suspicious.append({ "pid": info["pid"], "name": info["name"], "username": info["username"], "cmdline": " ".join(info["cmdline"] or []), "age_seconds": round(time.time() - info["create_time"], 1), }) except (psutil.NoSuchProcess, psutil.AccessDenied): continue return suspicious[:10] def deploy_canary_files(target_dirs, custom_files=None): """Deploy canary files to specified directories.""" templates = dict(CANARY_FILE_TEMPLATES) if custom_files: templates.update(custom_files) deployed = [] for directory in target_dirs: dir_path = Path(directory) if not dir_path.exists(): logger.warning("Directory does not exist: %s", directory) continue if not os.access(directory, os.W_OK): logger.warning("No write access to: %s", directory) continue for filename, content in templates.items(): filepath = dir_path / filename if filepath.exists(): logger.info("Canary already exists: %s", filepath) deployed.append(str(filepath)) continue try: with open(filepath, "wb") as f: f.write(content) if platform.system() != "Windows": os.chmod(filepath, 0o644) sha256 = compute_sha256(str(filepath)) deployed.append(str(filepath)) logger.info("Deployed canary: %s (SHA-256: %s)", filepath, sha256[:16]) except (PermissionError, OSError) as e: logger.error("Failed to deploy %s: %s", filepath, e) manifest = { "deployed_at": datetime.now(timezone.utc).isoformat(), "canary_count": len(deployed), "directories": target_dirs, "files": deployed, "hashes": {f: compute_sha256(f) for f in deployed}, } manifest_path = Path("canary_manifest.json") with open(manifest_path, "w") as f: json.dump(manifest, f, indent=2) logger.info("Deployed %d canary files, manifest saved to %s", len(deployed), manifest_path) return manifest def send_email_alert(alert_data, smtp_host, smtp_port, sender, recipients, password=None): """Send alert email via SMTP.""" subject = f"RANSOMWARE CANARY ALERT: {alert_data['event_type']} on {alert_data['canary_file']}" body = json.dumps(alert_data, indent=2, default=str) msg = MIMEText(body) msg["Subject"] = subject msg["From"] = sender msg["To"] = ", ".join(recipients) try: if smtp_port == 465: server = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=10) else: server = smtplib.SMTP(smtp_host, smtp_port, timeout=10) server.ehlo() if smtp_port == 587: server.starttls() server.ehlo() if password: server.login(sender, password) server.sendmail(sender, recipients, msg.as_string()) server.quit() logger.info("Email alert sent to %s", recipients) return True except Exception as e: logger.error("Email alert failed: %s", e) return False def send_slack_alert(alert_data, webhook_url): """Send alert to Slack via incoming webhook.""" if not HAS_REQUESTS: logger.error("requests library not installed, cannot send Slack alert") return False payload = { "text": f":rotating_light: *RANSOMWARE CANARY ALERT*", "blocks": [ { "type": "header", "text": {"type": "plain_text", "text": "Ransomware Canary File Triggered"} }, { "type": "section", "fields": [ {"type": "mrkdwn", "text": f"*Event:*\n{alert_data['event_type']}"}, {"type": "mrkdwn", "text": f"*File:*\n`{alert_data['canary_file']}`"}, {"type": "mrkdwn", "text": f"*Time:*\n{alert_data['timestamp']}"}, {"type": "mrkdwn", "text": f"*Host:*\n{alert_data.get('hostname', 'unknown')}"}, ] }, { "type": "section", "text": { "type": "mrkdwn", "text": f"*Immediate Action Required:* Investigate potential ransomware activity on `{alert_data.get('hostname', 'unknown')}`" } } ] } try: resp = requests.post(webhook_url, json=payload, timeout=10) if resp.status_code == 200: logger.info("Slack alert sent successfully") return True logger.error("Slack alert failed: %d %s", resp.status_code, resp.text) return False except Exception as e: logger.error("Slack alert failed: %s", e) return False def send_syslog_alert(alert_data, syslog_server="127.0.0.1", syslog_port=514): """Send alert to syslog server via UDP.""" import socket priority = 8 * 4 + 1 # facility=security, severity=alert message = ( f"<{priority}>1 {alert_data['timestamp']} {alert_data.get('hostname', '-')} " f"canary-monitor - - - RANSOMWARE_CANARY event={alert_data['event_type']} " f"file={alert_data['canary_file']} " f"hash_before={alert_data.get('hash_before', 'N/A')} " f"hash_after={alert_data.get('hash_after', 'N/A')}" ) try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(10) sock.sendto(message.encode("utf-8"), (syslog_server, syslog_port)) sock.close() logger.info("Syslog alert sent to %s:%d", syslog_server, syslog_port) return True except Exception as e: logger.error("Syslog alert failed: %s", e) return False class CanaryFileHandler(FileSystemEventHandler): """Watchdog event handler for canary file monitoring.""" def __init__(self, canary_files, config): super().__init__() self.canary_files = {str(Path(f).resolve()): compute_sha256(f) for f in canary_files} self.config = config self.alert_count = 0 self.last_alert_time = {} def _is_canary(self, path): resolved = str(Path(path).resolve()) return resolved in self.canary_files def _rate_limit_check(self, path, cooldown=10): now = time.time() last = self.last_alert_time.get(path, 0) if now - last < cooldown: return False self.last_alert_time[path] = now return True def _build_alert(self, event_type, src_path, dest_path=None): alert = { "timestamp": datetime.now(timezone.utc).isoformat(), "event_type": event_type, "canary_file": src_path, "hostname": platform.node(), "platform": platform.system(), "hash_before": self.canary_files.get(str(Path(src_path).resolve()), "unknown"), "hash_after": compute_sha256(src_path) if os.path.exists(src_path) else "file_deleted", "entropy_after": compute_entropy(src_path) if os.path.exists(src_path) else 0.0, "alert_number": self.alert_count + 1, } if dest_path: alert["destination"] = dest_path if HAS_PSUTIL: alert["recent_processes"] = get_process_info() return alert def _dispatch_alert(self, alert): self.alert_count += 1 logger.critical( "CANARY TRIGGERED: %s on %s (alert #%d)", alert["event_type"], alert["canary_file"], self.alert_count ) with open("canary_alerts.jsonl", "a") as f: f.write(json.dumps(alert, default=str) + "\n") if self.config.get("slack_webhook"): send_slack_alert(alert, self.config["slack_webhook"]) if self.config.get("smtp_host"): send_email_alert( alert, self.config["smtp_host"], self.config.get("smtp_port", 587), self.config.get("smtp_sender", "canary@localhost"), self.config.get("smtp_recipients", []), self.config.get("smtp_password"), ) if self.config.get("syslog_server"): send_syslog_alert( alert, self.config["syslog_server"], self.config.get("syslog_port", 514), ) def on_modified(self, event): if event.is_directory: return if self._is_canary(event.src_path) and self._rate_limit_check(event.src_path): alert = self._build_alert("FILE_MODIFIED", event.src_path) high_entropy = alert.get("entropy_after", 0) > 7.5 if high_entropy: alert["encryption_suspected"] = True alert["severity"] = "critical" self._dispatch_alert(alert) def on_deleted(self, event): if event.is_directory: return if self._is_canary(event.src_path) and self._rate_limit_check(event.src_path): alert = self._build_alert("FILE_DELETED", event.src_path) alert["severity"] = "critical" self._dispatch_alert(alert) def on_moved(self, event): if event.is_directory: return if self._is_canary(event.src_path) and self._rate_limit_check(event.src_path): alert = self._build_alert("FILE_RENAMED", event.src_path, event.dest_path) extension = Path(event.dest_path).suffix.lower() ransomware_extensions = { ".encrypted", ".locked", ".lockbit", ".crypt", ".enc", ".ransom", ".pay", ".aes", ".rsa", ".cry", ".ryk", ".revil", ".conti", ".hive", ".black", ".basta", } if extension in ransomware_extensions: alert["ransomware_extension_detected"] = extension alert["severity"] = "critical" self._dispatch_alert(alert) def on_created(self, event): if event.is_directory: return parent = str(Path(event.src_path).parent) ransom_note_patterns = [ "readme", "decrypt", "restore", "recover", "how_to", "ransom", "locked", "unlock", "pay", "instruction", ] basename = Path(event.src_path).stem.lower() if any(pattern in basename for pattern in ransom_note_patterns): for canary_dir in set(str(Path(c).parent) for c in self.canary_files): if parent == canary_dir: alert = self._build_alert("RANSOM_NOTE_DETECTED", event.src_path) alert["severity"] = "critical" alert["indicator"] = "Ransom note dropped in monitored directory" self._dispatch_alert(alert) break def start_monitoring(manifest_path, config): """Start real-time canary file monitoring.""" if not HAS_WATCHDOG: logger.error("watchdog library required: pip install watchdog") sys.exit(1) with open(manifest_path) as f: manifest = json.load(f) canary_files = manifest["files"] if not canary_files: logger.error("No canary files found in manifest") sys.exit(1) watch_dirs = set() for canary in canary_files: parent = str(Path(canary).parent) if os.path.isdir(parent): watch_dirs.add(parent) handler = CanaryFileHandler(canary_files, config) observer = Observer() for directory in watch_dirs: observer.schedule(handler, directory, recursive=False) logger.info("Watching directory: %s", directory) logger.info("Monitoring %d canary files across %d directories", len(canary_files), len(watch_dirs)) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() logger.info("Monitoring stopped. Total alerts: %d", handler.alert_count) observer.join() def verify_canary_integrity(manifest_path): """Verify all canary files match their original hashes.""" with open(manifest_path) as f: manifest = json.load(f) results = {"checked": 0, "intact": 0, "modified": 0, "missing": 0, "details": []} for filepath, original_hash in manifest.get("hashes", {}).items(): results["checked"] += 1 if not os.path.exists(filepath): results["missing"] += 1 results["details"].append({"file": filepath, "status": "MISSING"}) else: current_hash = compute_sha256(filepath) if current_hash == original_hash: results["intact"] += 1 results["details"].append({"file": filepath, "status": "INTACT"}) else: results["modified"] += 1 results["details"].append({ "file": filepath, "status": "MODIFIED", "original_hash": original_hash, "current_hash": current_hash, "entropy": compute_entropy(filepath), }) return results def simulate_ransomware_test(manifest_path): """Simulate ransomware activity against canary files for testing.""" with open(manifest_path) as f: manifest = json.load(f) test_results = [] for filepath in manifest.get("files", [])[:2]: if not os.path.exists(filepath): continue test_file = filepath + ".test_canary" try: import shutil shutil.copy2(filepath, test_file) with open(test_file, "ab") as f: f.write(os.urandom(64)) test_results.append({ "file": filepath, "test_action": "modified_copy", "test_file": test_file, "status": "triggered", }) os.remove(test_file) except Exception as e: test_results.append({"file": filepath, "error": str(e)}) return test_results def main(): parser = argparse.ArgumentParser(description="Ransomware Canary File Deployment and Monitoring Agent") parser.add_argument("--action", choices=["deploy", "monitor", "verify", "test"], default="deploy", help="Action to perform") parser.add_argument("--dirs", nargs="+", help="Directories to deploy canary files") parser.add_argument("--manifest", default="canary_manifest.json", help="Canary manifest file path") parser.add_argument("--config", help="JSON config file for alert settings") parser.add_argument("--slack-webhook", help="Slack incoming webhook URL") parser.add_argument("--smtp-host", help="SMTP server hostname") parser.add_argument("--smtp-port", type=int, default=587) parser.add_argument("--smtp-sender", help="Alert sender email") parser.add_argument("--smtp-recipients", nargs="+", help="Alert recipient emails") parser.add_argument("--syslog-server", help="Syslog server address") args = parser.parse_args() config = {} if args.config and os.path.exists(args.config): with open(args.config) as f: config = json.load(f) if args.slack_webhook: config["slack_webhook"] = args.slack_webhook if args.smtp_host: config["smtp_host"] = args.smtp_host config["smtp_port"] = args.smtp_port config["smtp_sender"] = args.smtp_sender config["smtp_recipients"] = args.smtp_recipients or [] if args.syslog_server: config["syslog_server"] = args.syslog_server if args.action == "deploy": if not args.dirs: print("Usage: python agent.py --action deploy --dirs /path/to/dir1 /path/to/dir2") print("\nExample:") print(" python agent.py --action deploy --dirs /srv/shares/finance /home/admin/Documents") return manifest = deploy_canary_files(args.dirs) print(json.dumps(manifest, indent=2)) elif args.action == "monitor": if not os.path.exists(args.manifest): print(f"Manifest not found: {args.manifest}") print("Run --action deploy first to create canary files") return start_monitoring(args.manifest, config) elif args.action == "verify": if not os.path.exists(args.manifest): print(f"Manifest not found: {args.manifest}") return results = verify_canary_integrity(args.manifest) print(json.dumps(results, indent=2)) if results["modified"] > 0 or results["missing"] > 0: print(f"\n[ALERT] {results['modified']} modified, {results['missing']} missing canary files!") elif args.action == "test": if not os.path.exists(args.manifest): print(f"Manifest not found: {args.manifest}") return results = simulate_ransomware_test(args.manifest) print(json.dumps(results, indent=2)) if __name__ == "__main__": main()