mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-14 11:40:15 +03:00
c21af3347e
- Add scripts/agent.py and references/api-reference.md to all remaining skills - Update all 648 LICENSE files: copyright now reads 'Mahipal' - Add implementing-security-monitoring-with-datadog (new skill with full anatomy) - All 649 skills now have: SKILL.md, LICENSE, scripts/agent.py, references/api-reference.md
181 lines
6.6 KiB
Python
181 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""ICS/SCADA anomaly detection agent for industrial control systems."""
|
|
|
|
import json
|
|
import sys
|
|
import argparse
|
|
import struct
|
|
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)
|
|
|
|
|
|
MODBUS_FUNCTION_CODES = {
|
|
1: "Read Coils", 2: "Read Discrete Inputs", 3: "Read Holding Registers",
|
|
4: "Read Input Registers", 5: "Write Single Coil", 6: "Write Single Register",
|
|
15: "Write Multiple Coils", 16: "Write Multiple Registers",
|
|
43: "Read Device Identification",
|
|
}
|
|
|
|
ANOMALOUS_FUNCTION_CODES = {8, 17, 43, 90, 100}
|
|
|
|
|
|
def scan_modbus_device(host, port=502, unit_id=1):
|
|
"""Read Modbus device identification and holding registers."""
|
|
if ModbusTcpClient is None:
|
|
return {"error": "Install pymodbus: pip install pymodbus"}
|
|
client = ModbusTcpClient(host, port=port, timeout=10)
|
|
result = {"host": host, "port": port, "unit_id": unit_id}
|
|
try:
|
|
if not client.connect():
|
|
result["error"] = "Connection failed"
|
|
return result
|
|
rr = client.read_holding_registers(0, count=10, slave=unit_id)
|
|
if not rr.isError():
|
|
result["holding_registers_0_9"] = rr.registers
|
|
rr2 = client.read_device_information(slave=unit_id)
|
|
if hasattr(rr2, "information") and not rr2.isError():
|
|
result["device_info"] = {k: v.decode() if isinstance(v, bytes) else v
|
|
for k, v in rr2.information.items()}
|
|
except Exception as e:
|
|
result["error"] = str(e)
|
|
finally:
|
|
client.close()
|
|
return result
|
|
|
|
|
|
def analyze_modbus_traffic(pcap_summary):
|
|
"""Analyze Modbus traffic patterns for anomalies from parsed PCAP data."""
|
|
findings = []
|
|
for entry in pcap_summary:
|
|
fc = entry.get("function_code", 0)
|
|
if fc in ANOMALOUS_FUNCTION_CODES:
|
|
findings.append({
|
|
"src": entry.get("src_ip", ""),
|
|
"dst": entry.get("dst_ip", ""),
|
|
"function_code": fc,
|
|
"issue": f"Unusual Modbus function code {fc} — potential reconnaissance",
|
|
"severity": "HIGH",
|
|
})
|
|
if entry.get("write_count", 0) > 100:
|
|
findings.append({
|
|
"src": entry.get("src_ip", ""),
|
|
"issue": f"High write frequency ({entry['write_count']} writes) — possible attack",
|
|
"severity": "CRITICAL",
|
|
})
|
|
if entry.get("exception_code"):
|
|
findings.append({
|
|
"src": entry.get("src_ip", ""),
|
|
"issue": f"Modbus exception code {entry['exception_code']} — device error",
|
|
"severity": "MEDIUM",
|
|
})
|
|
return findings
|
|
|
|
|
|
def check_ics_network_segmentation(host, ics_ports=None):
|
|
"""Verify ICS network segmentation by testing connectivity to OT ports."""
|
|
if ics_ports is None:
|
|
ics_ports = [502, 102, 44818, 20000, 4840]
|
|
port_names = {502: "Modbus", 102: "S7comm", 44818: "EtherNet/IP",
|
|
20000: "DNP3", 4840: "OPC-UA"}
|
|
results = []
|
|
for port in ics_ports:
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(3)
|
|
open_status = sock.connect_ex((host, port)) == 0
|
|
sock.close()
|
|
result = {
|
|
"host": host, "port": port,
|
|
"protocol": port_names.get(port, "unknown"),
|
|
"accessible": open_status,
|
|
}
|
|
if open_status:
|
|
result["finding"] = f"{port_names.get(port, '')} port accessible from IT network"
|
|
result["severity"] = "CRITICAL"
|
|
results.append(result)
|
|
except socket.error:
|
|
pass
|
|
return results
|
|
|
|
|
|
def query_historian_anomalies(historian_url, api_key, tag_name, hours=24):
|
|
"""Query process historian for anomalous sensor readings."""
|
|
headers = {"Authorization": f"Bearer {api_key}"}
|
|
try:
|
|
resp = requests.get(f"{historian_url}/api/v1/tags/{tag_name}/values",
|
|
params={"hours": hours}, headers=headers, timeout=15)
|
|
resp.raise_for_status()
|
|
data = resp.json().get("values", [])
|
|
values = [v["value"] for v in data if "value" in v]
|
|
if not values:
|
|
return {"tag": tag_name, "anomalies": []}
|
|
avg = sum(values) / len(values)
|
|
std = (sum((v - avg) ** 2 for v in values) / len(values)) ** 0.5
|
|
anomalies = [v for v in data if abs(v.get("value", avg) - avg) > 3 * std]
|
|
return {"tag": tag_name, "mean": avg, "std_dev": std,
|
|
"total_readings": len(values), "anomalies": len(anomalies)}
|
|
except Exception as e:
|
|
return {"tag": tag_name, "error": str(e)}
|
|
|
|
|
|
def run_audit(args):
|
|
"""Execute ICS anomaly detection audit."""
|
|
print(f"\n{'='*60}")
|
|
print(f" ICS ANOMALY DETECTION AUDIT")
|
|
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
|
|
print(f"{'='*60}\n")
|
|
|
|
report = {}
|
|
|
|
if args.modbus_host:
|
|
device = scan_modbus_device(args.modbus_host, args.modbus_port or 502)
|
|
report["modbus_device"] = device
|
|
print(f"--- MODBUS DEVICE SCAN ---")
|
|
print(f" Host: {device['host']}:{device['port']}")
|
|
if device.get("holding_registers_0_9"):
|
|
print(f" Registers 0-9: {device['holding_registers_0_9']}")
|
|
if device.get("error"):
|
|
print(f" Error: {device['error']}")
|
|
|
|
if args.scan_host:
|
|
seg_results = check_ics_network_segmentation(args.scan_host)
|
|
report["segmentation_check"] = seg_results
|
|
print(f"\n--- NETWORK SEGMENTATION CHECK ---")
|
|
for r in seg_results:
|
|
status = "ACCESSIBLE" if r["accessible"] else "BLOCKED"
|
|
print(f" {r['protocol']} (:{r['port']}): {status}")
|
|
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="ICS Anomaly Detection Agent")
|
|
parser.add_argument("--modbus-host", help="Modbus device IP to scan")
|
|
parser.add_argument("--modbus-port", type=int, default=502, help="Modbus port")
|
|
parser.add_argument("--scan-host", help="Host to test ICS segmentation")
|
|
parser.add_argument("--historian-url", help="Process historian API URL")
|
|
parser.add_argument("--historian-key", help="Historian 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()
|