mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 14:14:56 +03:00
27c6414ca5
Complete skill folder anatomy across all cybersecurity skills: - scripts/agent.py: 80-150 line Python agents using real libraries (impacket, boto3, azure-mgmt-*, kubernetes, pefile, yara, scapy, shodan, stix2, etc.) - references/api-reference.md: real API documentation with method signatures - LICENSE: MIT license for all skill folders
171 lines
5.4 KiB
Python
171 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Modbus protocol anomaly detection agent for OT/ICS networks.
|
|
|
|
Detects protocol-level anomalies in Modbus TCP traffic including malformed
|
|
packets, timing deviations, register range violations, and replay attacks.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from collections import Counter, defaultdict
|
|
from datetime import datetime
|
|
|
|
MODBUS_FUNCTIONS = {
|
|
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",
|
|
}
|
|
|
|
VALID_REGISTER_RANGES = {
|
|
"coils": (0, 65535), "discrete_inputs": (0, 65535),
|
|
"holding_registers": (0, 65535), "input_registers": (0, 65535),
|
|
}
|
|
|
|
MAX_REGISTER_READ = 125
|
|
MAX_COIL_READ = 2000
|
|
|
|
|
|
def parse_modbus_log(filepath):
|
|
events = []
|
|
with open(filepath, "r") as f:
|
|
headers = None
|
|
for line in f:
|
|
if line.startswith("#fields"):
|
|
headers = line.strip().split("\t")[1:]
|
|
continue
|
|
if line.startswith("#"):
|
|
continue
|
|
if not headers:
|
|
continue
|
|
fields = line.strip().split("\t")
|
|
if len(fields) >= len(headers):
|
|
events.append(dict(zip(headers, fields)))
|
|
return events
|
|
|
|
|
|
def detect_timing_anomalies(events, expected_interval=1.0, tolerance=0.5):
|
|
findings = []
|
|
pair_timestamps = defaultdict(list)
|
|
|
|
for evt in events:
|
|
src = evt.get("id.orig_h", "")
|
|
dst = evt.get("id.resp_h", "")
|
|
try:
|
|
ts = float(evt.get("ts", 0))
|
|
except ValueError:
|
|
continue
|
|
pair_timestamps[f"{src}->{dst}"].append(ts)
|
|
|
|
for pair, timestamps in pair_timestamps.items():
|
|
timestamps.sort()
|
|
for i in range(1, len(timestamps)):
|
|
interval = timestamps[i] - timestamps[i-1]
|
|
if interval < expected_interval - tolerance or interval > expected_interval * 3:
|
|
findings.append({
|
|
"type": "timing_anomaly",
|
|
"pair": pair,
|
|
"expected_interval": expected_interval,
|
|
"actual_interval": round(interval, 3),
|
|
"severity": "MEDIUM" if interval > expected_interval * 3 else "HIGH",
|
|
})
|
|
break
|
|
return findings
|
|
|
|
|
|
def detect_register_anomalies(events):
|
|
findings = []
|
|
for evt in events:
|
|
fc_str = evt.get("func", "0")
|
|
try:
|
|
fc = int(fc_str)
|
|
except ValueError:
|
|
continue
|
|
|
|
quantity = evt.get("quantity", "0")
|
|
try:
|
|
qty = int(quantity)
|
|
except ValueError:
|
|
qty = 0
|
|
|
|
if fc in (1, 2) and qty > MAX_COIL_READ:
|
|
findings.append({
|
|
"type": "excessive_coil_read",
|
|
"function_code": fc,
|
|
"quantity": qty,
|
|
"max_allowed": MAX_COIL_READ,
|
|
"severity": "HIGH",
|
|
"source": evt.get("id.orig_h", ""),
|
|
})
|
|
elif fc in (3, 4) and qty > MAX_REGISTER_READ:
|
|
findings.append({
|
|
"type": "excessive_register_read",
|
|
"function_code": fc,
|
|
"quantity": qty,
|
|
"max_allowed": MAX_REGISTER_READ,
|
|
"severity": "HIGH",
|
|
"source": evt.get("id.orig_h", ""),
|
|
})
|
|
|
|
if fc not in MODBUS_FUNCTIONS:
|
|
findings.append({
|
|
"type": "invalid_function_code",
|
|
"function_code": fc,
|
|
"severity": "HIGH",
|
|
"source": evt.get("id.orig_h", ""),
|
|
})
|
|
return findings
|
|
|
|
|
|
def detect_scan_patterns(events, threshold=50):
|
|
findings = []
|
|
src_fc_counter = defaultdict(Counter)
|
|
for evt in events:
|
|
src = evt.get("id.orig_h", "")
|
|
fc_str = evt.get("func", "0")
|
|
try:
|
|
fc = int(fc_str)
|
|
except ValueError:
|
|
continue
|
|
src_fc_counter[src][fc] += 1
|
|
|
|
for src, fc_counts in src_fc_counter.items():
|
|
unique_fcs = len(fc_counts)
|
|
total = sum(fc_counts.values())
|
|
if unique_fcs > 5 or (fc_counts.get(17, 0) > 0 and fc_counts.get(43, 0) > 0):
|
|
findings.append({
|
|
"type": "modbus_scan",
|
|
"source": src,
|
|
"unique_function_codes": unique_fcs,
|
|
"total_requests": total,
|
|
"severity": "HIGH",
|
|
"description": f"Modbus enumeration from {src}: {unique_fcs} function codes",
|
|
})
|
|
return findings
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Modbus Protocol Anomaly Detector")
|
|
parser.add_argument("--modbus-log", required=True, help="Zeek modbus.log file")
|
|
parser.add_argument("--expected-interval", type=float, default=1.0,
|
|
help="Expected polling interval in seconds")
|
|
args = parser.parse_args()
|
|
|
|
events = parse_modbus_log(args.modbus_log)
|
|
all_findings = []
|
|
all_findings.extend(detect_timing_anomalies(events, args.expected_interval))
|
|
all_findings.extend(detect_register_anomalies(events))
|
|
all_findings.extend(detect_scan_patterns(events))
|
|
|
|
results = {
|
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
"total_events": len(events),
|
|
"findings": all_findings,
|
|
"total_findings": len(all_findings),
|
|
}
|
|
print(json.dumps(results, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|