mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 22:24:56 +03:00
c47eed6a64
- 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
317 lines
11 KiB
Python
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()
|