
Detecting Attacks On Scada Systems
- 63 installs
- 27.3k repo stars
- Updated August 2, 2026
- mukul975/anthropic-cybersecurity-skills
Helps with ai & agent building tasks.
About
detecting-attacks-on-scada-systems is a Claude Code skill in the AI & Agent Building category.
- detecting-attacks-on-scada-systems
- AI & Agent Building
- AI-coding skill
Detecting Attacks On Scada Systems by the numbers
- 63 all-time installs (skills.sh)
- Ranked #6,243 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill detecting-attacks-on-scada-systemsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 63 |
|---|---|
| repo stars | ★ 27.3k |
| Last updated | August 2, 2026 |
| Repository | mukul975/anthropic-cybersecurity-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Detecting Attacks on SCADA Systems
When to Use
- When deploying intrusion detection capabilities in a SCADA environment for the first time
- When investigating suspected cyber attacks against industrial control systems
- When building detection rules for OT-specific attack patterns (Stuxnet, TRITON, Industroyer)
- When integrating OT network monitoring with an enterprise SOC for unified threat visibility
- When responding to alerts from OT security monitoring tools (Dragos, Nozomi, Claroty)
Do not use for detecting attacks on IT-only networks without SCADA/ICS components, for building generic network IDS rules (see building-detection-rules-with-sigma), or for incident response procedures after an attack is confirmed (see performing-ot-incident-response).
Prerequisites
- Passive network monitoring sensors deployed on SPAN/TAP ports at OT network boundaries
- OT intrusion detection system (Dragos Platform, Nozomi Guardian, Claroty xDome, or Suricata with OT rulesets)
- Understanding of industrial protocols in use (Modbus, DNP3, OPC UA, EtherNet/IP, S7comm)
- Baseline of normal SCADA communication patterns (polling intervals, function codes, register ranges)
- Access to process historian data for physical process anomaly correlation
Workflow
Step 1: Establish SCADA Communication Baselines
Before detecting anomalies, establish what normal SCADA traffic looks like. Industrial protocols are highly deterministic - the same master polls the same slaves at the same intervals reading the same registers.
#!/usr/bin/env python3
"""SCADA Communication Baseline Builder.
Analyzes OT network traffic to establish deterministic baselines for
Modbus/TCP, DNP3, EtherNet/IP, and S7comm communications.
"""
import json
import sys
from collections import defaultdict
from datetime import datetime
from statistics import mean, stdev
try:
from scapy.all import rdpcap, IP, TCP, UDP
except ImportError:
print("Install scapy: pip install scapy")
sys.exit(1)
MODBUS_FUNC_NAMES = {
1: "Read Coils", 2: "Read Discrete Inputs",
3: "Read Holding Registers", 4: "Read Input Registers",
5: "Write Single Coil", 6: "Write Single Register",
8: "Diagnostics", 15: "Write Multiple Coils",
16: "Write Multiple Registers", 17: "Report Slave ID",
22: "Mask Write Register", 23: "Read/Write Multiple Registers",
43: "Encapsulated Interface Transport",
}
class SCADABaselineBuilder:
"""Builds deterministic baselines from SCADA traffic captures."""
def __init__(self):
self.modbus_sessions = defaultdict(lambda: {
"func_codes": defaultdict(int),
"register_ranges": set(),
"intervals": [],
"last_seen": None,
"request_count": 0,
})
self.communication_pairs = defaultdict(lambda: {
"protocols": set(),
"packet_count": 0,
"first_seen": None,
"last_seen": None,
})
def process_pcap(self, pcap_file):
"""Process pcap file to build SCADA baselines."""
packets = rdpcap(pcap_file)
print(f"[*] Processing {len(packets)} packets for baseline...")
for pkt in packets:
if not pkt.haslayer(IP):
continue
src = pkt[IP].src
dst = pkt[IP].dst
ts = float(pkt.time)
# Track communication pairs
pair_key = f"{src}->{dst}"
pair = self.communication_pairs[pair_key]
pair["packet_count"] += 1
if pair["first_seen"] is None:
pair["first_seen"] = ts
pair["last_seen"] = ts
# Analyze Modbus/TCP
if pkt.haslayer(TCP) and pkt[TCP].dport == 502:
self._analyze_modbus(pkt, src, dst, ts)
def _analyze_modbus(self, pkt, src, dst, timestamp):
"""Extract Modbus function codes and register ranges."""
payload = bytes(pkt[TCP].payload)
if len(payload) < 8:
return
# MBAP header: transaction_id(2) + protocol_id(2) + length(2) + unit_id(1) + func_code(1)
func_code = payload[7]
session_key = f"{src}->{dst}"
session = self.modbus_sessions[session_key]
session["func_codes"][func_code] += 1
session["request_count"] += 1
session["protocols"] = {"Modbus/TCP"}
# Track polling intervals
if session["last_seen"] is not None:
interval = timestamp - session["last_seen"]
if 0.01 < interval < 60: # Reasonable polling interval
session["intervals"].append(interval)
session["last_seen"] = timestamp
# Extract register range for read/write operations
if len(payload) >= 12 and func_code in (1, 2, 3, 4, 5, 6, 15, 16):
start_register = (payload[8] << 8) | payload[9]
if func_code in (1, 2, 3, 4, 15, 16) and len(payload) >= 12:
count = (payload[10] << 8) | payload[11]
session["register_ranges"].add((func_code, start_register, start_register + count))
def generate_baseline(self):
"""Generate the baseline profile from collected data."""
baseline = {
"generated": datetime.now().isoformat(),
"modbus_baselines": {},
"communication_pairs": {},
}
for session_key, session in self.modbus_sessions.items():
avg_interval = mean(session["intervals"]) if session["intervals"] else 0
interval_std = stdev(session["intervals"]) if len(session["intervals"]) > 1 else 0
baseline["modbus_baselines"][session_key] = {
"allowed_function_codes": list(session["func_codes"].keys()),
"function_code_distribution": {
MODBUS_FUNC_NAMES.get(k, f"FC{k}"): v
for k, v in session["func_codes"].items()
},
"polling_interval_avg_sec": round(avg_interval, 3),
"polling_interval_stddev": round(interval_std, 3),
"register_ranges": [
{"func_code": r[0], "start": r[1], "end": r[2]}
for r in session["register_ranges"]
],
"total_requests": session["request_count"],
}
return baseline
def export_baseline(self, output_file):
"""Export baseline to JSON file."""
baseline = self.generate_baseline()
with open(output_file, "w") as f:
json.dump(baseline, f, indent=2)
print(f"[*] Baseline saved to: {output_file}")
# Print summary
print(f"\n{'='*60}")
print("SCADA COMMUNICATION BASELINE SUMMARY")
print(f"{'='*60}")
for session, data in baseline["modbus_baselines"].items():
print(f"\n Session: {session}")
print(f" Function Codes: {data['allowed_function_codes']}")
print(f" Polling Interval: {data['polling_interval_avg_sec']}s (+/- {data['polling_interval_stddev']}s)")
print(f" Register Ranges: {len(data['register_ranges'])}")
print(f" Total Requests: {data['total_requests']}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python scada_baseline.py <pcap_file> [output.json]")
sys.exit(1)
builder = SCADABaselineBuilder()
builder.process_pcap(sys.argv[1])
output = sys.argv[2] if len(sys.argv) > 2 else "scada_baseline.json"
builder.export_baseline(output)Step 2: Deploy OT-Specific Detection Rules
Create detection rules for known SCADA attack patterns including those used by TRITON, Industroyer/CrashOverride, and PIPEDREAM/INCONTROLLER.
# Suricata Rules for SCADA Attack Detection
# Deploy on IDS sensor monitoring OT network SPAN port
# --- Modbus Attack Detection ---
# Unauthorized Modbus write to PLC from non-engineering workstation
alert modbus any any -> $OT_PLC_SUBNET 502 (
msg:"OT-DETECT Modbus write from unauthorized source";
modbus_func:!read_coils; modbus_func:!read_discrete_inputs;
modbus_func:!read_holding_registers; modbus_func:!read_input_registers;
flow:to_server,established;
threshold:type both, track by_src, count 1, seconds 60;
classtype:attempted-admin;
sid:3000001; rev:1;
)
# Modbus diagnostic/restart command (FC 8) - potential PLC DoS
alert modbus any any -> $OT_PLC_SUBNET 502 (
msg:"OT-DETECT Modbus diagnostics command to PLC";
modbus_func:diagnostics;
flow:to_server,established;
classtype:attempted-dos;
sid:3000002; rev:1;
)
# Modbus broadcast write (unit ID 0) - affects all slaves
alert modbus any any -> $OT_PLC_SUBNET 502 (
msg:"OT-CRITICAL Modbus broadcast write command";
modbus_unit_id:0;
flow:to_server,established;
classtype:attempted-admin;
sid:3000003; rev:1;
priority:1;
)
# --- S7comm Attack Detection (Siemens) ---
# S7comm CPU STOP command - shuts down PLC execution
alert tcp any any -> $SIEMENS_PLC_SUBNET 102 (
msg:"OT-CRITICAL S7comm CPU STOP command detected";
content:"|03 00|"; offset:0; depth:2;
content:"|29|"; offset:17; depth:1;
flow:to_server,established;
classtype:attempted-dos;
sid:3000010; rev:1;
priority:1;
)
# S7comm PLC program upload (potential logic modification)
alert tcp any any -> $SIEMENS_PLC_SUBNET 102 (
msg:"OT-CRITICAL S7comm program download to PLC";
content:"|03 00|"; offset:0; depth:2;
content:"|1a|"; offset:17; depth:1;
flow:to_server,established;
classtype:attempted-admin;
sid:3000011; rev:1;
priority:1;
)
# --- DNP3 Attack Detection ---
# DNP3 cold restart command
alert tcp any any -> $OT_RTU_SUBNET 20000 (
msg:"OT-CRITICAL DNP3 cold restart command";
content:"|05 64|"; offset:0; depth:2;
content:"|0d|"; offset:12; depth:1;
flow:to_server,established;
classtype:attempted-dos;
sid:3000020; rev:1;
priority:1;
)
# DNP3 firmware update command - potential PIPEDREAM indicator
alert tcp any any -> $OT_RTU_SUBNET 20000 (
msg:"OT-CRITICAL DNP3 file transfer / firmware update";
content:"|05 64|"; offset:0; depth:2;
content:"|19|"; offset:12; depth:1;
flow:to_server,established;
classtype:attempted-admin;
sid:3000021; rev:1;
priority:1;
)
# --- Network Anomaly Detection ---
# New device communicating with PLCs (not in baseline)
alert ip !$AUTHORIZED_OT_HOSTS any -> $OT_PLC_SUBNET any (
msg:"OT-DETECT Unauthorized device communicating with PLC subnet";
flow:to_server;
threshold:type limit, track by_src, count 1, seconds 3600;
classtype:network-scan;
sid:3000030; rev:1;
)
# Port scan targeting OT protocols
alert tcp any any -> $OT_NETWORK any (
msg:"OT-DETECT Port scan targeting industrial protocols";
flags:S;
threshold:type threshold, track by_src, count 10, seconds 60;
classtype:network-scan;
sid:3000031; rev:1;
)Step 3: Implement Process Data Anomaly Detection
Monitor physical process data from the historian to detect attacks that manipulate the process while hiding their effects from operators (the Stuxnet attack pattern).
#!/usr/bin/env python3
"""SCADA Process Data Anomaly Detector.
Monitors historian data to detect physical process anomalies
that may indicate cyber attacks manipulating control logic
while spoofing sensor readings (Stuxnet-style attacks).
"""
import json
import sys
import time
from collections import deque
from dataclasses import dataclass
from datetime import datetime
from statistics import mean, stdev
from typing import Optional
try:
import requests
except ImportError:
print("Install requests: pip install requests")
sys.exit(1)
@dataclass
class ProcessVariable:
"""Represents a monitored process variable."""
tag_name: str
description: str
unit: str
low_limit: float
high_limit: float
rate_of_change_limit: float # Maximum change per second
engineering_low: float
engineering_high: float
@dataclass
class Anomaly:
"""Represents a detected process anomaly."""
timestamp: str
tag_name: str
anomaly_type: str
severity: str
current_value: float
expected_range: str
description: str
attack_pattern: str = ""
class ProcessAnomalyDetector:
"""Detects anomalies in SCADA process data from historian."""
def __init__(self, historian_url, api_key=None):
self.historian_url = historian_url
self.api_key = api_key
self.variables = {}
self.history = defaultdict(lambda: deque(maxlen=1000))
self.anomalies = []
def add_variable(self, var: ProcessVariable):
"""Register a process variable to monitor."""
self.variables[var.tag_name] = var
def fetch_current_values(self):
"""Fetch current values from historian API."""
headers = {}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
tag_list = list(self.variables.keys())
params = {"tags": ",".join(tag_list), "count": 1}
try:
resp = requests.get(
f"{self.historian_url}/api/v1/streams/values/current",
params=params,
headers=headers,
timeout=10,
verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true", # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
)
resp.raise_for_status()
return resp.json()
except requests.RequestException as e:
print(f"[ERROR] Historian API error: {e}")
return {}
def check_value(self, tag_name, value, timestamp):
"""Check a process variable value against all detection rules."""
var = self.variables.get(tag_name)
if not var:
return
self.history[tag_name].append((timestamp, value))
# Rule 1: Value out of engineering limits
if value < var.engineering_low or value > var.engineering_high:
self.anomalies.append(Anomaly(
timestamp=timestamp,
tag_name=tag_name,
anomaly_type="OUT_OF_RANGE",
severity="critical",
current_value=value,
expected_range=f"{var.engineering_low}-{var.engineering_high} {var.unit}",
description=f"{tag_name} ({var.description}) at {value} {var.unit} - outside engineering limits",
attack_pattern="Process manipulation - value driven outside safe operating range",
))
# Rule 2: Rate of change exceeds physical limits
history = list(self.history[tag_name])
if len(history) >= 2:
prev_ts, prev_val = history[-2]
try:
dt = (datetime.fromisoformat(timestamp) - datetime.fromisoformat(prev_ts)).total_seconds()
if dt > 0:
rate = abs(value - prev_val) / dt
if rate > var.rate_of_change_limit:
self.anomalies.append(Anomaly(
timestamp=timestamp,
tag_name=tag_name,
anomaly_type="RATE_OF_CHANGE_VIOLATION",
severity="high",
current_value=value,
expected_range=f"Max rate: {var.rate_of_change_limit} {var.unit}/s",
description=(
f"{tag_name} changing at {rate:.2f} {var.unit}/s "
f"(limit: {var.rate_of_change_limit} {var.unit}/s)"
),
attack_pattern="Possible sensor spoofing or actuator manipulation",
))
except (ValueError, TypeError):
pass
# Rule 3: Flatline detection (sensor reading not changing when process is active)
if len(history) >= 20:
recent_values = [v for _, v in list(history)[-20:]]
if len(set(recent_values)) == 1:
self.anomalies.append(Anomaly(
timestamp=timestamp,
tag_name=tag_name,
anomaly_type="FLATLINE_DETECTED",
severity="high",
current_value=value,
expected_range="Expected variation during active process",
description=f"{tag_name} flatlined at {value} for 20+ consecutive readings",
attack_pattern="Stuxnet-style replay attack - frozen sensor value while process is manipulated",
))
# Rule 4: Statistical anomaly (z-score based)
if len(history) >= 50:
values = [v for _, v in list(history)[-50:]]
avg = mean(values)
std = stdev(values) if len(values) > 1 else 0
if std > 0:
z_score = abs(value - avg) / std
if z_score > 3.5:
self.anomalies.append(Anomaly(
timestamp=timestamp,
tag_name=tag_name,
anomaly_type="STATISTICAL_ANOMALY",
severity="medium",
current_value=value,
expected_range=f"Mean: {avg:.2f}, StdDev: {std:.2f} (z={z_score:.1f})",
description=f"{tag_name} value {value} is {z_score:.1f} standard deviations from mean",
attack_pattern="Possible gradual process manipulation",
))
def report_anomalies(self):
"""Print detected anomalies."""
if not self.anomalies:
print("[*] No anomalies detected")
return
print(f"\n{'='*70}")
print(f"PROCESS ANOMALY DETECTION REPORT - {len(self.anomalies)} anomalies")
print(f"{'='*70}")
for a in self.anomalies:
print(f"\n [{a.severity.upper()}] {a.anomaly_type}")
print(f" Time: {a.timestamp}")
print(f" Tag: {a.tag_name}")
print(f" Value: {a.current_value}")
print(f" Expected: {a.expected_range}")
print(f" Detail: {a.description}")
if a.attack_pattern:
print(f" Attack Pattern: {a.attack_pattern}")
if __name__ == "__main__":
from collections import defaultdict
detector = ProcessAnomalyDetector(
historian_url="https://10.30.1.50:5450",
)
# Define monitored process variables for a chemical reactor
detector.add_variable(ProcessVariable(
tag_name="REACTOR_01.TEMP",
description="Reactor 1 Temperature",
unit="C",
low_limit=150, high_limit=280,
rate_of_change_limit=5.0,
engineering_low=100, engineering_high=350,
))
detector.add_variable(ProcessVariable(
tag_name="REACTOR_01.PRESSURE",
description="Reactor 1 Pressure",
unit="bar",
low_limit=2.0, high_limit=8.0,
rate_of_change_limit=0.5,
engineering_low=0, engineering_high=12.0,
))
detector.add_variable(ProcessVariable(
tag_name="PUMP_03.FLOW",
description="Feed Pump 3 Flow Rate",
unit="m3/h",
low_limit=5.0, high_limit=25.0,
rate_of_change_limit=2.0,
engineering_low=0, engineering_high=30.0,
))
print("[*] Starting process anomaly monitoring...")
print("[*] Press Ctrl+C to stop and generate report")
try:
while True:
data = detector.fetch_current_values()
for item in data.get("items", []):
detector.check_value(
item.get("tag"),
item.get("value"),
item.get("timestamp", datetime.now().isoformat()),
)
time.sleep(5)
except KeyboardInterrupt:
detector.report_anomalies()Step 4: Detect Known ICS Malware Indicators
Monitor for indicators of compromise (IOCs) associated with known ICS-targeting malware families.
# Known ICS Malware Detection Signatures
# Reference: MITRE ATT&CK for ICS, CISA ICS-CERT advisories
malware_families:
TRITON_TRISIS:
description: "Targets Schneider Electric Triconex Safety Instrumented Systems"
target: "Safety controllers (SIS)"
network_indicators:
- protocol: "TriStation"
port: 1502
pattern: "Unusual TriStation commands from non-engineering workstation"
- protocol: "TCP"
pattern: "Connection to Triconex controller from unauthorized IP"
host_indicators:
- "trilog.exe present on engineering workstation"
- "inject.bin in System32 directory"
- "imain.bin payload targeting Triconex firmware"
detection_rule: |
alert tcp !$SIS_ENGINEERING_WS any -> $SIS_CONTROLLERS 1502 (
msg:"OT-CRITICAL Unauthorized TriStation connection to SIS";
flow:to_server; sid:3000100; rev:1; priority:1;)
INDUSTROYER_CRASHOVERRIDE:
description: "Targets power grid SCADA via IEC 60870-5-101/104, IEC 61850, OPC DA"
target: "Power grid substations and SCADA"
network_indicators:
- protocol: "IEC 60870-5-104"
port: 2404
pattern: "Rapid sequence of control commands outside normal polling"
- protocol: "OPC DA"
pattern: "Enumeration of OPC servers followed by write commands"
host_indicators:
- "haslo.exe (backdoor launcher)"
- "61850.dll (IEC 61850 attack module)"
- "OPC.dll (OPC DA attack module)"
- "104.dll (IEC 104 attack module)"
detection_rule: |
alert tcp any any -> $SUBSTATION_RTU 2404 (
msg:"OT-CRITICAL Rapid IEC 104 control commands - Industroyer pattern";
flow:to_server,established;
threshold:type threshold, track by_src, count 50, seconds 10;
sid:3000110; rev:1; priority:1;)
PIPEDREAM_INCONTROLLER:
description: "Modular ICS attack framework targeting Schneider/OMRON PLCs and OPC UA"
target: "Multiple PLC vendors (Schneider, OMRON) and OPC UA servers"
network_indicators:
- protocol: "CODESYS"
port: 1217
pattern: "CODESYS runtime exploitation attempts"
- protocol: "OPC UA"
port: 4840
pattern: "OPC UA server enumeration and unauthorized method calls"
- protocol: "Modbus"
port: 502
pattern: "Rapid Modbus write commands to multiple unit IDs"
host_indicators:
- "TAGRUN tool for OPC UA scanning"
- "CODECALL tool for CODESYS exploitation"
- "OMSHELL tool for OMRON PLC interaction"
detection_rule: |
alert tcp any any -> $OT_NETWORK 1217 (
msg:"OT-CRITICAL CODESYS runtime connection - PIPEDREAM indicator";
flow:to_server,established;
sid:3000120; rev:1; priority:1;)Key Concepts
| Term | Definition |
|---|---|
| SCADA | Supervisory Control and Data Acquisition - architecture for remote monitoring and control of industrial processes via RTUs and communication infrastructure |
| IDS/IPS for OT | Intrusion Detection/Prevention Systems designed for industrial protocols, using both signature-based and anomaly-based detection methods |
| Process Anomaly | Deviation in physical process behavior (temperature, pressure, flow) that may indicate cyber manipulation of control systems |
| Man-in-the-Middle (MITM) | Attack intercepting communication between SCADA master and field devices to modify commands or spoof sensor readings |
| Replay Attack | Capturing legitimate SCADA traffic and replaying it to mask malicious changes to the process (used by Stuxnet) |
| Protocol Anomaly | Deviation from expected industrial protocol behavior including unauthorized function codes, unusual polling patterns, or command sequences |
Tools & Systems
- Dragos Platform: OT cybersecurity platform with threat detection powered by Dragos threat intelligence on ICS-targeting activity groups
- Nozomi Networks Guardian: OT/IoT visibility and threat detection using asset intelligence, anomaly detection, and vulnerability assessment
- Claroty xDome: Cyber-physical systems protection with continuous threat monitoring and alert prioritization
- Suricata with ET Open ICS rules: Open-source IDS/IPS with community-maintained rules for industrial protocol detection
- Zeek (Bro) with OT scripts: Network security monitor with protocol analyzers for Modbus, DNP3, and BACnet
Common Scenarios
Scenario: Detecting TRITON-Style Attack on Safety Systems
Context: An OT security monitoring system alerts on unusual TriStation protocol traffic to a Triconex safety controller from an IP address that is not the authorized SIS engineering workstation.
Approach: 1. Immediately verify the source IP of the TriStation traffic - is it the authorized SIS engineering workstation or a compromised host? 2. Check if there is an authorized maintenance activity scheduled for the SIS controllers 3. Capture full packet payload of the TriStation communication for forensic analysis 4. Alert the process safety team - SIS compromise is a safety-critical event 5. If unauthorized, isolate the source host from the network immediately 6. Verify SIS controller logic integrity by comparing running logic against known-good backup 7. Check all engineering workstations in the facility for TRITON indicators (trilog.exe, inject.bin)
Pitfalls: Never assume SIS traffic anomalies are false positives - TRITON demonstrated that sophisticated attackers specifically target safety systems. Do not restart the SIS controller without first verifying firmware and logic integrity. Avoid alerting only the IT SOC; the process safety team must be immediately engaged for any SIS-related incident.
Output Format
SCADA Attack Detection Report
===============================
Detection Time: YYYY-MM-DD HH:MM:SS UTC
Detection Source: [IDS/Anomaly Detector/Process Monitor]
ALERT DETAILS:
Alert ID: [unique identifier]
Severity: Critical/High/Medium/Low
Attack Category: [Protocol Anomaly/Process Manipulation/Unauthorized Access]
MITRE ATT&CK for ICS: [Technique ID and name]
Source: [IP/hostname]
Target: [IP/hostname - device type]
Protocol: [Modbus/DNP3/S7comm/etc]
Detail: [Specific finding description]
BASELINE COMPARISON:
Normal: [Expected behavior]
Observed: [Actual behavior that triggered alert]
Deviation: [How the observed differs from baseline]
RECOMMENDED RESPONSE:
1. [Immediate containment action]
2. [Verification step]
3. [Escalation path]
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. Please do not remove or change
the license header comment from a contributed file except when
necessary.
Copyright 2026 mukul975
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
SCADA Attack Detection — API Reference
Libraries
| Library | Install | Purpose |
|---|---|---|
| pymodbus | pip install pymodbus | Modbus TCP client for PLC interaction |
| requests | pip install requests | SIEM and historian API queries |
Common SCADA Protocols and Ports
| Port | Protocol | Vendor/Use |
|---|---|---|
| 502 | Modbus TCP | Universal PLC communication |
| 102 | S7comm (ISO-TSAP) | Siemens S7 PLCs |
| 44818 | EtherNet/IP CIP | Allen-Bradley / Rockwell |
| 20000 | DNP3 | Power grid, water systems |
| 4840 | OPC-UA | Universal ICS integration |
| 47808 | BACnet | Building automation |
| 34962 | PROFINET RT | Siemens distributed I/O |
Modbus Attack Indicators
| Indicator | Description | Severity |
|---|---|---|
| Broadcast unit ID (0/255) | Access to all devices simultaneously | CRITICAL |
| Write to coils from IT network | Unauthorized process control change | CRITICAL |
| Unusual function codes (8, 17, 43) | Diagnostic/recon commands | HIGH |
| Bulk register reads | Data exfiltration from PLC memory | MEDIUM |
S7comm Connection Request (COTP CR)
| Field | Value | Description |
|---|---|---|
| TPKT version | 0x03 | ISO transport header |
| COTP PDU type | 0xe0 | Connection request |
| Source TSAP | 0x0100 | Client address |
| Destination TSAP | 0x0102 | PLC rack/slot |
MITRE ATT&CK for ICS
| Technique | ID | Description |
|---|---|---|
| Point & Tag Identification | T0861 | Enumerate process data points |
| Unauthorized Command Message | T0855 | Send rogue commands to controller |
| Modify Controller Tasking | T0821 | Change PLC program logic |
| Denial of Service | T0814 | Disrupt SCADA communications |
External References
#!/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()