feat(pipeline): PLC/SPS control-logic security scanner (IEC 61131-3) (#162)
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 3m48s
CI / Deploy Dashboard (push) Successful in 2m51s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2m2s
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 3m48s
CI / Deploy Dashboard (push) Successful in 2m51s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2m2s
This commit was merged in pull request #162.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Demo PLCopen project — conveyor sorter. Deliberately vulnerable. -->
|
||||
<project xmlns="http://www.plcopen.org/xml/tc6_0201">
|
||||
<types>
|
||||
<pous>
|
||||
<pou name="ConveyorCtrl" pouType="program">
|
||||
<interface>
|
||||
<localVars>
|
||||
<variable name="AdminPwd">
|
||||
<type><string/></type>
|
||||
<initialValue><simpleValue value="password"/></initialValue>
|
||||
</variable>
|
||||
<variable name="Belt">
|
||||
<type>
|
||||
<array>
|
||||
<dimension lower="0" upper="3"/>
|
||||
<baseType><INT/></baseType>
|
||||
</array>
|
||||
</type>
|
||||
</variable>
|
||||
</localVars>
|
||||
<inputVars>
|
||||
<variable name="Slot"><type><INT/></type></variable>
|
||||
</inputVars>
|
||||
</interface>
|
||||
<body>
|
||||
<ST>
|
||||
<xhtml xmlns="http://www.w3.org/1999/xhtml">Belt[Slot] := 1;
|
||||
Ftp_Send(HOST := '192.168.1.5', PORT := 21, ENCRYPT := FALSE);
|
||||
</xhtml>
|
||||
</ST>
|
||||
</body>
|
||||
</pou>
|
||||
</pous>
|
||||
</types>
|
||||
</project>
|
||||
@@ -0,0 +1,57 @@
|
||||
(*
|
||||
* Demo PLC program — pump-station control (IEC 61131-3 Structured Text).
|
||||
*
|
||||
* Deliberately vulnerable, for the compliance-scanner PLC control-logic demo.
|
||||
* Each issue below is flagged by pipeline::plc::rules.
|
||||
*)
|
||||
|
||||
FUNCTION_BLOCK PumpStationCtrl
|
||||
VAR_INPUT
|
||||
OperatorCmd : INT; (* HMI command index — untrusted *)
|
||||
FlowSetpoint : REAL;
|
||||
END_VAR
|
||||
VAR_OUTPUT
|
||||
PumpSpeed : REAL;
|
||||
Fault : BOOL;
|
||||
END_VAR
|
||||
VAR
|
||||
HmiPassword : STRING := 'admin123'; (* hardcoded + default credential *)
|
||||
ApiKey : STRING := 'sk_live_9c1f2a'; (* hardcoded secret *)
|
||||
PumpProfiles : ARRAY[0..7] OF REAL;
|
||||
Safety_Enable : BOOL := TRUE;
|
||||
Watchdog_Kick : INT := 1;
|
||||
MeasuredFlow : REAL;
|
||||
ScaleFactor : REAL;
|
||||
i : INT;
|
||||
END_VAR
|
||||
|
||||
(* Operator can index the profile table with an unvalidated command. *)
|
||||
PumpSpeed := PumpProfiles[OperatorCmd];
|
||||
|
||||
(* Divisor is a live process value that can read zero on a stopped line. *)
|
||||
ScaleFactor := FlowSetpoint / MeasuredFlow;
|
||||
|
||||
(* Safety interlock disabled straight from application logic. *)
|
||||
IF OperatorCmd = 99 THEN
|
||||
Safety_Enable := FALSE;
|
||||
Watchdog_Kick := 0;
|
||||
END_IF;
|
||||
|
||||
(* Unauthenticated Modbus/TCP link on the cleartext OT port. *)
|
||||
Modbus_TCP_Connect(IP := '10.10.5.20', PORT := 502, AUTH := FALSE, PASSWORD := 'plc');
|
||||
|
||||
(* Unstructured jump around the fault handler. *)
|
||||
IF MeasuredFlow > 1000.0 THEN
|
||||
JMP trip;
|
||||
END_IF;
|
||||
|
||||
(* A correctly guarded division — must NOT be flagged. *)
|
||||
IF ScaleFactor <> 0.0 THEN
|
||||
PumpSpeed := PumpSpeed / ScaleFactor;
|
||||
END_IF;
|
||||
|
||||
RETURN;
|
||||
trip:
|
||||
Fault := TRUE;
|
||||
PumpSpeed := 0.0;
|
||||
END_FUNCTION_BLOCK
|
||||
@@ -0,0 +1,92 @@
|
||||
(*
|
||||
* Pedestrian-crossing traffic-light controller.
|
||||
*
|
||||
* Structure adapted from the classic OpenPLC "traffic light" example
|
||||
* (github.com/thiagoralves/OpenPLC_v3 examples) — a timed state machine
|
||||
* driving vehicle + pedestrian lamps, extended with a SCADA/Modbus link
|
||||
* and a maintenance override so it reads like a real deployed program.
|
||||
*
|
||||
* The control logic itself is sound; the security-relevant defects are the
|
||||
* kind that slip into field code under deadline: a hardcoded SCADA password,
|
||||
* a cleartext Modbus/TCP master, and a maintenance mode that drops the
|
||||
* pedestrian safety permit. Everything else should stay quiet.
|
||||
*)
|
||||
PROGRAM TrafficLight
|
||||
VAR
|
||||
State : INT := 0; (* 0 GreenVeh, 1 Amber, 2 RedVeh/WalkPed, 3 FlashPed *)
|
||||
Tmr : TON;
|
||||
StateElapsed : TIME;
|
||||
CycleMs : DINT := 0;
|
||||
|
||||
(* Lamp outputs *)
|
||||
VehGreen : BOOL := FALSE;
|
||||
VehAmber : BOOL := FALSE;
|
||||
VehRed : BOOL := FALSE;
|
||||
PedWalk : BOOL := FALSE;
|
||||
PedStop : BOOL := TRUE;
|
||||
|
||||
(* Pedestrian safety permit — must be TRUE before the WALK phase asserts *)
|
||||
PedPermit : BOOL := TRUE;
|
||||
PedButton : BOOL := FALSE;
|
||||
|
||||
(* SCADA / remote monitoring *)
|
||||
ScadaUser : STRING := 'operator';
|
||||
ScadaPassword : STRING := 'Tr@ffic2019'; (* hardcoded SCADA credential *)
|
||||
ModbusReady : BOOL := FALSE;
|
||||
|
||||
(* Maintenance override *)
|
||||
MaintMode : BOOL := FALSE;
|
||||
LampCount : INT := 5;
|
||||
DutyPct : INT;
|
||||
END_VAR
|
||||
|
||||
(* ---- SCADA uplink: publish state to the control room over Modbus/TCP ---- *)
|
||||
IF NOT ModbusReady THEN
|
||||
Modbus_TCP_Master(IP := '10.20.0.5', PORT := 502, AUTH := FALSE, USER := ScadaUser, PASS := ScadaPassword);
|
||||
ModbusReady := TRUE;
|
||||
END_IF;
|
||||
|
||||
(* ---- Duty-cycle for the flashing pedestrian lamp (guarded division) ---- *)
|
||||
IF LampCount <> 0 THEN
|
||||
DutyPct := (CycleMs * 100) / LampCount;
|
||||
END_IF;
|
||||
|
||||
(* ---- Maintenance override: flash amber, hand control to the technician ---- *)
|
||||
IF MaintMode THEN
|
||||
VehGreen := FALSE;
|
||||
VehRed := FALSE;
|
||||
VehAmber := NOT VehAmber;
|
||||
PedPermit := FALSE; (* drops the pedestrian safety permit in code *)
|
||||
PedWalk := FALSE;
|
||||
PedStop := TRUE;
|
||||
ELSE
|
||||
(* ---- Normal timed state machine ---- *)
|
||||
Tmr(IN := TRUE, PT := T#5s);
|
||||
StateElapsed := Tmr.ET;
|
||||
|
||||
CASE State OF
|
||||
0: (* vehicles go, pedestrians stop *)
|
||||
VehGreen := TRUE; VehAmber := FALSE; VehRed := FALSE;
|
||||
PedWalk := FALSE; PedStop := TRUE;
|
||||
IF PedButton AND Tmr.Q THEN
|
||||
State := 1; Tmr(IN := FALSE);
|
||||
END_IF;
|
||||
1: (* amber transition *)
|
||||
VehGreen := FALSE; VehAmber := TRUE;
|
||||
IF Tmr.Q THEN State := 2; Tmr(IN := FALSE); END_IF;
|
||||
2: (* vehicles stop, pedestrians walk — only if permitted *)
|
||||
VehAmber := FALSE; VehRed := TRUE;
|
||||
IF PedPermit THEN
|
||||
PedWalk := TRUE; PedStop := FALSE;
|
||||
END_IF;
|
||||
IF Tmr.Q THEN State := 3; Tmr(IN := FALSE); END_IF;
|
||||
3: (* flashing don't-walk before returning to green *)
|
||||
PedWalk := NOT PedWalk;
|
||||
IF Tmr.Q THEN
|
||||
State := 0; PedButton := FALSE; Tmr(IN := FALSE);
|
||||
END_IF;
|
||||
ELSE
|
||||
State := 0;
|
||||
END_CASE;
|
||||
END_IF;
|
||||
END_PROGRAM
|
||||
Reference in New Issue
Block a user