mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-15 15:34:56 +03:00
424 lines
16 KiB
Python
424 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Ransomware Incident Response Automation Script
|
|
|
|
Automates key ransomware IR tasks:
|
|
- Identifies ransomware variant from file extensions and ransom notes
|
|
- Scans for encryption indicators across file systems
|
|
- Checks for Volume Shadow Copy deletion
|
|
- Queries backup integrity
|
|
- Generates scope assessment report
|
|
|
|
Requirements:
|
|
pip install requests yara-python watchdog
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from collections import Counter, defaultdict
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
print("Install requests: pip install requests")
|
|
sys.exit(1)
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
handlers=[
|
|
logging.StreamHandler(),
|
|
logging.FileHandler(f"ransomware_ir_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"),
|
|
],
|
|
)
|
|
logger = logging.getLogger("ransomware_ir")
|
|
|
|
# Known ransomware file extensions mapped to families
|
|
RANSOMWARE_EXTENSIONS = {
|
|
".lockbit": "LockBit",
|
|
".lockbit3": "LockBit 3.0",
|
|
".BlackCat": "BlackCat/ALPHV",
|
|
".cl0p": "Cl0p",
|
|
".royal": "Royal",
|
|
".play": "Play",
|
|
".akira": "Akira",
|
|
".rhysida": "Rhysida",
|
|
".blacksuit": "BlackSuit",
|
|
".medusa": "Medusa",
|
|
".8base": "8Base",
|
|
".bianlian": "BianLian",
|
|
".encrypted": "Generic/Multiple",
|
|
".locked": "Generic/Multiple",
|
|
".crypt": "Generic/Multiple",
|
|
".enc": "Generic/Multiple",
|
|
".ryk": "Ryuk",
|
|
".conti": "Conti",
|
|
".hive": "Hive",
|
|
".maze": "Maze",
|
|
".revil": "REvil/Sodinokibi",
|
|
".darkside": "DarkSide",
|
|
".babuk": "Babuk",
|
|
".phobos": "Phobos",
|
|
".dharma": "Dharma",
|
|
".stop": "STOP/Djvu",
|
|
".djvu": "STOP/Djvu",
|
|
}
|
|
|
|
RANSOM_NOTE_PATTERNS = [
|
|
"README*.txt", "README*.html", "DECRYPT*.txt", "DECRYPT*.html",
|
|
"HOW_TO_RECOVER*", "RESTORE_FILES*", "RECOVER_YOUR_DATA*",
|
|
"!README!*", "_readme.txt", "info.txt", "info.hta",
|
|
"HELP_RECOVER*", "YOUR_FILES*", "#DECRYPT#*", "RANSOM_NOTE*",
|
|
]
|
|
|
|
DECRYPTOR_SOURCES = {
|
|
"No More Ransom": "https://www.nomoreransom.org/en/decryption-tools.html",
|
|
"Emsisoft": "https://www.emsisoft.com/en/ransomware-decryption/",
|
|
"Kaspersky": "https://noransom.kaspersky.com/",
|
|
"Avast": "https://www.avast.com/ransomware-decryption-tools",
|
|
"Bitdefender": "https://www.bitdefender.com/blog/labs/bitdefender-offers-free-universal-decryptor-for-revil-sodinokibi-ransomware/",
|
|
}
|
|
|
|
|
|
class RansomwareScanner:
|
|
"""Scan file systems for ransomware indicators."""
|
|
|
|
def __init__(self, scan_paths: list):
|
|
self.scan_paths = scan_paths
|
|
self.encrypted_files = []
|
|
self.ransom_notes = []
|
|
self.extension_counts = Counter()
|
|
self.affected_directories = set()
|
|
|
|
def scan_for_encrypted_files(self, max_files: int = 10000) -> dict:
|
|
"""Scan for files with known ransomware extensions."""
|
|
logger.info(f"Scanning {len(self.scan_paths)} paths for encrypted files...")
|
|
count = 0
|
|
for scan_path in self.scan_paths:
|
|
scan_path = Path(scan_path)
|
|
if not scan_path.exists():
|
|
logger.warning(f"Path does not exist: {scan_path}")
|
|
continue
|
|
try:
|
|
for item in scan_path.rglob("*"):
|
|
if count >= max_files:
|
|
logger.warning(f"Reached max file scan limit ({max_files})")
|
|
break
|
|
if item.is_file():
|
|
ext = item.suffix.lower()
|
|
if ext in RANSOMWARE_EXTENSIONS:
|
|
self.encrypted_files.append({
|
|
"path": str(item),
|
|
"extension": ext,
|
|
"family": RANSOMWARE_EXTENSIONS[ext],
|
|
"size": item.stat().st_size,
|
|
"modified": datetime.fromtimestamp(item.stat().st_mtime).isoformat(),
|
|
})
|
|
self.extension_counts[ext] += 1
|
|
self.affected_directories.add(str(item.parent))
|
|
count += 1
|
|
except PermissionError as e:
|
|
logger.warning(f"Permission denied: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Error scanning {scan_path}: {e}")
|
|
|
|
logger.info(f"Found {len(self.encrypted_files)} encrypted files")
|
|
return {
|
|
"total_encrypted": len(self.encrypted_files),
|
|
"extension_breakdown": dict(self.extension_counts),
|
|
"affected_directories": len(self.affected_directories),
|
|
"likely_family": self._identify_family(),
|
|
}
|
|
|
|
def scan_for_ransom_notes(self) -> list:
|
|
"""Scan for ransom note files."""
|
|
logger.info("Scanning for ransom notes...")
|
|
for scan_path in self.scan_paths:
|
|
scan_path = Path(scan_path)
|
|
if not scan_path.exists():
|
|
continue
|
|
for pattern in RANSOM_NOTE_PATTERNS:
|
|
try:
|
|
for note in scan_path.rglob(pattern):
|
|
if note.is_file():
|
|
content = ""
|
|
try:
|
|
content = note.read_text(errors="ignore")[:2000]
|
|
except Exception:
|
|
pass
|
|
self.ransom_notes.append({
|
|
"path": str(note),
|
|
"size": note.stat().st_size,
|
|
"modified": datetime.fromtimestamp(note.stat().st_mtime).isoformat(),
|
|
"content_preview": content[:500],
|
|
"bitcoin_addresses": self._extract_bitcoin_addresses(content),
|
|
"onion_urls": self._extract_onion_urls(content),
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error scanning for notes with pattern {pattern}: {e}")
|
|
|
|
logger.info(f"Found {len(self.ransom_notes)} ransom notes")
|
|
return self.ransom_notes
|
|
|
|
def _identify_family(self) -> str:
|
|
if not self.extension_counts:
|
|
return "Unknown"
|
|
most_common_ext = self.extension_counts.most_common(1)[0][0]
|
|
return RANSOMWARE_EXTENSIONS.get(most_common_ext, "Unknown")
|
|
|
|
@staticmethod
|
|
def _extract_bitcoin_addresses(text: str) -> list:
|
|
btc_pattern = r'\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b|bc1[a-zA-HJ-NP-Z0-9]{25,89}\b'
|
|
return re.findall(btc_pattern, text)
|
|
|
|
@staticmethod
|
|
def _extract_onion_urls(text: str) -> list:
|
|
onion_pattern = r'[a-z2-7]{16,56}\.onion'
|
|
return re.findall(onion_pattern, text)
|
|
|
|
|
|
class BackupAssessor:
|
|
"""Assess backup availability and integrity for recovery planning."""
|
|
|
|
def __init__(self):
|
|
self.backup_status = []
|
|
|
|
def check_vss_status(self) -> dict:
|
|
"""Check Volume Shadow Copy status on Windows."""
|
|
if os.name != "nt":
|
|
return {"status": "not_applicable", "platform": "linux"}
|
|
try:
|
|
result = subprocess.run(
|
|
["vssadmin", "list", "shadows"],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
shadows = re.findall(r"Shadow Copy Volume: (.+)", result.stdout)
|
|
deleted = "No items found" in result.stdout or "no shadow copies" in result.stdout.lower()
|
|
return {
|
|
"status": "deleted" if deleted else "available",
|
|
"shadow_count": len(shadows),
|
|
"output": result.stdout[:2000],
|
|
}
|
|
except Exception as e:
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
def check_windows_backup(self) -> dict:
|
|
"""Check Windows Server Backup status."""
|
|
if os.name != "nt":
|
|
return {"status": "not_applicable"}
|
|
try:
|
|
result = subprocess.run(
|
|
["wbadmin", "get", "versions"],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
versions = re.findall(r"Version identifier: (.+)", result.stdout)
|
|
return {
|
|
"status": "available" if versions else "no_backups",
|
|
"versions": versions[:10],
|
|
}
|
|
except Exception as e:
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
def check_backup_directory(self, backup_path: str) -> dict:
|
|
"""Check if a backup directory exists and has recent files."""
|
|
bp = Path(backup_path)
|
|
if not bp.exists():
|
|
return {"path": backup_path, "status": "not_found"}
|
|
try:
|
|
files = list(bp.rglob("*"))
|
|
file_count = len([f for f in files if f.is_file()])
|
|
if file_count == 0:
|
|
return {"path": backup_path, "status": "empty"}
|
|
newest = max(f.stat().st_mtime for f in files if f.is_file())
|
|
return {
|
|
"path": backup_path,
|
|
"status": "available",
|
|
"file_count": file_count,
|
|
"newest_file": datetime.fromtimestamp(newest).isoformat(),
|
|
"total_size_gb": round(sum(f.stat().st_size for f in files if f.is_file()) / (1024**3), 2),
|
|
}
|
|
except Exception as e:
|
|
return {"path": backup_path, "status": "error", "error": str(e)}
|
|
|
|
|
|
class EncryptionScopeAssessor:
|
|
"""Assess the scope of ransomware encryption across the environment."""
|
|
|
|
def __init__(self):
|
|
self.scope_data = defaultdict(list)
|
|
|
|
def assess_windows_event_logs(self) -> dict:
|
|
"""Check Windows event logs for ransomware indicators."""
|
|
if os.name != "nt":
|
|
return {"status": "not_applicable"}
|
|
indicators = {}
|
|
# Check for VSS deletion events
|
|
try:
|
|
result = subprocess.run(
|
|
["wevtutil", "qe", "Application",
|
|
"/q:*[System[Provider[@Name='VSS'] and (EventID=8193 or EventID=8194)]]",
|
|
"/f:text", "/c:20"],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
indicators["vss_events"] = result.stdout[:2000] if result.stdout else "No VSS events found"
|
|
except Exception as e:
|
|
indicators["vss_events_error"] = str(e)
|
|
|
|
# Check for service stop events (ransomware often stops services)
|
|
try:
|
|
result = subprocess.run(
|
|
["wevtutil", "qe", "System",
|
|
"/q:*[System[EventID=7036]]",
|
|
"/f:text", "/c:50"],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
indicators["service_stops"] = result.stdout[:2000] if result.stdout else "No service stop events"
|
|
except Exception as e:
|
|
indicators["service_stops_error"] = str(e)
|
|
|
|
return indicators
|
|
|
|
def check_running_encryption(self) -> dict:
|
|
"""Check if encryption is still actively running."""
|
|
try:
|
|
if os.name == "nt":
|
|
result = subprocess.run(["tasklist", "/FO", "CSV"], capture_output=True, text=True)
|
|
else:
|
|
result = subprocess.run(["ps", "aux"], capture_output=True, text=True)
|
|
suspicious = []
|
|
suspicious_names = [
|
|
"encrypt", "ransom", "lock", "crypt", "vssadmin", "wbadmin",
|
|
"bcdedit", "wmic shadowcopy", "cipher",
|
|
]
|
|
for line in result.stdout.lower().split("\n"):
|
|
for name in suspicious_names:
|
|
if name in line:
|
|
suspicious.append(line.strip())
|
|
return {
|
|
"active_encryption": len(suspicious) > 0,
|
|
"suspicious_processes": suspicious[:20],
|
|
}
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
def generate_scope_report(incident_id: str, scanner: RansomwareScanner,
|
|
backup: BackupAssessor, output_dir: str):
|
|
"""Generate a comprehensive ransomware scope assessment report."""
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
report = {
|
|
"incident_id": incident_id,
|
|
"assessment_time": datetime.now(timezone.utc).isoformat(),
|
|
"ransomware_family": scanner._identify_family(),
|
|
"encryption_scope": {
|
|
"total_encrypted_files": len(scanner.encrypted_files),
|
|
"extension_breakdown": dict(scanner.extension_counts),
|
|
"affected_directories": len(scanner.affected_directories),
|
|
},
|
|
"ransom_notes": {
|
|
"total_found": len(scanner.ransom_notes),
|
|
"bitcoin_addresses": list(set(
|
|
addr for note in scanner.ransom_notes for addr in note.get("bitcoin_addresses", [])
|
|
)),
|
|
"onion_urls": list(set(
|
|
url for note in scanner.ransom_notes for url in note.get("onion_urls", [])
|
|
)),
|
|
},
|
|
"backup_status": {
|
|
"vss": backup.check_vss_status(),
|
|
},
|
|
"decryptor_check": {
|
|
"family": scanner._identify_family(),
|
|
"check_sources": DECRYPTOR_SOURCES,
|
|
"recommendation": "Check listed sources for available free decryptors",
|
|
},
|
|
}
|
|
|
|
report_path = os.path.join(output_dir, f"ransomware_scope_{incident_id}.json")
|
|
with open(report_path, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
logger.info(f"Scope report saved to: {report_path}")
|
|
|
|
# Export encrypted files list
|
|
if scanner.encrypted_files:
|
|
csv_path = os.path.join(output_dir, f"encrypted_files_{incident_id}.csv")
|
|
with open(csv_path, "w", newline="") as f:
|
|
writer = csv.DictWriter(f, fieldnames=scanner.encrypted_files[0].keys())
|
|
writer.writeheader()
|
|
writer.writerows(scanner.encrypted_files)
|
|
logger.info(f"Encrypted files list saved to: {csv_path}")
|
|
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Ransomware Incident Response Automation")
|
|
parser.add_argument("--incident-id", required=True, help="Incident tracking ID")
|
|
parser.add_argument("--scan-paths", nargs="+", required=True, help="Paths to scan for encrypted files")
|
|
parser.add_argument("--backup-paths", nargs="*", default=[], help="Backup paths to check integrity")
|
|
parser.add_argument("--output-dir", default="./ransomware_ir_output", help="Output directory")
|
|
parser.add_argument("--max-files", type=int, default=10000, help="Maximum files to scan")
|
|
parser.add_argument("--check-processes", action="store_true", help="Check for active encryption processes")
|
|
|
|
args = parser.parse_args()
|
|
|
|
logger.info(f"Starting ransomware IR assessment for {args.incident_id}")
|
|
|
|
# Scan for encrypted files
|
|
scanner = RansomwareScanner(args.scan_paths)
|
|
enc_results = scanner.scan_for_encrypted_files(max_files=args.max_files)
|
|
logger.info(f"Encryption scan: {enc_results}")
|
|
|
|
# Scan for ransom notes
|
|
notes = scanner.scan_for_ransom_notes()
|
|
if notes:
|
|
logger.info(f"Found {len(notes)} ransom notes")
|
|
for note in notes[:5]:
|
|
logger.info(f" Note: {note['path']}")
|
|
if note.get("bitcoin_addresses"):
|
|
logger.info(f" Bitcoin addresses: {note['bitcoin_addresses']}")
|
|
|
|
# Check backup status
|
|
backup = BackupAssessor()
|
|
vss_status = backup.check_vss_status()
|
|
logger.info(f"VSS status: {vss_status['status']}")
|
|
|
|
for bp in args.backup_paths:
|
|
bp_status = backup.check_backup_directory(bp)
|
|
logger.info(f"Backup path {bp}: {bp_status['status']}")
|
|
|
|
# Check for active encryption
|
|
if args.check_processes:
|
|
scope_assessor = EncryptionScopeAssessor()
|
|
active = scope_assessor.check_running_encryption()
|
|
if active.get("active_encryption"):
|
|
logger.critical("ACTIVE ENCRYPTION DETECTED - IMMEDIATE ISOLATION REQUIRED")
|
|
for proc in active.get("suspicious_processes", []):
|
|
logger.critical(f" Suspicious process: {proc}")
|
|
|
|
# Generate report
|
|
report = generate_scope_report(args.incident_id, scanner, backup, args.output_dir)
|
|
logger.info(f"Assessment complete. Family: {report['ransomware_family']}")
|
|
logger.info(f"Total encrypted files: {report['encryption_scope']['total_encrypted_files']}")
|
|
|
|
print(f"\nRansomware IR Assessment Complete")
|
|
print(f"Incident ID: {args.incident_id}")
|
|
print(f"Likely Family: {report['ransomware_family']}")
|
|
print(f"Encrypted Files: {report['encryption_scope']['total_encrypted_files']}")
|
|
print(f"Ransom Notes: {report['ransom_notes']['total_found']}")
|
|
print(f"Report: {args.output_dir}/ransomware_scope_{args.incident_id}.json")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|