mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 21:54:56 +03:00
518 lines
21 KiB
Python
518 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Active Security Breach Containment Automation Script
|
|
|
|
Automates containment actions during an active security breach:
|
|
- Queries SIEM for scope assessment
|
|
- Isolates endpoints via EDR API
|
|
- Blocks IPs/domains at firewall
|
|
- Disables compromised AD accounts
|
|
- Generates containment action log
|
|
|
|
Requirements:
|
|
pip install requests ldap3 python-dateutil pyyaml
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
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)
|
|
|
|
try:
|
|
from ldap3 import Server, Connection, MODIFY_REPLACE, ALL
|
|
except ImportError:
|
|
ldap3_available = False
|
|
else:
|
|
ldap3_available = True
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
handlers=[
|
|
logging.StreamHandler(),
|
|
logging.FileHandler(f"containment_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"),
|
|
],
|
|
)
|
|
logger = logging.getLogger("breach_containment")
|
|
|
|
|
|
class ContainmentActionLog:
|
|
"""Tracks all containment actions with timestamps for audit trail."""
|
|
|
|
def __init__(self, incident_id: str):
|
|
self.incident_id = incident_id
|
|
self.actions = []
|
|
self.start_time = datetime.now(timezone.utc)
|
|
|
|
def log_action(self, action_type: str, target: str, result: str, details: str = ""):
|
|
entry = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"incident_id": self.incident_id,
|
|
"action_type": action_type,
|
|
"target": target,
|
|
"result": result,
|
|
"details": details,
|
|
"operator": os.getenv("USERNAME", os.getenv("USER", "unknown")),
|
|
}
|
|
self.actions.append(entry)
|
|
logger.info(f"[{action_type}] {target}: {result} - {details}")
|
|
|
|
def export_csv(self, filepath: str):
|
|
if not self.actions:
|
|
logger.warning("No actions to export")
|
|
return
|
|
with open(filepath, "w", newline="") as f:
|
|
writer = csv.DictWriter(f, fieldnames=self.actions[0].keys())
|
|
writer.writeheader()
|
|
writer.writerows(self.actions)
|
|
logger.info(f"Containment log exported to {filepath}")
|
|
|
|
def export_json(self, filepath: str):
|
|
report = {
|
|
"incident_id": self.incident_id,
|
|
"containment_start": self.start_time.isoformat(),
|
|
"containment_end": datetime.now(timezone.utc).isoformat(),
|
|
"total_actions": len(self.actions),
|
|
"actions": self.actions,
|
|
}
|
|
with open(filepath, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
logger.info(f"Containment report exported to {filepath}")
|
|
|
|
|
|
class CrowdStrikeContainment:
|
|
"""CrowdStrike Falcon endpoint containment via API."""
|
|
|
|
def __init__(self, client_id: str, client_secret: str, base_url: str = "https://api.crowdstrike.com"):
|
|
self.base_url = base_url
|
|
self.client_id = client_id
|
|
self.client_secret = client_secret
|
|
self.token = None
|
|
|
|
def authenticate(self):
|
|
resp = requests.post(
|
|
f"{self.base_url}/oauth2/token",
|
|
data={"client_id": self.client_id, "client_secret": self.client_secret},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
resp.raise_for_status()
|
|
self.token = resp.json()["access_token"]
|
|
logger.info("Authenticated to CrowdStrike Falcon API")
|
|
|
|
def _headers(self):
|
|
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
|
|
|
|
def get_device_id_by_hostname(self, hostname: str) -> Optional[str]:
|
|
resp = requests.get(
|
|
f"{self.base_url}/devices/queries/devices/v1",
|
|
headers=self._headers(),
|
|
params={"filter": f"hostname:'{hostname}'"},
|
|
)
|
|
resp.raise_for_status()
|
|
resources = resp.json().get("resources", [])
|
|
return resources[0] if resources else None
|
|
|
|
def contain_host(self, device_id: str) -> dict:
|
|
resp = requests.post(
|
|
f"{self.base_url}/devices/entities/devices-actions/v2",
|
|
headers=self._headers(),
|
|
params={"action_name": "contain"},
|
|
json={"ids": [device_id]},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
def lift_containment(self, device_id: str) -> dict:
|
|
resp = requests.post(
|
|
f"{self.base_url}/devices/entities/devices-actions/v2",
|
|
headers=self._headers(),
|
|
params={"action_name": "lift_containment"},
|
|
json={"ids": [device_id]},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
def get_detections(self, severity: str = "Critical") -> list:
|
|
resp = requests.get(
|
|
f"{self.base_url}/detects/queries/detects/v1",
|
|
headers=self._headers(),
|
|
params={"filter": f"max_severity_displayname:'{severity}'+status:'new'", "limit": 100},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json().get("resources", [])
|
|
|
|
|
|
class SentinelOneContainment:
|
|
"""SentinelOne endpoint containment via API."""
|
|
|
|
def __init__(self, api_token: str, base_url: str):
|
|
self.base_url = base_url
|
|
self.api_token = api_token
|
|
|
|
def _headers(self):
|
|
return {"Authorization": f"ApiToken {self.api_token}", "Content-Type": "application/json"}
|
|
|
|
def disconnect_agent(self, agent_id: str) -> dict:
|
|
resp = requests.post(
|
|
f"{self.base_url}/web/api/v2.1/agents/actions/disconnect",
|
|
headers=self._headers(),
|
|
json={"filter": {"ids": [agent_id]}, "data": {}},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
def reconnect_agent(self, agent_id: str) -> dict:
|
|
resp = requests.post(
|
|
f"{self.base_url}/web/api/v2.1/agents/actions/connect",
|
|
headers=self._headers(),
|
|
json={"filter": {"ids": [agent_id]}, "data": {}},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
class ActiveDirectoryContainment:
|
|
"""Active Directory account containment via LDAP."""
|
|
|
|
def __init__(self, server_addr: str, domain: str, username: str, password: str):
|
|
if not ldap3_available:
|
|
raise ImportError("ldap3 package required: pip install ldap3")
|
|
self.server = Server(server_addr, get_info=ALL)
|
|
self.domain = domain
|
|
self.conn = Connection(self.server, user=f"{domain}\\{username}", password=password, auto_bind=True)
|
|
|
|
def disable_account(self, sam_account_name: str) -> bool:
|
|
search_base = ",".join([f"DC={part}" for part in self.domain.split(".")])
|
|
self.conn.search(
|
|
search_base,
|
|
f"(sAMAccountName={sam_account_name})",
|
|
attributes=["userAccountControl", "distinguishedName"],
|
|
)
|
|
if not self.conn.entries:
|
|
logger.warning(f"Account {sam_account_name} not found in AD")
|
|
return False
|
|
|
|
dn = self.conn.entries[0].distinguishedName.value
|
|
current_uac = int(self.conn.entries[0].userAccountControl.value)
|
|
# Set ACCOUNTDISABLE flag (bit 1, value 2)
|
|
new_uac = current_uac | 0x0002
|
|
self.conn.modify(dn, {"userAccountControl": [(MODIFY_REPLACE, [str(new_uac)])]})
|
|
logger.info(f"Disabled AD account: {sam_account_name}")
|
|
return True
|
|
|
|
def reset_password(self, sam_account_name: str, new_password: str) -> bool:
|
|
search_base = ",".join([f"DC={part}" for part in self.domain.split(".")])
|
|
self.conn.search(search_base, f"(sAMAccountName={sam_account_name})", attributes=["distinguishedName"])
|
|
if not self.conn.entries:
|
|
return False
|
|
dn = self.conn.entries[0].distinguishedName.value
|
|
encoded_pw = f'"{new_password}"'.encode("utf-16-le")
|
|
self.conn.modify(dn, {"unicodePwd": [(MODIFY_REPLACE, [encoded_pw])]})
|
|
logger.info(f"Reset password for AD account: {sam_account_name}")
|
|
return True
|
|
|
|
|
|
class FirewallContainment:
|
|
"""Block IPs and domains at network perimeter."""
|
|
|
|
@staticmethod
|
|
def block_ips_iptables(ip_list: list, chain: str = "INPUT") -> list:
|
|
results = []
|
|
for ip in ip_list:
|
|
try:
|
|
cmd = ["iptables", "-A", chain, "-s", ip, "-j", "DROP"]
|
|
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
cmd_out = ["iptables", "-A", "OUTPUT", "-d", ip, "-j", "DROP"]
|
|
subprocess.run(cmd_out, capture_output=True, text=True, check=True)
|
|
results.append({"ip": ip, "status": "blocked", "method": "iptables"})
|
|
logger.info(f"Blocked IP via iptables: {ip}")
|
|
except subprocess.CalledProcessError as e:
|
|
results.append({"ip": ip, "status": "failed", "error": str(e)})
|
|
logger.error(f"Failed to block IP {ip}: {e}")
|
|
return results
|
|
|
|
@staticmethod
|
|
def block_ips_windows_firewall(ip_list: list) -> list:
|
|
results = []
|
|
for ip in ip_list:
|
|
try:
|
|
rule_name = f"IR_Block_{ip.replace('.', '_')}"
|
|
cmd = [
|
|
"netsh", "advfirewall", "firewall", "add", "rule",
|
|
f"name={rule_name}", "dir=in", "action=block",
|
|
f"remoteip={ip}", "protocol=any",
|
|
]
|
|
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
cmd_out = [
|
|
"netsh", "advfirewall", "firewall", "add", "rule",
|
|
f"name={rule_name}_out", "dir=out", "action=block",
|
|
f"remoteip={ip}", "protocol=any",
|
|
]
|
|
subprocess.run(cmd_out, capture_output=True, text=True, check=True)
|
|
results.append({"ip": ip, "status": "blocked", "method": "windows_firewall"})
|
|
logger.info(f"Blocked IP via Windows Firewall: {ip}")
|
|
except subprocess.CalledProcessError as e:
|
|
results.append({"ip": ip, "status": "failed", "error": str(e)})
|
|
logger.error(f"Failed to block IP {ip}: {e}")
|
|
return results
|
|
|
|
@staticmethod
|
|
def block_domains_hosts_file(domain_list: list) -> list:
|
|
results = []
|
|
hosts_path = r"C:\Windows\System32\drivers\etc\hosts" if os.name == "nt" else "/etc/hosts"
|
|
try:
|
|
with open(hosts_path, "a") as f:
|
|
for domain in domain_list:
|
|
f.write(f"\n0.0.0.0 {domain} # IR Containment Block")
|
|
results.append({"domain": domain, "status": "sinkholed", "method": "hosts_file"})
|
|
logger.info(f"Sinkholed domain: {domain}")
|
|
except PermissionError:
|
|
logger.error("Insufficient permissions to modify hosts file. Run as administrator.")
|
|
for domain in domain_list:
|
|
results.append({"domain": domain, "status": "failed", "error": "permission_denied"})
|
|
return results
|
|
|
|
|
|
class SplunkScopeAssessment:
|
|
"""Query Splunk SIEM for incident scope assessment."""
|
|
|
|
def __init__(self, base_url: str, token: str):
|
|
self.base_url = base_url
|
|
self.token = token
|
|
|
|
def _headers(self):
|
|
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
|
|
|
|
def search(self, query: str, earliest: str = "-24h", latest: str = "now") -> dict:
|
|
resp = requests.post(
|
|
f"{self.base_url}/services/search/jobs",
|
|
headers=self._headers(),
|
|
data={
|
|
"search": f"search {query}",
|
|
"earliest_time": earliest,
|
|
"latest_time": latest,
|
|
"output_mode": "json",
|
|
},
|
|
verify=False,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
def find_related_hosts(self, attacker_ip: str) -> dict:
|
|
query = f"""index=security (src_ip="{attacker_ip}" OR dest_ip="{attacker_ip}")
|
|
| stats count values(dest_ip) as targets values(src_ip) as sources by sourcetype
|
|
| sort -count"""
|
|
return self.search(query)
|
|
|
|
def find_compromised_accounts(self, host_list: list) -> dict:
|
|
hosts_filter = " OR ".join([f'src="{h}"' for h in host_list])
|
|
query = f"""index=security EventCode=4624 ({hosts_filter})
|
|
| stats count values(src) as source_hosts by Account_Name, Logon_Type
|
|
| where Logon_Type IN ("3","10")
|
|
| sort -count"""
|
|
return self.search(query)
|
|
|
|
|
|
def collect_volatile_evidence(output_dir: str) -> dict:
|
|
"""Collect volatile evidence from current system before containment."""
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
evidence = {}
|
|
|
|
# Network connections
|
|
try:
|
|
if os.name == "nt":
|
|
result = subprocess.run(["netstat", "-anob"], capture_output=True, text=True)
|
|
else:
|
|
result = subprocess.run(["ss", "-tulnp"], capture_output=True, text=True)
|
|
netconn_file = os.path.join(output_dir, "network_connections.txt")
|
|
with open(netconn_file, "w") as f:
|
|
f.write(result.stdout)
|
|
evidence["network_connections"] = {
|
|
"file": netconn_file,
|
|
"sha256": hashlib.sha256(result.stdout.encode()).hexdigest(),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to collect network connections: {e}")
|
|
|
|
# Running processes
|
|
try:
|
|
if os.name == "nt":
|
|
result = subprocess.run(["tasklist", "/V", "/FO", "CSV"], capture_output=True, text=True)
|
|
else:
|
|
result = subprocess.run(["ps", "auxwwf"], capture_output=True, text=True)
|
|
proc_file = os.path.join(output_dir, "running_processes.txt")
|
|
with open(proc_file, "w") as f:
|
|
f.write(result.stdout)
|
|
evidence["running_processes"] = {
|
|
"file": proc_file,
|
|
"sha256": hashlib.sha256(result.stdout.encode()).hexdigest(),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to collect process list: {e}")
|
|
|
|
# DNS cache
|
|
try:
|
|
if os.name == "nt":
|
|
result = subprocess.run(["ipconfig", "/displaydns"], capture_output=True, text=True)
|
|
else:
|
|
dns_cache_file = "/var/cache/nscd/hosts" if os.path.exists("/var/cache/nscd/hosts") else ""
|
|
result = subprocess.run(["cat", dns_cache_file], capture_output=True, text=True) if dns_cache_file else None
|
|
if result and result.stdout:
|
|
dns_file = os.path.join(output_dir, "dns_cache.txt")
|
|
with open(dns_file, "w") as f:
|
|
f.write(result.stdout)
|
|
evidence["dns_cache"] = {
|
|
"file": dns_file,
|
|
"sha256": hashlib.sha256(result.stdout.encode()).hexdigest(),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to collect DNS cache: {e}")
|
|
|
|
# ARP table
|
|
try:
|
|
result = subprocess.run(["arp", "-a"], capture_output=True, text=True)
|
|
arp_file = os.path.join(output_dir, "arp_table.txt")
|
|
with open(arp_file, "w") as f:
|
|
f.write(result.stdout)
|
|
evidence["arp_table"] = {
|
|
"file": arp_file,
|
|
"sha256": hashlib.sha256(result.stdout.encode()).hexdigest(),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to collect ARP table: {e}")
|
|
|
|
# Logged-in users
|
|
try:
|
|
if os.name == "nt":
|
|
result = subprocess.run(["query", "user"], capture_output=True, text=True)
|
|
else:
|
|
result = subprocess.run(["who"], capture_output=True, text=True)
|
|
users_file = os.path.join(output_dir, "logged_in_users.txt")
|
|
with open(users_file, "w") as f:
|
|
f.write(result.stdout)
|
|
evidence["logged_in_users"] = {
|
|
"file": users_file,
|
|
"sha256": hashlib.sha256(result.stdout.encode()).hexdigest(),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to collect logged-in users: {e}")
|
|
|
|
return evidence
|
|
|
|
|
|
def run_containment(args):
|
|
"""Execute the full containment workflow."""
|
|
action_log = ContainmentActionLog(args.incident_id)
|
|
logger.info(f"Starting containment for incident: {args.incident_id}")
|
|
|
|
# Step 1: Collect volatile evidence if requested
|
|
if args.collect_evidence:
|
|
evidence_dir = os.path.join(args.output_dir, "evidence", args.incident_id)
|
|
logger.info(f"Collecting volatile evidence to {evidence_dir}")
|
|
evidence = collect_volatile_evidence(evidence_dir)
|
|
for etype, edata in evidence.items():
|
|
action_log.log_action("evidence_collection", etype, "collected", f"SHA256: {edata['sha256']}")
|
|
|
|
# Step 2: Block IPs at firewall
|
|
if args.block_ips:
|
|
ip_list = [ip.strip() for ip in args.block_ips.split(",")]
|
|
logger.info(f"Blocking {len(ip_list)} IPs at firewall")
|
|
if os.name == "nt":
|
|
results = FirewallContainment.block_ips_windows_firewall(ip_list)
|
|
else:
|
|
results = FirewallContainment.block_ips_iptables(ip_list)
|
|
for r in results:
|
|
action_log.log_action("ip_block", r["ip"], r["status"], r.get("method", r.get("error", "")))
|
|
|
|
# Step 3: Block domains
|
|
if args.block_domains:
|
|
domain_list = [d.strip() for d in args.block_domains.split(",")]
|
|
logger.info(f"Sinkholing {len(domain_list)} domains")
|
|
results = FirewallContainment.block_domains_hosts_file(domain_list)
|
|
for r in results:
|
|
action_log.log_action("domain_block", r["domain"], r["status"], r.get("method", ""))
|
|
|
|
# Step 4: Isolate endpoints via CrowdStrike
|
|
if args.crowdstrike_isolate and args.cs_client_id and args.cs_client_secret:
|
|
cs = CrowdStrikeContainment(args.cs_client_id, args.cs_client_secret)
|
|
try:
|
|
cs.authenticate()
|
|
action_log.log_action("edr_auth", "crowdstrike", "success", "API authenticated")
|
|
hostnames = [h.strip() for h in args.crowdstrike_isolate.split(",")]
|
|
for hostname in hostnames:
|
|
device_id = cs.get_device_id_by_hostname(hostname)
|
|
if device_id:
|
|
cs.contain_host(device_id)
|
|
action_log.log_action("endpoint_isolation", hostname, "contained", f"Device ID: {device_id}")
|
|
else:
|
|
action_log.log_action("endpoint_isolation", hostname, "failed", "Device not found in Falcon")
|
|
except Exception as e:
|
|
action_log.log_action("edr_auth", "crowdstrike", "failed", str(e))
|
|
logger.error(f"CrowdStrike containment failed: {e}")
|
|
|
|
# Step 5: Disable AD accounts
|
|
if args.disable_accounts and args.ad_server and ldap3_available:
|
|
try:
|
|
ad = ActiveDirectoryContainment(
|
|
args.ad_server, args.ad_domain, args.ad_username, args.ad_password
|
|
)
|
|
accounts = [a.strip() for a in args.disable_accounts.split(",")]
|
|
for account in accounts:
|
|
result = ad.disable_account(account)
|
|
action_log.log_action(
|
|
"account_disable", account, "disabled" if result else "failed",
|
|
"AD account disabled" if result else "Account not found",
|
|
)
|
|
except Exception as e:
|
|
action_log.log_action("account_disable", "AD", "failed", str(e))
|
|
logger.error(f"AD containment failed: {e}")
|
|
|
|
# Export containment action log
|
|
os.makedirs(args.output_dir, exist_ok=True)
|
|
csv_path = os.path.join(args.output_dir, f"containment_log_{args.incident_id}.csv")
|
|
json_path = os.path.join(args.output_dir, f"containment_report_{args.incident_id}.json")
|
|
action_log.export_csv(csv_path)
|
|
action_log.export_json(json_path)
|
|
|
|
logger.info(f"Containment workflow completed for {args.incident_id}")
|
|
logger.info(f"Total actions taken: {len(action_log.actions)}")
|
|
return action_log
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Active Security Breach Containment Automation")
|
|
parser.add_argument("--incident-id", required=True, help="Incident tracking ID (e.g., IR-2024-001)")
|
|
parser.add_argument("--output-dir", default="./containment_output", help="Output directory for logs and reports")
|
|
parser.add_argument("--collect-evidence", action="store_true", help="Collect volatile evidence before containment")
|
|
parser.add_argument("--block-ips", help="Comma-separated list of IPs to block at firewall")
|
|
parser.add_argument("--block-domains", help="Comma-separated list of domains to sinkhole")
|
|
parser.add_argument("--crowdstrike-isolate", help="Comma-separated hostnames to isolate via CrowdStrike")
|
|
parser.add_argument("--cs-client-id", default=os.getenv("CS_CLIENT_ID"), help="CrowdStrike API client ID")
|
|
parser.add_argument("--cs-client-secret", default=os.getenv("CS_CLIENT_SECRET"), help="CrowdStrike API client secret")
|
|
parser.add_argument("--disable-accounts", help="Comma-separated AD accounts to disable")
|
|
parser.add_argument("--ad-server", default=os.getenv("AD_SERVER"), help="Active Directory server address")
|
|
parser.add_argument("--ad-domain", default=os.getenv("AD_DOMAIN"), help="Active Directory domain")
|
|
parser.add_argument("--ad-username", default=os.getenv("AD_USERNAME"), help="AD admin username")
|
|
parser.add_argument("--ad-password", default=os.getenv("AD_PASSWORD"), help="AD admin password")
|
|
|
|
args = parser.parse_args()
|
|
run_containment(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|