Files
Anthropic-Cybersecurity-Skills/skills/deploying-ransomware-canary-files/scripts/agent.py
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

556 lines
21 KiB
Python

#!/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()