Files
Anthropic-Cybersecurity-Skills/skills/detecting-attacks-on-scada-systems/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

211 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""SCADA system attack detection agent."""
import json
import sys
import argparse
import socket
from datetime import datetime
try:
from pymodbus.client import ModbusTcpClient
except ImportError:
ModbusTcpClient = None
try:
import requests
except ImportError:
print("Install: pip install requests")
sys.exit(1)
SCADA_PORTS = {
502: ("Modbus TCP", "CRITICAL"),
102: ("Siemens S7comm", "CRITICAL"),
44818: ("EtherNet/IP CIP", "CRITICAL"),
20000: ("DNP3", "CRITICAL"),
4840: ("OPC-UA", "HIGH"),
47808: ("BACnet", "HIGH"),
2222: ("EtherNet/IP implicit", "HIGH"),
1089: ("Foundation Fieldbus HSE", "MEDIUM"),
34962: ("PROFINET RT", "HIGH"),
}
def scan_scada_services(host):
"""Scan for exposed SCADA protocol ports."""
results = []
for port, (proto, severity) in SCADA_PORTS.items():
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3)
if sock.connect_ex((host, port)) == 0:
results.append({
"host": host, "port": port, "protocol": proto,
"accessible": True, "severity": severity,
"finding": f"{proto} service exposed on port {port}",
})
sock.close()
except socket.error:
pass
return results
def detect_modbus_anomalies(host, port=502, unit_id=1):
"""Detect Modbus protocol anomalies indicating attack."""
if ModbusTcpClient is None:
return {"error": "Install pymodbus: pip install pymodbus"}
client = ModbusTcpClient(host, port=port, timeout=10)
findings = []
try:
if not client.connect():
return {"error": "Connection failed"}
rr = client.read_holding_registers(0, count=10, slave=unit_id)
if not rr.isError():
findings.append({
"check": "Read holding registers",
"status": "accessible",
"severity": "HIGH" if unit_id == 0 else "MEDIUM",
"detail": f"Registers 0-9 readable: {rr.registers}",
})
for test_unit in [0, 255]:
rr = client.read_holding_registers(0, count=1, slave=test_unit)
if not rr.isError():
findings.append({
"check": f"Broadcast unit ID {test_unit}",
"status": "accessible",
"severity": "CRITICAL",
"detail": f"Unit ID {test_unit} responds — broadcast address accessible",
})
rr = client.read_coils(0, count=100, slave=unit_id)
if not rr.isError():
findings.append({
"check": "Bulk coil read",
"status": "accessible",
"severity": "MEDIUM",
"detail": f"100 coils readable from address 0",
})
except Exception as e:
findings.append({"check": "error", "detail": str(e)})
finally:
client.close()
return {"host": host, "findings": findings}
def detect_s7comm_access(host, port=102):
"""Test Siemens S7comm accessibility (basic connection test)."""
result = {"host": host, "port": port, "protocol": "S7comm"}
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((host, port))
cotp_cr = bytes([
0x03, 0x00, 0x00, 0x16,
0x11, 0xe0, 0x00, 0x00,
0x00, 0x01, 0x00, 0xc0,
0x01, 0x0a, 0xc1, 0x02,
0x01, 0x00, 0xc2, 0x02,
0x01, 0x02,
])
sock.send(cotp_cr)
resp = sock.recv(1024)
sock.close()
if len(resp) > 0:
result["accessible"] = True
result["finding"] = "S7comm COTP connection accepted — PLC accessible"
result["severity"] = "CRITICAL"
else:
result["accessible"] = False
except Exception as e:
result["accessible"] = False
result["error"] = str(e)
return result
def query_scada_siem(siem_url, api_key, hours=24):
"""Query SIEM for SCADA-related security events."""
headers = {"Authorization": f"Bearer {api_key}"}
try:
resp = requests.get(f"{siem_url}/api/v1/events", headers=headers,
params={"category": "scada", "hours": hours}, timeout=15)
resp.raise_for_status()
events = resp.json().get("events", [])
findings = []
for evt in events:
if evt.get("severity", 0) >= 7:
findings.append({
"event_id": evt.get("id", ""),
"source": evt.get("source_ip", ""),
"target": evt.get("dest_ip", ""),
"description": evt.get("description", ""),
"severity": "CRITICAL" if evt["severity"] >= 9 else "HIGH",
})
return findings
except Exception as e:
return [{"error": str(e)}]
def run_audit(args):
"""Execute SCADA attack detection audit."""
print(f"\n{'='*60}")
print(f" SCADA SYSTEM ATTACK DETECTION")
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
print(f"{'='*60}\n")
report = {}
if args.host:
services = scan_scada_services(args.host)
report["scada_services"] = services
print(f"--- SCADA SERVICE SCAN ({args.host}) ---")
if services:
for s in services:
print(f" [{s['severity']}] {s['protocol']} on port {s['port']}")
else:
print(" No SCADA ports detected (good segmentation)")
if args.modbus_host:
modbus = detect_modbus_anomalies(args.modbus_host, args.modbus_port or 502)
report["modbus_audit"] = modbus
print(f"\n--- MODBUS ANOMALY DETECTION ---")
for f in modbus.get("findings", []):
print(f" [{f.get('severity','')}] {f['check']}: {f.get('detail','')[:80]}")
if args.s7_host:
s7 = detect_s7comm_access(args.s7_host)
report["s7comm_check"] = s7
print(f"\n--- S7COMM ACCESS CHECK ---")
print(f" Accessible: {s7.get('accessible', False)}")
if s7.get("finding"):
print(f" [{s7['severity']}] {s7['finding']}")
return report
def main():
parser = argparse.ArgumentParser(description="SCADA Attack Detection Agent")
parser.add_argument("--host", help="SCADA host to scan for services")
parser.add_argument("--modbus-host", help="Modbus device to audit")
parser.add_argument("--modbus-port", type=int, default=502)
parser.add_argument("--s7-host", help="Siemens S7 PLC to check")
parser.add_argument("--siem-url", help="SIEM API URL for SCADA events")
parser.add_argument("--siem-key", help="SIEM API key")
parser.add_argument("--output", help="Save report to JSON file")
args = parser.parse_args()
report = run_audit(args)
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"\n[+] Report saved to {args.output}")
if __name__ == "__main__":
main()