From f8861419cb732ac7c78fef56d2fe7aae68a3a2d8 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:25:31 +0200 Subject: [PATCH] test(plc): add realistic OpenPLC-style traffic-light sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second demo fixture (public-sample shape) to complement the all-rules pump_station.st: a timed pedestrian-crossing state machine adapted from the OpenPLC traffic-light example, extended with a SCADA/Modbus uplink and a maintenance override. Mostly sound control logic with three planted, field-realistic defects (hardcoded SCADA password, cleartext Modbus master, maintenance mode that drops the pedestrian safety permit). The regression test asserts the scanner surfaces those defects while staying quiet on the guarded duty-cycle division and the JMP-free CASE machine — demonstrating low false positives on real-world-shaped code. Co-Authored-By: Claude Opus 4.8 --- compliance-agent/src/pipeline/plc/mod.rs | 44 ++++++++++++ examples/plc-demo/traffic_light.st | 92 ++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 examples/plc-demo/traffic_light.st diff --git a/compliance-agent/src/pipeline/plc/mod.rs b/compliance-agent/src/pipeline/plc/mod.rs index b23991e..f64b676 100644 --- a/compliance-agent/src/pipeline/plc/mod.rs +++ b/compliance-agent/src/pipeline/plc/mod.rs @@ -153,4 +153,48 @@ mod tests { .count(); assert_eq!(div0, 1, "only the unguarded division should be flagged"); } + + /// The realistic OpenPLC-style traffic-light sample is mostly sound control + /// logic: the scanner must surface its few genuine defects and stay quiet on + /// the timed state machine and the guarded duty-cycle division. + #[test] + fn realistic_sample_flags_only_real_issues() { + let all = analyze_tree(&demo_dir(), "demo-target"); + let tl: Vec<_> = all + .iter() + .filter(|f| { + f.file_path + .as_deref() + .is_some_and(|p| p.ends_with("traffic_light.st")) + }) + .collect(); + assert!(!tl.is_empty(), "traffic_light.st should produce findings"); + + let rules: HashSet<&str> = tl.iter().filter_map(|f| f.rule_id.as_deref()).collect(); + // The three planted defects: hardcoded SCADA password, cleartext Modbus + // master (no auth), and a maintenance mode that drops the PedPermit. + for r in [ + "plc-hardcoded-credential", + "plc-insecure-comm", + "plc-safety-bypass", + ] { + assert!(rules.contains(r), "expected rule {r}; got {rules:?}"); + } + // Modbus/TCP on 502 is also an insecure-protocol port. + assert!(rules.contains("plc-insecure-protocol-port")); + + // Low false positives: the guarded `IF LampCount <> 0` division and the + // JMP-free state machine must not trip anything. + assert_eq!( + tl.iter() + .filter(|f| f.rule_id.as_deref() == Some("plc-division-by-zero")) + .count(), + 0, + "the guarded duty-cycle division must not be flagged" + ); + assert!( + !rules.contains("plc-unstructured-jump"), + "the CASE state machine uses no JMP" + ); + } } diff --git a/examples/plc-demo/traffic_light.st b/examples/plc-demo/traffic_light.st new file mode 100644 index 0000000..98c3ba5 --- /dev/null +++ b/examples/plc-demo/traffic_light.st @@ -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