Skip to content

Commit 15372b1

Browse files
Add test data, demo scripts, and sample reports for portal integration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 81b6c05 commit 15372b1

5 files changed

Lines changed: 3154 additions & 0 deletions

File tree

test_data/gen_pcap.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""Generate a test PCAP file using only Python stdlib (struct module).
2+
No external dependencies needed - writes raw PCAP binary format directly."""
3+
import struct, os, time
4+
5+
def write_pcap_header(f):
6+
# Global header: magic, version 2.4, thiszone=0, sigfigs=0, snaplen=65535, network=1 (Ethernet)
7+
f.write(struct.pack("<IHHiIII", 0xa1b2c3d4, 2, 4, 0, 0, 65535, 1))
8+
9+
def write_packet(f, data, ts):
10+
ts_sec = int(ts)
11+
ts_usec = int((ts - ts_sec) * 1e6)
12+
f.write(struct.pack("<IIII", ts_sec, ts_usec, len(data), len(data)))
13+
f.write(data)
14+
15+
def ip_checksum(header):
16+
if len(header) % 2: header += b'\x00'
17+
s = sum(struct.unpack("!%dH" % (len(header)//2), header))
18+
s = (s >> 16) + (s & 0xffff)
19+
s += s >> 16
20+
return ~s & 0xffff
21+
22+
def make_eth_ip_tcp(src_mac, dst_mac, src_ip, dst_ip, sport, dport, payload):
23+
eth = bytes.fromhex(dst_mac.replace(":","")) + bytes.fromhex(src_mac.replace(":","")) + b'\x08\x00'
24+
ip_hdr = struct.pack("!BBHHHBBH4s4s",
25+
0x45, 0, 20+20+len(payload), 0x1234, 0x4000, 64, 6, 0,
26+
bytes(int(x) for x in src_ip.split(".")),
27+
bytes(int(x) for x in dst_ip.split(".")))
28+
csum = ip_checksum(ip_hdr)
29+
ip_hdr = ip_hdr[:10] + struct.pack("!H", csum) + ip_hdr[12:]
30+
tcp_hdr = struct.pack("!HHIIBBHHH", sport, dport, 1000, 1000, 0x50, 0x18, 65535, 0, 0)
31+
return eth + ip_hdr + tcp_hdr + payload
32+
33+
def make_eth_ip_udp(src_mac, dst_mac, src_ip, dst_ip, sport, dport, payload):
34+
eth = bytes.fromhex(dst_mac.replace(":","")) + bytes.fromhex(src_mac.replace(":","")) + b'\x08\x00'
35+
ip_hdr = struct.pack("!BBHHHBBH4s4s",
36+
0x45, 0, 20+8+len(payload), 0x1234, 0x4000, 64, 17, 0,
37+
bytes(int(x) for x in src_ip.split(".")),
38+
bytes(int(x) for x in dst_ip.split(".")))
39+
csum = ip_checksum(ip_hdr)
40+
ip_hdr = ip_hdr[:10] + struct.pack("!H", csum) + ip_hdr[12:]
41+
udp_hdr = struct.pack("!HHHH", sport, dport, 8+len(payload), 0)
42+
return eth + ip_hdr + udp_hdr + payload
43+
44+
def modbus_req(tid, uid, fc, data=b""):
45+
pdu = bytes([fc]) + data
46+
return struct.pack(">HHH", tid, 0, len(pdu)+1) + bytes([uid]) + pdu
47+
48+
def modbus_resp(tid, uid, fc, data=b""):
49+
pdu = bytes([fc]) + data
50+
return struct.pack(">HHH", tid, 0, len(pdu)+1) + bytes([uid]) + pdu
51+
52+
def modbus_mei_resp(tid, uid):
53+
objs = [(0,b"Schneider Electric"),(1,b"Modicon M580"),(2,b"3.20"),(4,b"M580 ePAC Controller"),(5,b"BMEP585040")]
54+
od = b""
55+
for oid, val in objs:
56+
od += bytes([oid, len(val)]) + val
57+
mei = bytes([0x0E, 0x01, 0x01, 0x00, 0x00, len(objs)]) + od
58+
return modbus_resp(tid, uid, 0x2B, mei)
59+
60+
def s7_cotp_cr():
61+
params = bytes([0xC1,0x02,0x01,0x00, 0xC2,0x02,0x01,0x02, 0xC0,0x01,0x0A])
62+
cotp = bytes([len(params)+6, 0xE0, 0,0, 0,1, 0]) + params
63+
return struct.pack(">BBH", 3, 0, 4+len(cotp)) + cotp
64+
65+
def s7_cotp_cc():
66+
params = bytes([0xC1,0x02,0x01,0x00, 0xC2,0x02,0x01,0x02, 0xC0,0x01,0x0A])
67+
cotp = bytes([len(params)+6, 0xD0, 0,1, 0,1, 0]) + params
68+
return struct.pack(">BBH", 3, 0, 4+len(cotp)) + cotp
69+
70+
def s7_data():
71+
s7 = bytes([0x32,0x01,0,0,0,1,0,14,0,0, 0x04,0x01, 0x12,0x0A,0x10,0x02,0,100,0,1,0x84,0,0,0])
72+
cotp_dt = bytes([2, 0xF0, 0x80])
73+
p = cotp_dt + s7
74+
return struct.pack(">BBH", 3, 0, 4+len(p)) + p
75+
76+
def eip_list_identity():
77+
pname = b"1756-L71/B ControlLogix5571"
78+
ident = struct.pack("<H", 1)
79+
ident += struct.pack(">HH4s8s", 2, 44818, bytes([10,10,1,102]), b"\x00"*8)
80+
ident += struct.pack("<H", 1) # vendor=Rockwell
81+
ident += struct.pack("<H", 0x10) # device type=PLC
82+
ident += struct.pack("<H", 55) # product code
83+
ident += bytes([30, 11]) # revision
84+
ident += struct.pack("<H", 0) # status
85+
ident += struct.pack("<I", 0xDEADBEEF)
86+
ident += bytes([len(pname)]) + pname + bytes([3])
87+
item = struct.pack("<HH", 0x000C, len(ident)) + ident
88+
body = struct.pack("<H", 1) + item
89+
hdr = struct.pack("<HHII", 0x0063, len(body), 0, 0) + b"\x00"*8 + struct.pack("<I", 0)
90+
return hdr + body
91+
92+
def eip_register():
93+
body = struct.pack("<HH", 1, 0)
94+
return struct.pack("<HHII", 0x0065, len(body), 0, 0) + b"\x00"*8 + struct.pack("<I", 0) + body
95+
96+
def dnp3_frame(ctrl, dest, src, app_fc=None):
97+
app = b""
98+
if app_fc is not None:
99+
app = bytes([0xC0, 0xC0, app_fc])
100+
return bytes([0x05, 0x64, 5+len(app), ctrl]) + struct.pack("<HH", dest, src) + app
101+
102+
def iec104_startdt():
103+
return bytes([0x68, 0x04, 0x07, 0x00, 0x00, 0x00])
104+
105+
def iec104_interrog():
106+
asdu = bytes([100, 0x01, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x14])
107+
return bytes([0x68, len(asdu)+4, 0,0,0,0]) + asdu
108+
109+
def opcua_hello():
110+
url = b"opc.tcp://10.10.2.60:4840/OPCUA/SimServer"
111+
body = struct.pack("<IIIII", 0, 65536, 65536, 0, 0) + struct.pack("<I", len(url)) + url
112+
return b"HELF" + struct.pack("<I", 8+len(body)) + body
113+
114+
def opcua_open_none():
115+
sp = b"http://opcfoundation.org/UA/SecurityPolicy#None"
116+
body = struct.pack("<I", len(sp)) + sp + b"\x00"*20
117+
return b"OPNF" + struct.pack("<I", 8+len(body)) + body
118+
119+
def mqtt_connect(cid=b"ot_sensor_01"):
120+
vh = b"\x00\x04MQTT" + bytes([0x04, 0x02]) + struct.pack(">H", 60)
121+
pl = struct.pack(">H", len(cid)) + cid
122+
return bytes([0x10, len(vh)+len(pl)]) + vh + pl
123+
124+
def mqtt_publish(topic=b"ot/plc/data", msg=b'{"temp":85.2}'):
125+
vh = struct.pack(">H", len(topic)) + topic
126+
return bytes([0x30, len(vh)+len(msg)]) + vh + msg
127+
128+
def bacnet_whois():
129+
npdu = bytes([0x01, 0x20, 0xFF, 0xFF, 0x00, 0xFF])
130+
apdu = bytes([0x10, 0x08])
131+
p = npdu + apdu
132+
return bytes([0x81, 0x0B]) + struct.pack(">H", 4+len(p)) + p
133+
134+
def main():
135+
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ot_test_traffic.pcap")
136+
ts = time.time()
137+
M = "00:1A:2B:00:01:10"; EW = "00:1A:2B:00:01:20"
138+
P1 = "00:80:F4:00:01:64"; P2 = "00:0E:8C:00:01:65"; P3 = "00:00:BC:00:01:66"
139+
DM = "00:1A:2B:00:02:0A"; DR = "00:1A:2B:00:02:32"; IR = "00:1A:2B:00:02:33"
140+
OU = "00:1A:2B:00:02:3C"; MQ = "00:1A:2B:00:03:0A"; BA = "00:1A:2B:00:03:14"
141+
142+
with open(out, "wb") as f:
143+
write_pcap_header(f)
144+
# Modbus read requests + responses
145+
for i in range(5):
146+
write_packet(f, make_eth_ip_tcp(M,P1,"10.10.1.10","10.10.1.100",50000+i,502,
147+
modbus_req(i+1,1,0x03,struct.pack(">HH",0,10))), ts+i*0.1)
148+
write_packet(f, make_eth_ip_tcp(P1,M,"10.10.1.100","10.10.1.10",502,50000+i,
149+
modbus_resp(i+1,1,0x03,bytes([20])+b"\x00\x01"*10)), ts+i*0.1+0.05)
150+
# Modbus write coils
151+
for i in range(3):
152+
write_packet(f, make_eth_ip_tcp(M,P1,"10.10.1.10","10.10.1.100",51000+i,502,
153+
modbus_req(10+i,1,0x05,struct.pack(">HH",i,0xFF00))), ts+1+i*0.1)
154+
# Modbus MEI
155+
write_packet(f, make_eth_ip_tcp(P1,M,"10.10.1.100","10.10.1.10",502,50010,
156+
modbus_mei_resp(20,1)), ts+2)
157+
# S7comm CR/CC/Data
158+
write_packet(f, make_eth_ip_tcp(EW,P2,"10.10.1.20","10.10.1.101",52000,102,s7_cotp_cr()), ts+3)
159+
write_packet(f, make_eth_ip_tcp(P2,EW,"10.10.1.101","10.10.1.20",102,52000,s7_cotp_cc()), ts+3.1)
160+
for i in range(5):
161+
write_packet(f, make_eth_ip_tcp(EW,P2,"10.10.1.20","10.10.1.101",52000,102,s7_data()), ts+3.2+i*0.1)
162+
# EtherNet/IP
163+
write_packet(f, make_eth_ip_tcp(M,P3,"10.10.1.10","10.10.1.102",53000,44818,eip_register()), ts+5)
164+
for i in range(3):
165+
write_packet(f, make_eth_ip_tcp(P3,M,"10.10.1.102","10.10.1.10",44818,53000,eip_list_identity()), ts+5.1+i*0.2)
166+
# DNP3 reads
167+
for i in range(3):
168+
write_packet(f, make_eth_ip_tcp(DM,DR,"10.10.2.10","10.10.2.50",54000,20000,
169+
dnp3_frame(0xC4,10,1,0x01)), ts+7+i*0.2)
170+
write_packet(f, make_eth_ip_tcp(DR,DM,"10.10.2.50","10.10.2.10",20000,54000,
171+
dnp3_frame(0x44,1,10,0x81)), ts+7.1+i*0.2)
172+
# DNP3 Direct Operate (bypasses SBO)
173+
for i in range(2):
174+
write_packet(f, make_eth_ip_tcp(DM,DR,"10.10.2.10","10.10.2.50",54001,20000,
175+
dnp3_frame(0xC4,10,1,0x05)), ts+8+i*0.2)
176+
# DNP3 Cold Restart
177+
write_packet(f, make_eth_ip_tcp(DM,DR,"10.10.2.10","10.10.2.50",54002,20000,
178+
dnp3_frame(0xC4,10,1,0x0D)), ts+9)
179+
# DNP3 File Open
180+
write_packet(f, make_eth_ip_tcp(DM,DR,"10.10.2.10","10.10.2.50",54003,20000,
181+
dnp3_frame(0xC4,10,1,0x19)), ts+9.5)
182+
# DNP3 over UDP
183+
write_packet(f, make_eth_ip_udp(DM,DR,"10.10.2.10","10.10.2.50",54010,20000,
184+
dnp3_frame(0xC4,10,1,0x01)), ts+10)
185+
# IEC-104
186+
write_packet(f, make_eth_ip_tcp(M,IR,"10.10.1.10","10.10.2.51",55000,2404,iec104_startdt()), ts+11)
187+
for i in range(3):
188+
write_packet(f, make_eth_ip_tcp(M,IR,"10.10.1.10","10.10.2.51",55000,2404,iec104_interrog()), ts+11.1+i*0.1)
189+
# OPC-UA
190+
write_packet(f, make_eth_ip_tcp(EW,OU,"10.10.1.20","10.10.2.60",56000,4840,opcua_hello()), ts+13)
191+
for i in range(3):
192+
write_packet(f, make_eth_ip_tcp(EW,OU,"10.10.1.20","10.10.2.60",56000,4840,opcua_open_none()), ts+13.1+i*0.2)
193+
# MQTT (no TLS, no auth)
194+
write_packet(f, make_eth_ip_tcp(EW,MQ,"10.10.1.20","10.10.3.10",57000,1883,mqtt_connect()), ts+15)
195+
for i in range(5):
196+
write_packet(f, make_eth_ip_tcp(EW,MQ,"10.10.1.20","10.10.3.10",57000,1883,
197+
mqtt_publish(b"ot/plc/telemetry",f'{{"s":{i},"v":{65+i*2.5}}}'.encode())), ts+15.1+i*0.5)
198+
# BACnet/IP
199+
for i in range(3):
200+
write_packet(f, make_eth_ip_udp(M,BA,"10.10.1.10","10.10.3.20",47808,47808,bacnet_whois()), ts+17+i*0.3)
201+
# Cross-zone: HTTP from PLC to external
202+
write_packet(f, make_eth_ip_tcp(P1,"00:1A:2B:FF:FF:01","10.10.1.100","192.168.1.50",60000,80,
203+
b"GET /update HTTP/1.1\r\nHost: vendor.com\r\n\r\n"), ts+19)
204+
# Telnet to OT device
205+
write_packet(f, make_eth_ip_tcp(EW,P1,"10.10.1.20","10.10.1.100",60100,23,
206+
b"\xff\xfb\x01\xff\xfb\x03"), ts+19.5)
207+
# Extra Modbus from different master (EW -> PLC)
208+
for i in range(3):
209+
write_packet(f, make_eth_ip_tcp(EW,P1,"10.10.1.20","10.10.1.100",52100+i,502,
210+
modbus_req(30+i,1,0x03,struct.pack(">HH",100,10))), ts+20+i*0.1)
211+
212+
print(f"Generated: {out}")
213+
print(f"Size: {os.path.getsize(out)} bytes")
214+
215+
if __name__ == "__main__":
216+
main()

0 commit comments

Comments
 (0)