Files
Anthropic-Cybersecurity-Skills/skills/testing-ransomware-recovery-procedures/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

317 lines
11 KiB
Python

#!/usr/bin/env python3
"""Agent for testing and validating ransomware recovery procedures.
Measures RTO/RPO against targets, validates backup restore integrity,
tracks recovery sequencing, and generates compliance reports.
"""
import argparse
import hashlib
import json
import os
import subprocess
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
class RecoveryTest:
"""Represents a single system recovery test with timing and validation."""
def __init__(self, system_name, tier, rto_target_seconds, rpo_target_seconds):
self.system_name = system_name
self.tier = tier
self.rto_target = rto_target_seconds
self.rpo_target = rpo_target_seconds
self.timestamps = {}
self.validations = {}
self.errors = []
def mark(self, phase):
"""Record a timestamp for a recovery phase."""
self.timestamps[phase] = time.time()
def validate(self, check_name, passed, detail=""):
"""Record a validation result."""
self.validations[check_name] = {"passed": passed, "detail": detail}
def actual_rto(self):
"""Calculate actual RTO from incident declaration to service restored."""
t0 = self.timestamps.get("incident_declared")
t4 = self.timestamps.get("service_restored")
if t0 and t4:
return t4 - t0
return None
def actual_rpo(self, backup_timestamp_epoch):
"""Calculate actual RPO from last backup to incident declaration."""
t0 = self.timestamps.get("incident_declared")
if t0 and backup_timestamp_epoch:
return t0 - backup_timestamp_epoch
return None
def to_dict(self, backup_timestamp_epoch=None):
rto = self.actual_rto()
rpo = self.actual_rpo(backup_timestamp_epoch)
return {
"system_name": self.system_name,
"tier": self.tier,
"rto_target_seconds": self.rto_target,
"rpo_target_seconds": self.rpo_target,
"actual_rto_seconds": round(rto, 2) if rto else None,
"actual_rpo_seconds": round(rpo, 2) if rpo else None,
"rto_met": rto <= self.rto_target if rto else None,
"rpo_met": rpo <= self.rpo_target if rpo else None,
"timestamps": {
k: datetime.fromtimestamp(v, tz=timezone.utc).isoformat()
for k, v in self.timestamps.items()
},
"validations": self.validations,
"errors": self.errors,
}
def compute_file_hashes(directory, algorithm="sha256"):
"""Compute hashes for all files in a directory for integrity verification."""
hashes = {}
dir_path = Path(directory)
if not dir_path.is_dir():
return {"error": f"Directory not found: {directory}"}
for fpath in sorted(dir_path.rglob("*")):
if fpath.is_file():
h = hashlib.new(algorithm)
try:
with open(fpath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
rel = str(fpath.relative_to(dir_path))
hashes[rel] = h.hexdigest()
except PermissionError:
hashes[str(fpath.relative_to(dir_path))] = "PERMISSION_DENIED"
return hashes
def compare_manifests(original_manifest, restored_manifest):
"""Compare two hash manifests to detect missing, added, or changed files."""
missing = []
modified = []
added = []
for fname, orig_hash in original_manifest.items():
if fname not in restored_manifest:
missing.append(fname)
elif restored_manifest[fname] != orig_hash:
modified.append(fname)
for fname in restored_manifest:
if fname not in original_manifest:
added.append(fname)
return {
"total_original": len(original_manifest),
"total_restored": len(restored_manifest),
"missing_files": missing,
"modified_files": modified,
"added_files": added,
"integrity_pass": len(missing) == 0 and len(modified) == 0,
}
def check_service_health(service_name):
"""Check if a service is running and responsive."""
if sys.platform == "win32":
try:
result = subprocess.run(
["sc", "query", service_name],
capture_output=True, text=True, timeout=10
)
running = "RUNNING" in result.stdout
return {"service": service_name, "running": running, "platform": "windows"}
except (subprocess.SubprocessError, FileNotFoundError):
return {"service": service_name, "running": False, "error": "check failed"}
else:
try:
result = subprocess.run(
["systemctl", "is-active", service_name],
capture_output=True, text=True, timeout=10
)
active = result.stdout.strip() == "active"
return {"service": service_name, "running": active, "platform": "linux"}
except (subprocess.SubprocessError, FileNotFoundError):
return {"service": service_name, "running": False, "error": "check failed"}
def check_database_connectivity(db_type, host="localhost", port=None):
"""Verify database is accessible after restore."""
ports = {"postgresql": 5432, "mysql": 3306, "mssql": 1433}
port = port or ports.get(db_type, 5432)
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
result = sock.connect_ex((host, port))
return {
"database": db_type,
"host": host,
"port": port,
"reachable": result == 0,
}
except socket.error as e:
return {"database": db_type, "host": host, "port": port, "reachable": False,
"error": str(e)}
finally:
sock.close()
def run_recovery_drill(config):
"""Execute a recovery drill based on a configuration dict."""
results = []
for system in config.get("systems", []):
test = RecoveryTest(
system_name=system["name"],
tier=system.get("tier", 3),
rto_target_seconds=system.get("rto_target_seconds", 14400),
rpo_target_seconds=system.get("rpo_target_seconds", 3600),
)
test.mark("incident_declared")
print(f"[*] Recovery drill started for: {system['name']}")
# Phase: Locate backup
test.mark("backup_identified")
backup_ts = system.get("backup_timestamp_epoch", time.time() - 3600)
# Phase: Validate restore directory if provided
restore_dir = system.get("restore_directory")
if restore_dir and os.path.isdir(restore_dir):
test.mark("restore_initiated")
hashes = compute_file_hashes(restore_dir)
file_count = len([v for v in hashes.values() if v != "PERMISSION_DENIED"])
test.validate("file_count", file_count > 0,
f"{file_count} files found in restored directory")
test.mark("restore_completed")
# Compare with manifest if provided
manifest_path = system.get("manifest_file")
if manifest_path and os.path.isfile(manifest_path):
with open(manifest_path, "r") as f:
original_manifest = json.load(f)
comparison = compare_manifests(original_manifest, hashes)
test.validate("integrity_check", comparison["integrity_pass"],
json.dumps(comparison, indent=2))
else:
test.validate("restore_directory", False,
f"Directory not found: {restore_dir}")
# Phase: Check services
for svc in system.get("services", []):
health = check_service_health(svc)
test.validate(f"service_{svc}", health.get("running", False),
json.dumps(health))
# Phase: Check database
db = system.get("database")
if db:
db_check = check_database_connectivity(
db.get("type", "postgresql"),
db.get("host", "localhost"),
db.get("port"),
)
test.validate("database_connectivity", db_check["reachable"],
json.dumps(db_check))
test.mark("service_restored")
results.append(test.to_dict(backup_ts))
print(f"[*] Recovery drill completed for: {system['name']}")
return results
def generate_report(results, output_path=None):
"""Generate a recovery test report."""
report = {
"report_date": datetime.now(timezone.utc).isoformat(),
"drill_type": "ransomware_recovery_validation",
"systems_tested": len(results),
"systems_meeting_rto": sum(1 for r in results if r.get("rto_met")),
"systems_meeting_rpo": sum(1 for r in results if r.get("rpo_met")),
"overall_pass": all(
r.get("rto_met") and r.get("rpo_met") for r in results
if r.get("rto_met") is not None
),
"results": results,
}
if output_path:
with open(output_path, "w") as f:
json.dump(report, f, indent=2)
print(f"[*] Report saved to {output_path}")
return report
def main():
parser = argparse.ArgumentParser(
description="Ransomware Recovery Procedure Testing Agent"
)
parser.add_argument("--config", help="JSON config file for recovery drill")
parser.add_argument("--hash-dir", help="Compute file hashes for a directory")
parser.add_argument("--compare", nargs=2, metavar=("ORIGINAL", "RESTORED"),
help="Compare two hash manifest JSON files")
parser.add_argument("--check-service", help="Check if a system service is running")
parser.add_argument("--check-db", help="Check database connectivity (type:host:port)")
parser.add_argument("--output", "-o", help="Output report file path")
args = parser.parse_args()
print("[*] Ransomware Recovery Procedure Testing Agent")
if args.hash_dir:
hashes = compute_file_hashes(args.hash_dir)
print(json.dumps(hashes, indent=2))
if args.output:
with open(args.output, "w") as f:
json.dump(hashes, f, indent=2)
print(f"[*] Hash manifest saved to {args.output}")
return
if args.compare:
with open(args.compare[0], "r") as f:
orig = json.load(f)
with open(args.compare[1], "r") as f:
restored = json.load(f)
result = compare_manifests(orig, restored)
print(json.dumps(result, indent=2))
return
if args.check_service:
result = check_service_health(args.check_service)
print(json.dumps(result, indent=2))
return
if args.check_db:
parts = args.check_db.split(":")
db_type = parts[0]
host = parts[1] if len(parts) > 1 else "localhost"
port = int(parts[2]) if len(parts) > 2 else None
result = check_database_connectivity(db_type, host, port)
print(json.dumps(result, indent=2))
return
if args.config:
with open(args.config, "r") as f:
config = json.load(f)
results = run_recovery_drill(config)
report = generate_report(results, args.output)
print(json.dumps(report, indent=2))
return
parser.print_help()
if __name__ == "__main__":
main()