├── epanetCPA ├── epanet2.dll ├── epanet2.lib ├── PLC.m ├── SCADA.m ├── Cyberlink.m ├── LICENSE ├── Controller.m ├── AttackOnActuator.m ├── CyberPhysicalAttack.m ├── AttackOnControl.m ├── EpanetControl.m ├── epanet2.h ├── AttackOnSensor.m ├── EpanetCPAMap.m ├── AttackOnCommunication.m ├── EpanetCPA.m ├── EpanetHelper.m └── EpanetCPASimulation.m ├── .gitignore ├── scenarios ├── minitown │ ├── minitown_no_attacks.cpa │ └── minitown_attack.cpa └── ctown │ ├── no_attacks.cpa │ ├── scenario04.cpa │ ├── scenario01.cpa │ ├── scenario02.cpa │ ├── scenario03.cpa │ ├── scenario05.cpa │ └── attacks.cpa ├── main.m ├── ctown_patterns.csv ├── README.md ├── minitown_patterns.csv └── minitown_map.inp /epanetCPA/epanet2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtaormina/epanetCPA/HEAD/epanetCPA/epanet2.dll -------------------------------------------------------------------------------- /epanetCPA/epanet2.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtaormina/epanetCPA/HEAD/epanetCPA/epanet2.lib -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all .csv files 2 | *.csv 3 | 4 | # but the patterns 5 | !ctown_patterns.csv 6 | !minitown_patterns.csv 7 | 8 | # ignore all .asv files 9 | *.asv 10 | 11 | # ignore all .svg files (output of jupyter notebook) 12 | *.svg 13 | 14 | # ignore all temp files from epanetCPA 15 | *.inpx 16 | *.$$$ -------------------------------------------------------------------------------- /epanetCPA/PLC.m: -------------------------------------------------------------------------------- 1 | classdef PLC < Controller 2 | % Class implementing a PLC. 3 | 4 | methods 5 | 6 | % constructor 7 | function self = PLC(PLCinfo, controls) 8 | self@Controller(PLCinfo, controls); 9 | end 10 | 11 | % end of public methods 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /scenarios/minitown/minitown_no_attacks.cpa: -------------------------------------------------------------------------------- 1 | [CYBERNODES] 2 | ;Name, Sensors, Actuators 3 | PLC1, P_TANK, 4 | PLC2, S_PUMP1 S_PUMP2, PUMP1 PUMP2 5 | [CYBERLINKS] 6 | ;Source, Destination, Sensors 7 | PLC1, PLC2, P_TANK 8 | PLC1, SCADA, P_TANK 9 | [CYBERATTACKS] 10 | ; Type, Target, Init_cond, End_cond, Arguments 11 | [CYBEROPTIONS] 12 | verbosity, 1 13 | what_to_store, TANK PUMP1 PUMP2, PRESSURE, FLOW STATUS 14 | initial_conditions, 4 15 | patterns_file, ./minitown_patterns.csv 16 | -------------------------------------------------------------------------------- /scenarios/minitown/minitown_attack.cpa: -------------------------------------------------------------------------------- 1 | [CYBERNODES] 2 | ;Name, Sensors, Actuators 3 | PLC1, P_TANK, 4 | PLC2, S_PUMP1 S_PUMP2, PUMP1 PUMP2 5 | [CYBERLINKS] 6 | ;Source, Destination, Sensors 7 | PLC1, PLC2, P_TANK 8 | PLC1, SCADA, P_TANK 9 | [CYBERATTACKS] 10 | ; Type, Target, Init_cond, End_cond, Arguments 11 | Communication, PLC1-P_TANK-PLC2, TIME==90, TIME==140, DoS 12 | Communication, PLC1-P_TANK-SCADA, TIME==70, TIME==-1, replay 50 0.05 5 0 13 | [CYBEROPTIONS] 14 | verbosity, 1 15 | what_to_store, TANK PUMP1 PUMP2, PRESSURE, FLOW STATUS 16 | initial_conditions, 4 17 | patterns_file, ./minitown_patterns.csv -------------------------------------------------------------------------------- /epanetCPA/SCADA.m: -------------------------------------------------------------------------------- 1 | classdef SCADA < Controller 2 | % Class implementing SCADA system. 3 | 4 | methods 5 | 6 | % constructor 7 | function self = SCADA(SCADAinfo, controls, allSensors) 8 | if isempty(SCADAinfo) 9 | % populate SCADA if not specified in .cpa file 10 | SCADAinfo.name = 'SCADA'; 11 | SCADAinfo.sensors = {}; 12 | SCADAinfo.actuators = {}; 13 | end 14 | 15 | % invoke superclass constructors 16 | self@Controller(SCADAinfo, controls); 17 | 18 | % % add all remaining sensors (i.e., SCADA always sees all) 19 | % self.sensorsIn = setdiff(allSensors,self.sensors); 20 | end 21 | 22 | % end of public methods 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | %% Examples of cyber-physical attacks on WADI testbed 2 | 3 | %% INITIALIZATION 4 | clear; clc; 5 | 6 | % add path for epanetCPA toolbox 7 | addpath('.\epanetCPA\') 8 | 9 | % add location of the map and cpa files 10 | inpFilePath = 'minitown_map.inp'; 11 | 12 | % Define scenario 13 | scenarioFolder = './scenarios/minitown/'; 14 | cpaFilePath = 'minitown_attack.cpa'; 15 | noAttackCpaFile = 'minitown_no_attacks.cpa'; 16 | exp_name = cpaFilePath(1:strfind(cpaFilePath,'.cpa')-1); 17 | 18 | % Similation without attacks (used for comparison). 19 | simul = EpanetCPA(inpFilePath, [scenarioFolder, noAttackCpaFile]); % 20 | simul = simul.run(); 21 | simul.outputResults('minitown_no_attacks'); 22 | 23 | % Similation with attacks 24 | simul = EpanetCPA(inpFilePath, [scenarioFolder,cpaFilePath]); % 25 | simul = simul.run(); 26 | simul.outputResults(exp_name); 27 | -------------------------------------------------------------------------------- /epanetCPA/Cyberlink.m: -------------------------------------------------------------------------------- 1 | classdef Cyberlink 2 | % Class implementing a cyberlink 3 | 4 | properties 5 | name % name (same naming used for attack targets) 6 | sender % sender 7 | receiver % receiver 8 | signal % signal (variable) being transmitted 9 | end 10 | 11 | methods 12 | 13 | % constructor 14 | function self = Cyberlink(cyberlinkInfo) 15 | % creates controller instance from info 16 | self.name = sprintf('%s-%s-%s',... 17 | cyberlinkInfo.sender,cyberlinkInfo.signal,cyberlinkInfo.receiver); 18 | self.sender = cyberlinkInfo.sender; 19 | self.receiver = cyberlinkInfo.receiver; 20 | self.signal = cyberlinkInfo.signal; 21 | end 22 | 23 | % end of public methods 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /scenarios/ctown/no_attacks.cpa: -------------------------------------------------------------------------------- 1 | [CYBERNODES] 2 | ; Name, Sensors, Actuators 3 | PLC1, F_PU1 F_PU2 F_PU3 P_J280 P_J269, PU1 PU2 PU3 4 | PLC2, P_T1, 5 | PLC3, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422, PU4 PU5 PU6 PU7 V2 6 | PLC4, P_T3, 7 | PLC5, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317, PU8 PU9 PU10 PU11 8 | PLC6, P_T4, 9 | PLC7, P_T5, 10 | PLC8, P_T6, 11 | PLC9, P_T7, 12 | ; SCADA 13 | 14 | 15 | [CYBERLINKS] 16 | ; Source,Destination,Sensors 17 | PLC1, SCADA, F_PU1 F_PU2 F_PU3 P_J280 P_J269 18 | PLC2, SCADA, P_T1 19 | PLC2, PLC1, P_T1 20 | PLC3, SCADA, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422 21 | PLC4, SCADA, P_T3 22 | PLC4, PLC3, P_T3 23 | PLC5, SCADA, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317 24 | PLC6, SCADA, P_T4 25 | PLC6, PLC3, P_T4 26 | PLC7, SCADA, P_T5 27 | PLC7, PLC5, P_T5 28 | PLC8, SCADA, P_T6 29 | PLC9, SCADA, P_T7 30 | PLC9, PLC5, P_T7 31 | 32 | 33 | [CYBERATTACKS] 34 | ; Type,Target,Init_cond,End_cond,Arguments 35 | 36 | 37 | [CYBEROPTIONS] 38 | verbosity, 1 39 | initial_conditions, 4 3.75 3.25 3.75 3.5 3.5 3 40 | patterns_file, ./ctown_patterns.csv -------------------------------------------------------------------------------- /epanetCPA/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016-2018 Riccardo Taormina, 4 | Singapore University of Technology and Design. 5 | email: riccardo_taormina@sutd.edu.sg, riccardo.taormina@gmail.com 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. -------------------------------------------------------------------------------- /scenarios/ctown/scenario04.cpa: -------------------------------------------------------------------------------- 1 | [CYBERNODES] 2 | ; Name, Sensors, Actuators 3 | PLC1, F_PU1 F_PU2 F_PU3 P_J280 P_J269, PU1 PU2 PU3 4 | PLC2, P_T1, 5 | PLC3, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422, PU4 PU5 PU6 PU7 V2 6 | PLC4, P_T3, 7 | PLC5, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317, PU8 PU9 PU10 PU11 8 | PLC6, P_T4, 9 | PLC7, P_T5, 10 | PLC8, P_T6, 11 | PLC9, P_T7, 12 | ; SCADA 13 | 14 | 15 | [CYBERLINKS] 16 | ; Source,Destination,Sensors 17 | PLC1, SCADA, F_PU1 F_PU2 F_PU3 P_J280 P_J269 18 | PLC2, SCADA, P_T1 19 | PLC2, PLC1, P_T1 20 | PLC3, SCADA, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422 21 | PLC4, SCADA, P_T3 22 | PLC4, PLC3, P_T3 23 | PLC5, SCADA, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317 24 | PLC6, SCADA, P_T4 25 | PLC6, PLC3, P_T4 26 | PLC7, SCADA, P_T5 27 | PLC7, PLC5, P_T5 28 | PLC8, SCADA, P_T6 29 | PLC9, SCADA, P_T7 30 | PLC9, PLC5, P_T7 31 | 32 | [CYBERATTACKS] 33 | ; Type Target Init_cond End_cond Arguments 34 | ; Denial-of-service of the connection link between PLC2 and PLC1. PLC1 fails to receive updated readings water level data for tank T1 and keeps the pumps (PU1,PU2) ON. This causes a surge in the tank T1. 35 | Communication, PLC2-P_T1-PLC1, (TIME>50)&&(PU2>0), TIME==-1, DoS 36 | 37 | [CYBEROPTIONS] 38 | verbosity, 1 39 | what_to_store, T1 T2 T3 T4 T5 T6 T7 PU1 PU2 PU3 J269 J280, PRESSURE, FLOW 40 | initial_conditions, 4 3.75 3.25 3.75 3.5 3.5 3 41 | patterns_file, ./ctown_patterns.csv -------------------------------------------------------------------------------- /scenarios/ctown/scenario01.cpa: -------------------------------------------------------------------------------- 1 | [CYBERNODES] 2 | ; Name, Sensors, Actuators 3 | PLC1, F_PU1 F_PU2 F_PU3 P_J280 P_J269, PU1 PU2 PU3 4 | PLC2, P_T1, 5 | PLC3, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422, PU4 PU5 PU6 PU7 V2 6 | PLC4, P_T3, 7 | PLC5, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317, PU8 PU9 PU10 PU11 8 | PLC6, P_T4, 9 | PLC7, P_T5, 10 | PLC8, P_T6, 11 | PLC9, P_T7, 12 | ; SCADA 13 | 14 | 15 | [CYBERLINKS] 16 | ; Source,Destination,Sensors 17 | PLC1, SCADA, F_PU1 F_PU2 F_PU3 P_J280 P_J269 18 | PLC2, SCADA, P_T1 19 | PLC2, PLC1, P_T1 20 | PLC3, SCADA, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422 21 | PLC4, SCADA, P_T3 22 | PLC4, PLC3, P_T3 23 | PLC5, SCADA, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317 24 | PLC6, SCADA, P_T4 25 | PLC6, PLC3, P_T4 26 | PLC7, SCADA, P_T5 27 | PLC7, PLC5, P_T5 28 | PLC8, SCADA, P_T6 29 | PLC9, SCADA, P_T7 30 | PLC9, PLC5, P_T7 31 | 32 | 33 | [CYBERATTACKS] 34 | ; Type,Target,Init_cond,End_cond,Arguments 35 | ; Attack on communication link between T2 water level sensor and PLC3. A constant (HIGH) value of 5.6 meters ; is injected, leading PLC3 to close valve V2. Tank T2 empties and network is disconnected. 36 | Communication, NULL-P_T2-PLC3, TIME==10, TIME==20, constant 5.6 37 | 38 | [CYBEROPTIONS] 39 | verbosity, 1 40 | what_to_store, T1 T2 T3 T4 T5 T6 T7 PU1 PU2 PU3 J269 J280, PRESSURE, FLOW 41 | initial_conditions, 4 3.75 3.25 3.75 3.5 3.5 3 42 | patterns_file, ./ctown_patterns.csv 43 | -------------------------------------------------------------------------------- /scenarios/ctown/scenario02.cpa: -------------------------------------------------------------------------------- 1 | [CYBERNODES] 2 | ; Name, Sensors, Actuators 3 | PLC1, F_PU1 F_PU2 F_PU3 P_J280 P_J269, PU1 PU2 PU3 4 | PLC2, P_T1, 5 | PLC3, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422, PU4 PU5 PU6 PU7 V2 6 | PLC4, P_T3, 7 | PLC5, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317, PU8 PU9 PU10 PU11 8 | PLC6, P_T4, 9 | PLC7, P_T5, 10 | PLC8, P_T6, 11 | PLC9, P_T7, 12 | ; SCADA 13 | 14 | 15 | [CYBERLINKS] 16 | ; Source,Destination,Sensors 17 | PLC1, SCADA, F_PU1 F_PU2 F_PU3 P_J280 P_J269 18 | PLC2, SCADA, P_T1 19 | PLC2, PLC1, P_T1 20 | PLC3, SCADA, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422 21 | PLC4, SCADA, P_T3 22 | PLC4, PLC3, P_T3 23 | PLC5, SCADA, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317 24 | PLC6, SCADA, P_T4 25 | PLC6, PLC3, P_T4 26 | PLC7, SCADA, P_T5 27 | PLC7, PLC5, P_T5 28 | PLC8, SCADA, P_T6 29 | PLC9, SCADA, P_T7 30 | PLC9, PLC5, P_T7 31 | 32 | 33 | [CYBERATTACKS] 34 | ; Type,Target,Init_cond,End_cond,Arguments 35 | ; Attack on communication link between T2 water level sensor and PLC3. A constant (HIGH) value of 5.6 meters ; is injected, leading PLC3 to close valve V2. Tank T2 empties and network is disconnected. 36 | Communication, NULL-P_T2-PLC3, TIME==10, TIME==20, constant 5.6 37 | 38 | [CYBEROPTIONS] 39 | verbosity, 1 40 | what_to_store, T1 T2 T3 T4 T5 T6 T7 PU1 PU2 PU3 J269 J280, PRESSURE, FLOW 41 | initial_conditions, 4 3.75 3.25 3.75 3.5 3.5 3 42 | patterns_file, ./ctown_patterns.csv 43 | pda_options, 0.5 0 20 Wagner 44 | -------------------------------------------------------------------------------- /scenarios/ctown/scenario03.cpa: -------------------------------------------------------------------------------- 1 | [CYBERNODES] 2 | ; Name, Sensors, Actuators 3 | PLC1, F_PU1 F_PU2 F_PU3 P_J280 P_J269, PU1 PU2 PU3 4 | PLC2, P_T1, 5 | PLC3, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422, PU4 PU5 PU6 PU7 V2 6 | PLC4, P_T3, 7 | PLC5, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317, PU8 PU9 PU10 PU11 8 | PLC6, P_T4, 9 | PLC7, P_T5, 10 | PLC8, P_T6, 11 | PLC9, P_T7, 12 | ; SCADA 13 | 14 | 15 | [CYBERLINKS] 16 | ; Source,Destination,Sensors 17 | PLC1, SCADA, F_PU1 F_PU2 F_PU3 P_J280 P_J269 18 | PLC2, SCADA, P_T1 19 | PLC2, PLC1, P_T1 20 | PLC3, SCADA, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422 21 | PLC4, SCADA, P_T3 22 | PLC4, PLC3, P_T3 23 | PLC5, SCADA, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317 24 | PLC6, SCADA, P_T4 25 | PLC6, PLC3, P_T4 26 | PLC7, SCADA, P_T5 27 | PLC7, PLC5, P_T5 28 | PLC8, SCADA, P_T6 29 | PLC9, SCADA, P_T7 30 | PLC9, PLC5, P_T7 31 | 32 | 33 | [CYBERATTACKS] 34 | ; Type Target Init_cond End_cond Arguments 35 | ; The attacker modifies the control logic of PLC5 so that some of the controlled pumps (PU10, PU11) switch on/off intermittently. 36 | Control, CTRL17n, TIME==30, TIME==35, 3 37 | Control, CTRL18n, TIME==30, TIME==35, 3.1 38 | Control, CTRL19n, TIME==30, TIME==35, 3 39 | Control, CTRL20n, TIME==30, TIME==35, 3.1 40 | 41 | [CYBEROPTIONS] 42 | verbosity, 1 43 | what_to_store, T1 T2 T3 T4 T5 T6 T7 PU10 PU11 J307 J317, PRESSURE, FLOW 44 | initial_conditions, 4 3.75 3.25 3.75 3.5 3.5 3 45 | patterns_file, ./ctown_patterns.csv 46 | -------------------------------------------------------------------------------- /scenarios/ctown/scenario05.cpa: -------------------------------------------------------------------------------- 1 | [CYBERNODES] 2 | ; Name, Sensors, Actuators 3 | PLC1, F_PU1 F_PU2 F_PU3 P_J280 P_J269, PU1 PU2 PU3 4 | PLC2, P_T1, 5 | PLC3, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422, PU4 PU5 PU6 PU7 V2 6 | PLC4, P_T3, 7 | PLC5, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317, PU8 PU9 PU10 PU11 8 | PLC6, P_T4, 9 | PLC7, P_T5, 10 | PLC8, P_T6, 11 | PLC9, P_T7, 12 | ; SCADA 13 | 14 | 15 | [CYBERLINKS] 16 | ; Source,Destination,Sensors 17 | PLC1, SCADA, F_PU1 F_PU2 F_PU3 P_J280 P_J269 18 | PLC2, SCADA, P_T1 19 | PLC2, PLC1, P_T1 20 | PLC3, SCADA, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422 21 | PLC4, SCADA, P_T3 22 | PLC4, PLC3, P_T3 23 | PLC5, SCADA, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317 24 | PLC6, SCADA, P_T4 25 | PLC6, PLC3, P_T4 26 | PLC7, SCADA, P_T5 27 | PLC7, PLC5, P_T5 28 | PLC8, SCADA, P_T6 29 | PLC9, SCADA, P_T7 30 | PLC9, PLC5, P_T7 31 | 32 | 33 | [CYBERATTACKS] 34 | ; Type Target Init_cond End_cond Arguments 35 | ; Denial-of-service of the connection link between PLC2 and PLC1. PLC1 fails to receive updated readings water level data for tank T1 and keeps the pumps (PU1,PU2) ON. This causes a surge in the tank T1. 36 | ; Attack to communication link from PLC2 to SCADA used to conceal the attack. 37 | Communication, PLC2-P_T1-PLC1, (TIME>50)&&(PU2>0), TIME==-1, DoS 38 | Communication, PLC2-P_T1-SCADA, TIME==58, TIME==-1, replay 50 0.05 7 0 39 | 40 | 41 | [CYBEROPTIONS] 42 | verbosity, 1 43 | what_to_store, T1 T2 T3 T4 T5 T6 T7 PU1 PU2 PU3 J269 J280, PRESSURE, FLOW 44 | initial_conditions, 4 3.75 3.25 3.75 3.5 3.5 3 45 | patterns_file, ./ctown_patterns.csv -------------------------------------------------------------------------------- /scenarios/ctown/attacks.cpa: -------------------------------------------------------------------------------- 1 | [CYBERNODES] 2 | ; Name, Sensors, Actuators 3 | PLC1, F_PU1 F_PU2 F_PU3 P_J280 P_J269, PU1 PU2 PU3 4 | PLC2, P_T1, 5 | PLC3, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422, PU4 PU5 PU6 PU7 V2 6 | PLC4, P_T3, 7 | PLC5, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317, PU8 PU9 PU10 PU11 8 | PLC6, P_T4, 9 | PLC7, P_T5, 10 | PLC8, P_T6, 11 | PLC9, P_T7, 12 | ; SCADA 13 | 14 | 15 | [CYBERLINKS] 16 | ; Source,Destination,Sensors 17 | PLC1, SCADA, F_PU1 F_PU2 F_PU3 P_J280 P_J269 18 | PLC2, SCADA, P_T1 19 | PLC2, PLC1, P_T1 20 | PLC3, SCADA, P_T2 F_PU4 F_PU5 F_PU6 F_PU7 F_V2 P_J300 P_J256 P_J289 P_J415 P_J14 P_J422 21 | PLC4, SCADA, P_T3 22 | PLC4, PLC3, P_T3 23 | PLC5, SCADA, F_PU8 F_PU9 F_PU10 F_PU11 P_J302 P_J306 P_J307 P_J317 24 | PLC6, SCADA, P_T4 25 | PLC6, PLC3, P_T4 26 | PLC7, SCADA, P_T5 27 | PLC7, PLC5, P_T5 28 | PLC8, SCADA, P_T6 29 | PLC9, SCADA, P_T7 30 | PLC9, PLC5, P_T7 31 | 32 | 33 | [CYBERATTACKS] 34 | ; Type,Target,Init_cond,End_cond,Arguments 35 | ;Control, CTRL17n, TIME==30, TIME==45, 1 36 | ;Control, CTRL18n, TIME==30, TIME==45, 1.5 37 | ;Control, CTRL19n, TIME==30, TIME==45, 1 38 | ;Control, CTRL20n, TIME==30, TIME==45, 1.5 39 | ;Communication, PLC9-P_T7-SCADA, TIME==25, TIME==-1, replay 24 0.1 6.5 0 40 | 41 | Communication, PLC2-P_T1-PLC1, TIME==10, TIME==30, constant 0.5 42 | Communication, PLC2-P_T1-SCADA, TIME==10, TIME==20, offset -0.5 43 | Communication, PLC2-P_T1-SCADA, TIME==20, TIME==25, offset -1 44 | Communication, PLC2-P_T1-SCADA, TIME==25, TIME==30, offset -2 45 | 46 | 47 | [CYBEROPTIONS] 48 | verbosity, 1 49 | initial_conditions, 4 3.75 3.25 3.75 3.5 3.5 3 50 | what_to_store, T1 T2 T3 T4 T5 T6 T7 PU1 PU2 PU3 J269 J280, PRESSURE, FLOW 51 | patterns_file, ./ctown_patterns.csv -------------------------------------------------------------------------------- /epanetCPA/Controller.m: -------------------------------------------------------------------------------- 1 | classdef Controller < matlab.mixin.Heterogeneous 2 | % Class implementing a controller (PLC, SCADA...) 3 | 4 | properties 5 | name % controller identifier 6 | sensors % list of sensors connected to controller 7 | actuators % list of actuators controlled by controller 8 | sensorsIn % list of sensors used to control the actuators 9 | % but sent by other controllers. 10 | controlsID % list of controls IDs of this controller 11 | % TO DO: see whether transfer all controls in this class rather 12 | % than keep them in EpanetCPAMap 13 | end 14 | 15 | methods 16 | 17 | % constructor 18 | function self = Controller(controllerInfo, controls) 19 | % creates controller instance from info 20 | self.name = controllerInfo.name; 21 | self.sensors = controllerInfo.sensors; 22 | self.actuators = controllerInfo.actuators; 23 | 24 | % get sensorsIn by reading controls 25 | self = self.getControls(controls); 26 | end 27 | 28 | % end of public methods 29 | 30 | function self = addSensorIn(self, sensor) 31 | self.sensorsIn = cat(2, self.sensorsIn, sensor); 32 | end 33 | 34 | end 35 | 36 | 37 | 38 | % private methods 39 | methods (Sealed) 40 | 41 | function self = getControls(self, controls) 42 | % get controller controls from Map list 43 | self.sensorsIn = {}; self.controlsID = []; 44 | for i = 1 : numel(controls) 45 | thisActuator = EpanetHelper.getComponentId(controls(i).lIndex, 0); 46 | if ismember(thisActuator, self.actuators) 47 | % retrieve control ID 48 | self.controlsID = cat(1, self.controlsID, i); 49 | 50 | % OVERRIDE DUE TO CYBER LINKS 51 | % % see if node is read by controller or reading is from another controller 52 | % thisSensor = EpanetHelper.getComponentId(controls(i).nIndex, 1); 53 | % if ~ismember(thisSensor, self.sensors) 54 | % % is coming from another controller, add to sensorsIn 55 | % % (add P_ since we have water levels and pressures) 56 | % self.sensorsIn = cat(2, self.sensorsIn, ['P_',thisSensor]); 57 | % end 58 | end 59 | end 60 | 61 | % remove redundant sensorsIn 62 | if ~isempty(self.sensorsIn) 63 | self.sensorsIn = unique(self.sensorsIn); 64 | end 65 | end 66 | 67 | % end of private methods 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /ctown_patterns.csv: -------------------------------------------------------------------------------- 1 | 0.64547,0.60274,0.61929,0.61245,0.56299 2 | 0.44347,0.47292,0.43708,0.45158,0.44302 3 | 0.4425,0.44308,0.42327,0.42198,0.46155 4 | 0.34758,0.32864,0.32152,0.38931,0.37059 5 | 0.31589,0.31536,0.32141,0.31101,0.31773 6 | 0.33749,0.33343,0.31454,0.34811,0.3393 7 | 0.43299,0.42065,0.39917,0.40224,0.36198 8 | 0.4801,0.49792,0.45553,0.45911,0.49431 9 | 0.56584,0.59559,0.49898,0.55928,0.51552 10 | 0.59249,0.61499,0.57711,0.61643,0.60791 11 | 0.70652,0.69402,0.69861,0.70565,0.74827 12 | 0.74879,0.73208,0.71882,0.70898,0.74947 13 | 0.7119,0.73522,0.7131,0.72976,0.71806 14 | 0.75565,0.74576,0.71203,0.67857,0.73387 15 | 0.7138,0.69581,0.74009,0.68557,0.6873 16 | 0.76467,0.75173,0.72035,0.762,0.77466 17 | 0.81773,0.82043,0.75102,0.87607,0.84616 18 | 0.83375,0.8239,0.80008,0.78232,0.79242 19 | 0.82886,0.84262,0.78425,0.64776,0.93914 20 | 0.75209,0.7347,0.80279,0.79161,0.75791 21 | 0.8063,0.79776,0.75556,0.73287,0.71694 22 | 0.84752,0.7608,0.74749,0.75716,0.74568 23 | 0.73914,0.8243,0.67938,0.65615,0.7664 24 | 0.68193,0.6823,0.61584,0.76211,0.63072 25 | 0.65836,0.76188,0.71781,0.66395,0.67996 26 | 0.54184,0.50431,0.60891,0.63489,0.56918 27 | 0.46248,0.44092,0.54847,0.47403,0.52975 28 | 0.36822,0.41422,0.43288,0.40342,0.42572 29 | 0.48112,0.42155,0.30808,0.36848,0.2196 30 | 0.41283,0.37076,0.45252,0.39347,0.37873 31 | 0.38684,0.45058,0.47042,0.35745,0.56106 32 | 0.6503,0.44272,0.5551,0.65553,0.58176 33 | 0.60786,0.73103,0.55151,0.79591,0.68188 34 | 0.6998,0.70938,0.63254,0.80179,0.67825 35 | 0.64465,0.78557,0.65626,0.64413,0.70402 36 | 0.61808,0.64877,0.56971,0.73698,0.7628 37 | 0.60108,0.77114,0.48076,0.51645,0.53911 38 | 0.57316,0.52298,0.7186,0.42686,0.61414 39 | 0.64467,0.65168,0.60274,0.74161,0.63635 40 | 0.7283,0.64604,0.60391,0.63178,0.57965 41 | 0.62397,0.71022,0.64683,0.64199,0.63465 42 | 0.53029,0.61704,0.62298,0.64251,0.60653 43 | 0.64238,0.68207,0.65727,0.61253,0.65467 44 | 0.87845,0.80057,0.75003,0.55503,0.78577 45 | 0.86999,0.8363,0.72683,0.5166,0.83965 46 | 0.75974,0.82306,0.81923,0.82004,0.90211 47 | 0.65613,0.70911,0.8785,0.86967,0.80644 48 | 0.80065,0.73546,0.79123,0.87229,0.73837 49 | 0.47956,0.42868,0.63274,0.66936,0.70109 50 | 0.46779,0.50499,0.65681,0.45436,0.48167 51 | 0.46725,0.52514,0.51661,0.47976,0.47982 52 | 0.41368,0.41011,0.40334,0.44311,0.42415 53 | 0.41538,0.3266,0.3934,0.42099,0.3738 54 | 0.42623,0.46544,0.47248,0.37722,0.44279 55 | 0.38773,0.4981,0.30207,0.51889,0.42558 56 | 0.44977,0.66149,0.67434,0.57015,0.66511 57 | 0.70703,0.53679,0.70245,0.89409,0.52966 58 | 0.77392,0.87676,0.72331,0.74021,0.67725 59 | 0.6484,0.65621,0.66602,0.71243,0.68902 60 | 0.54647,0.69308,0.71656,0.75048,0.73606 61 | 0.64301,0.63292,0.77247,0.4479,0.8333 62 | 0.58373,0.76747,0.46019,0.63319,0.5988 63 | 0.61489,0.49693,0.50897,0.77909,0.72698 64 | 0.54505,0.67752,0.71662,0.65068,0.61149 65 | 0.62017,0.67214,0.62851,0.73535,0.63542 66 | 0.69943,0.71387,0.59408,0.7022,0.62594 67 | 0.65368,0.68895,0.65306,0.65042,0.63743 68 | 0.72126,0.68741,0.75813,0.61122,0.80786 69 | 0.75902,0.66577,0.74318,0.71534,0.90407 70 | 0.70679,0.80166,0.78392,0.76317,0.83947 71 | 0.74734,0.65539,0.83796,0.68176,0.72837 72 | 0.77276,0.86971,0.67847,0.69368,0.71301 73 | -------------------------------------------------------------------------------- /epanetCPA/AttackOnActuator.m: -------------------------------------------------------------------------------- 1 | classdef AttackOnActuator < CyberPhysicalAttack 2 | % Physical attack to actuator. Actuator can be turned on, off. Settings 3 | % (nominal speed for pumps) can be altered. 4 | 5 | % public methods 6 | methods 7 | 8 | function self = AttackOnActuator(... 9 | target, ini_condition, end_condition, args) 10 | 11 | % one argument at most 12 | if numel(args) == 1 13 | setting = str2num(args{1}); 14 | else 15 | error('AttackOnActuator: this class needs 1 argument only.'); 16 | end 17 | 18 | % call superclass constructor 19 | self@CyberPhysicalAttack('PHY', target, ini_condition, end_condition, setting) 20 | end 21 | 22 | function self = performAttack(self, varargin) 23 | % get arguments 24 | epanetSim = varargin{1}; 25 | 26 | % get dummy controls 27 | dummyControls = epanetSim.epanetMap.dummyControls; 28 | 29 | % get attacked component and index 30 | thisComponent = self.target; 31 | thisIndex = EpanetHelper.getComponentIndex(thisComponent); 32 | 33 | % activate dummy control, save and exit 34 | for i = 1 : numel(dummyControls) 35 | if dummyControls(i).lIndex == thisIndex 36 | self.actControl = i; 37 | return 38 | end 39 | end 40 | end 41 | 42 | function [self, epanetSim] = stopAttack(self, varargin) 43 | % get arguments 44 | time = varargin{1}; 45 | epanetSim = varargin{2}; 46 | 47 | % attack is off 48 | self.inplace = 0; 49 | self.endTime = epanetSim.symbolDict('TIME'); 50 | 51 | % deactivate dummy control 52 | epanetSim.epanetMap.dummyControls(self.actControl) = ... 53 | epanetSim.epanetMap.dummyControls(self.actControl).deactivateWithValues(... 54 | int64(time)); 55 | end 56 | 57 | function [self, epanetSim] = evaluateAttack(self, epanetSim) 58 | if self.inplace == 0 59 | % attack is not active, check if starting condition met 60 | flag = self.evaluateStartingCondition(epanetSim.symbolDict); 61 | if flag 62 | % start attack 63 | self = self.startAttack(... 64 | int64(epanetSim.simTime + epanetSim.tstep),epanetSim); 65 | 66 | % TO DO: should find a better way to perform dummy 67 | % control activation, outside of this class. 68 | epanetSim.epanetMap.dummyControls(self.actControl).isActive = 1; 69 | end 70 | else 71 | % attack is ongoing, check if ending condition is met for this attack 72 | flag = self.evaluateEndingCondition(epanetSim.symbolDict); 73 | if flag 74 | % stop attack 75 | [self, epanetSim] = self.stopAttack(... 76 | int64(epanetSim.simTime + epanetSim.tstep), epanetSim); 77 | end 78 | end 79 | end 80 | 81 | end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /epanetCPA/CyberPhysicalAttack.m: -------------------------------------------------------------------------------- 1 | classdef CyberPhysicalAttack 2 | % Basic class for implementing cyber physical attacks. 3 | % All other attacks inherit from it. 4 | 5 | properties 6 | layer % targeted layer (PHY,PLC,SCADA,CTRL) 7 | target % targeted component 8 | ini_condition % if condition true, attack starts 9 | end_condition % if condition true, attack ends 10 | actControl % control used for simulation 11 | deactControls % controls deactivated for simulation 12 | setting % actuator/control setting 13 | inplace % 0 (no attack) or 1 (attack on) 14 | iniTime % when the attack starts 15 | endTime % when the attack ends 16 | end 17 | 18 | 19 | % public methods 20 | methods 21 | 22 | % constructor 23 | function self = CyberPhysicalAttack(... 24 | layer, target, ini_condition, end_condition, setting ) 25 | % store properties 26 | self.layer = layer; 27 | self.target = target; 28 | self.ini_condition = ini_condition; 29 | self.end_condition = end_condition; 30 | self.setting = setting; 31 | self.actControl = []; 32 | self.deactControls = []; 33 | self.inplace = 0; 34 | end 35 | 36 | % evaluate attack 37 | function self = evaluateAttack(varargin) 38 | % this is a prototype method! 39 | error('Implement this method for subclass of CyberPhysicalAttack!') 40 | end 41 | 42 | function self = startAttack(self, time, epanetSim) 43 | % mark that the attack started 44 | self.inplace = 1; 45 | self.iniTime = time; 46 | % perform attack 47 | self = self.performAttack(epanetSim); 48 | end 49 | 50 | % stop attack 51 | function self = stopAttack(varargin) 52 | % this is a prototype method! 53 | error('Implement this method for subclass of CyberPhysicalAttack!') 54 | end 55 | 56 | % perform attack 57 | function self = performAttack(varargin) 58 | % this is a prototype method! 59 | error('Implement this method for subclass of CyberPhysicalAttack!') 60 | end 61 | 62 | function flag = evaluateStartingCondition(self, symbolDict) 63 | % Checks whether condition to start attack is verified or not. 64 | 65 | % get condition 66 | thisCondition = self.ini_condition; 67 | % find vars 68 | vars = symvar(thisCondition); 69 | % evaluate each var 70 | for j = 1 : numel(vars) 71 | thisVar = vars{j}; 72 | eval(sprintf('%s = symbolDict(''%s'');',thisVar,thisVar)); 73 | end 74 | % evaluate condition 75 | flag = eval([thisCondition,';']); 76 | end 77 | 78 | 79 | function flag = evaluateEndingCondition(self, symbolDict) 80 | % Checks whether condition to end attack is verified or not. 81 | 82 | % get condition 83 | thisCondition = self.end_condition; 84 | % find vars 85 | vars = symvar(thisCondition); 86 | % evaluate each var 87 | for j = 1 : numel(vars) 88 | thisVar = vars{j}; 89 | eval(sprintf('%s = symbolDict(''%s'');',thisVar,thisVar)); 90 | end 91 | % evaluate condition 92 | try 93 | flag = eval([thisCondition,';']); 94 | catch 95 | disp('Problem here!') 96 | end 97 | end 98 | 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A MATLAB® toolbox for assessing the impacts of cyber-physical attacks on water distribution systems 2 | 3 | *epanetCPA* is an open-source object-oriented MATLAB® toolbox for modelling the hydraulic response of water distribution systems to cyber-physical attacks. epanetCPA allows users to quickly design various attack scenarios and assess their impact via simulation with EPANET, a popular public-domain model for water network analysis. 4 | 5 | If you happen to use this code for a publication, please cite the following paper which describes epanetCPA: 6 | ``` 7 | Taormina, R., Galelli, S., Douglas, H.C., Tippenhauer, N. O., Salomons, E., & A. Ostfeld. A toolbox for assessing the impacts of cyber-physical attacks on water distribution systems. Environmental Modelling & Software, DOI: https://doi.org/10.1016/j.envsoft.2018.11.008. 8 | ``` 9 | 10 | ### Requirements: 11 | 1. **EPANET2.0**    If you are runinng on a 32bit OS please download the EPANET2 Programmer's Toolkit from the [EPA website](https://www.epa.gov/water-research/epanet), and substitute the epanet2.h, epanet2.lib and epanet2.dll files in your local epanetCPA folder. Compiled librarires for a 64bit machine are included in the repository. These libraries can also be found [here](http://epanet.de/developer/64bit.html.en). 12 | 13 | 2. **MATLAB**    The toolbox has been tested on MATLAB® R2014b, and it should work for later versions. Make sure that C++ compilers (e.g. Windows SDK 7.1 for MATLAB® R2014b) are installed and interfaced with MATLAB® so that dlls can be invoked. 14 | Feedback on using epanetCPA with other versions of MATLAB is greatly appreciated. Please contact riccardo.taormina@gmail.com to provide your feedback. 15 | 16 | 3. **PYTHON**    You need PYTHON installed only if you want to employ the provided IPython (Jupyter) notebook provided here for visualizing the results. If that is the case, please install the PYTHON modules required, reported at the beginning of the notebook. 17 | 18 | ### Usage 19 | 1. Edit the *main.m* file in the repository to specify which attack scenario you want to simulate. Five different scenarios are provided in the *.cpa* files contained in the repository (see following section). 20 | 2. Simulate the attack scenario by runinng *main.m*. The results are provided as one or two *csv* files depending on the type of attacks. 21 | 3. Use the IPython notebook for visualizing the results, unless you want to do otherwise. 22 | 23 | ### Examples 24 | (Please refer to the EPANET maps in the *.inp* file for details on the water networks layout and control logic) 25 | 26 | Folder scenarios/ctown/: 27 | 1. *scenario01.cpa*     Manipulation of sensor readings arriving to PLC3. The attacker shows that tank T2 is full. The PLC closes valve V2, thus preventing the flow to reach the tank and disconnecting part of the network. 28 | 2. *scenario02.cpa*     Same as *scenario01* but run using the pressure driven engine to obtain more reliable results. 29 | 3. *scenario03.cpa*     The attacker modifies the control logic of PLC5 so that some of the controlled pumps (PU10, PU11) switch on/off intermittently. 30 | 4. *scenario04.cpa*     Denial-of-service of the connection link between PLC2 and PLC1. PLC1 fails to receive updated readings of T1 water level and keeps the pumps (PU1,PU2) ON. This causes a surge in the tank T1. 31 | 5. *scenario05.cpa*     Same as *scenario04* but this time the attacker conceals the tanks surge from SCADA by altering the data sent by PLC2 to SCADA. 32 | 33 | Folder scenarios/minitown/: 34 | 1. *minitown_attack.cpa*     Denial-of-service of the connection link between PLC1 and PLC2. PLC1 fails to receive updated readings of TANK water level and keeps the pumps (PUMP1,PUMP2) ON. This causes a surge in the tank TANK. The attacker conceals the tanks surge from SCADA by altering the data sent by PLC1 to SCADA. 35 | 36 | ### Authors 37 | Riccardo Taormina is the main developer of epanetCPA. The core of the pressure driven engine was developed by Hunter C. Douglas. 38 | 39 | ### License 40 | epanetCPA is under the MIT license. Please read it carefully before employing the toolbox. 41 | -------------------------------------------------------------------------------- /epanetCPA/AttackOnControl.m: -------------------------------------------------------------------------------- 1 | classdef AttackOnControl < CyberPhysicalAttack 2 | % Class implementing attack changing control logic. 3 | 4 | properties 5 | isNode % are we modifying the link or node setting of control? 6 | deact_setting % control setting (when deactivated) 7 | end 8 | 9 | 10 | % public methods 11 | methods 12 | 13 | % constructor 14 | function self = AttackOnControl(... 15 | target, ini_condition, end_condition, args) 16 | % get the attack target 17 | % it comes as a string of the format "CTRLxt" 18 | % where: 19 | % x is the number of the attacked control 20 | % t is either "n" or "l", depending whether the attack changes 21 | % the controlling node or controlled link setting 22 | expression = '\d+'; 23 | [temp,ix1,ix2] = regexp(target,expression,'split'); 24 | controlIx = str2num(target(ix1:ix2)); 25 | if ~strcmp(temp{1},'CTRL') 26 | error('Target must start with CTRL') 27 | end 28 | 29 | if numel(args)>1 30 | error('Wrong number of arguments specified for AttackOnControl'); 31 | else 32 | setting = str2num(args{1}); 33 | end 34 | 35 | % call superclass constructor 36 | self@CyberPhysicalAttack(... 37 | 'CTRL', controlIx, ini_condition, end_condition,setting); 38 | 39 | % check if is a node or a link setting to be changed 40 | if strcmp(temp{2},'n') 41 | self.isNode = true; 42 | elseif strcmp(temp{2},'l') 43 | self.isNode = false; 44 | end 45 | % initialize deact_setting 46 | self.deact_setting = []; 47 | end 48 | 49 | function [self,controls] = startAttack(self, time, epanetSim) 50 | % mark that the attack started 51 | self.inplace = 1; 52 | self.iniTime = time; 53 | 54 | % get controls 55 | controls = epanetSim.epanetMap.controls; 56 | 57 | % store original value 58 | if self.isNode 59 | self.deact_setting = controls(self.target).nSetting; 60 | else 61 | self.deact_setting = controls(self.target).lSetting; 62 | end 63 | 64 | % perform attack 65 | [self, controls] = self.performAttack(controls); 66 | end 67 | 68 | function [self,controls] = performAttack(self, varargin) 69 | % get arguments 70 | controls = varargin{1}; 71 | if self.isNode 72 | % change original value (node) 73 | controls(self.target).nSetting =... 74 | single(self.setting); 75 | else 76 | % change original value (link) 77 | controls(self.target).lSetting =... 78 | single(self.setting); 79 | end 80 | end 81 | 82 | function [self,controls] = stopAttack(self, varargin) 83 | % get arguments 84 | time = varargin{1}; 85 | epanetSim = varargin{2}; 86 | % attack is off 87 | self.inplace = 0; 88 | self.endTime = time; 89 | % get controls 90 | controls = epanetSim.epanetMap.controls; 91 | if self.isNode 92 | % change original value (node) 93 | controls(self.target).nSetting =... 94 | single(self.deact_setting); 95 | else 96 | % change original value (link) 97 | controls(self.target).lSetting =... 98 | single(self.deact_setting); 99 | end 100 | end 101 | 102 | function [self, epanetSim] = evaluateAttack(self, epanetSim) 103 | if self.inplace == 0 104 | % attack is not active, check if starting condition met 105 | flag = self.evaluateStartingCondition(epanetSim.symbolDict); 106 | if flag 107 | % start attack 108 | [self, epanetSim.epanetMap.controls] = ... 109 | self.startAttack(epanetSim.symbolDict('TIME'),epanetSim); 110 | end 111 | else 112 | % attack is ongoing 113 | % check if ending condition is met for this attack 114 | flag = self.evaluateEndingCondition(epanetSim.symbolDict); 115 | if flag 116 | % stop attack 117 | [self, epanetSim.epanetMap.controls] = ... 118 | self.stopAttack(epanetSim.symbolDict('TIME'),epanetSim); 119 | end 120 | end 121 | end 122 | 123 | end 124 | 125 | end 126 | -------------------------------------------------------------------------------- /minitown_patterns.csv: -------------------------------------------------------------------------------- 1 | 0.5691504,65.9725,98.2825 2 | 0.46466868,64.445,103.6625 3 | 0.43854825,63.5875,102.3575 4 | 0.3604161,35.8425,93.75 5 | 0.309779115,35.9075,80.8075 6 | 0.334524795,15.92,89.0825 7 | 0.384703515,32.01,89.3975 8 | 0.471542475,30.79,70.69 9 | 0.55013289,0,78.005 10 | 0.680505915,24.63,80.1625 11 | 0.70364769,50.03,77.48 12 | 0.67088259,67.525,19.4475 13 | 0.79690221,67.32,0.83 14 | 0.719915685,67.225,0.75 15 | 0.770552655,66.91,0.8 16 | 0.73847493,66.94,0.77 17 | 0.81477408,67.2825,0.85 18 | 0.72426909,66.1775,64.975 19 | 0.806983785,64.835,79.7075 20 | 0.779259465,64.8275,76.9425 21 | 0.866785815,32.1475,78.3875 22 | 0.740766195,31.68,78.3625 23 | 0.67042434,31.9375,77.715 24 | 0.632389335,39.5925,78.1375 25 | 0.52252044,63.385,74.5125 26 | 0.506709855,55.935,78.7225 27 | 0.445588455,35.9925,88.735 28 | 0.45465063,42.9275,70.6425 29 | 0.47547432,64.075,75.34 30 | 0.53389635,31.29,79.045 31 | 0.662501925,31.1175,72.3025 32 | 0.62239704,15.71,65.3675 33 | 0.67599876,16.3175,73.96 34 | 0.72593706,57.915,69.6175 35 | 0.803061855,66.0275,70.595 36 | 0.70144995,66.015,72.645 37 | 0.69335184,65.76,73.1775 38 | 0.653825385,64.92,74.255 39 | 0.621432975,64.4275,75.13 40 | 0.65228289,64.035,70.61 41 | 0.571880295,55.8275,70.6425 42 | 0.57901434,30.9925,71.71 43 | 0.58209933,7.6975,63.335 44 | 0.711668985,8.07,14.5825 45 | 0.70453494,32.4475,0.73 46 | 0.665779725,58.6975,0.69 47 | 0.741747645,66.8575,0.77 48 | 0.692194965,66.915,0.72 49 | 0.72708954,66.725,0.75 50 | 0.619247205,66.475,27.1325 51 | 0.46304505,65.4325,103.6575 52 | 0.45095511,57.105,89.025 53 | 0.309260895,0,84.375 54 | 0.282421215,0,86.6 55 | 0.433303785,16.165,92.6575 56 | 0.40670589,32.2875,82.28 57 | 0.545014905,31.2275,73.765 58 | 0.69928266,7.9275,83.62 59 | 0.739904895,49.4925,40.47 60 | 0.6434271,66.3525,0.67 61 | 0.680905935,65.885,0.71 62 | 0.691061505,65.1475,0.72 63 | 0.67727895,64.97,0.7 64 | 0.626259375,64.01,0.65 65 | 0.626742975,63.4425,44.92 66 | 0.7142742,63.875,81.595 67 | 0.681873135,31.165,69.31 68 | 0.788264685,31.845,78.6475 69 | 0.75779802,31.565,76.3175 70 | 0.788264685,49.04,68.9225 71 | 0.802047225,65.5,65.615 72 | 0.845571045,64.645,63.4025 73 | 0.604884255,64.615,86.325 74 | 0.443633715,56.305,88.4875 75 | 0.5131584,36.19,87.06 76 | 0.36476118,41.6825,84.1625 77 | 0.31782714,31.4575,73.625 78 | 0.463887405,22.57,66.1925 79 | 0.39572595,0,83.6225 80 | 0.64149669,16.43,76.2675 81 | 0.671098245,33.035,73.385 82 | 0.668761275,58.9,71.19 83 | 0.79456785,66.855,68.0875 84 | 0.674603685,66.515,72.515 85 | 0.754255215,66.87,64.1775 86 | 0.808394895,66.7925,68.9475 87 | 0.713747835,65.9025,71.1 88 | 0.68570427,65.3975,70.3325 89 | 0.68122509,65.2725,67.595 90 | 0.549576105,23.7925,71.945 91 | 0.68219883,8.18,70.8325 92 | 0.569829795,32.775,31.8275 93 | 0.74334939,32.84,0.77 94 | 0.832154025,49.59,0.86 95 | 0.67888812,66.6425,0.7 96 | 0.746270595,66.185,0.77 97 | 0.601325505,65.945,0.62 98 | 0.503529285,64.9975,0.52 99 | 0.51908778,64.26,52.6125 100 | 0.35946171,33.8875,101.6925 101 | 0.29924835,0,91.01 102 | 0.453216765,16.4575,80.435 103 | 0.42957594,32.8475,93.89 104 | 0.539495655,32.205,91.0125 105 | 0.896330625,32.5,85.54 106 | 0.89673474,58.49,80.825 107 | 0.682552935,66.88,86.505 108 | 0.57990732,66.24,87.6775 109 | 0.428969775,43.945,85.405 110 | 0.4200792,36.5825,79.5575 111 | 0.62941161,57.48,58.22 112 | 0.691039395,40.2975,0.72 113 | 0.670631505,31.6275,0.7 114 | 0.618904575,31.41,0.64 115 | 0.682552935,31.44,0.71 116 | 0.832884315,48.1,0.86 117 | 0.90279648,64.3625,0.94 118 | 0.821771115,64.7425,0.85 119 | 0.84945309,65.475,0.88 120 | 0.691039395,65.0175,67.08 121 | 0.716292135,64.4375,86.2525 122 | 0.54775281,63.665,89.0725 123 | 0.53349507,49.1725,84.4375 124 | 0.419645895,26.6875,86.445 125 | 0.35601804,23.6475,84.65 126 | 0.33814266,30.945,68.9275 127 | 0.45241743,15.14,60.8125 128 | 0.509874015,0,74.85 129 | 0.56456418,32.21,68.8075 130 | 0.760767795,49.6575,58.3325 131 | 0.62159517,66.33,66.585 132 | 0.67820055,65.49,68.345 133 | 0.6928839,66.3275,71.7225 134 | 0.639257745,66.12,74.5025 135 | 0.619892745,66.2075,75.1775 136 | 0.659686755,51.4175,71.5475 137 | 0.65628192,26.6475,72.465 138 | 0.631809675,32.6975,71.6675 139 | 0.604145385,32.385,71.565 140 | 0.70394961,32.285,69.17 141 | 0.772471905,49.5375,66.3175 142 | 0.88546986,66.7425,67.135 143 | 0.88334184,66.63,67.6975 144 | 0.752042895,66.54,70.3125 145 | 0.71409849,64.9625,71.3875 146 | 0.457925235,63.9675,71.965 147 | 0.446544345,56.6525,56.9875 148 | 0.42936957,18.05,74.5925 149 | 0.40184853,16.0125,75.1425 150 | 0.43309422,32.11,43.5625 151 | 0.469926885,31.0425,0.49 152 | 0.52227894,0,0.54 153 | 0.590771145,16.6225,0.61 154 | 0.754862745,67.6275,0.78 155 | 0.76996827,67.395,0.8 156 | 0.644778585,66.9,0.67 157 | 0.73210098,66.295,0.76 158 | 0.70954614,65.835,0.74 159 | 0.548144565,64.3425,109.53 160 | 0.577734855,63.6075,99.2375 161 | 0.592426545,39.35,97.77 162 | 0.624499935,15.53,95.975 163 | 0.55973238,8.065,91.245 164 | 0.766864395,32.34,76.5525 165 | 0.716995455,57.205,86.11 166 | 0.77452062,65.3975,85.8775 167 | 0.90446958,65.485,83.2575 168 | 0.85749759,65.69,83.2675 169 | -------------------------------------------------------------------------------- /epanetCPA/EpanetControl.m: -------------------------------------------------------------------------------- 1 | classdef EpanetControl 2 | % Class implementing an EPANET control. 3 | 4 | properties 5 | isActive % yes or no 6 | % TODO: these two should be accessed with a method maybe... 7 | lSetting % setting of link being controlled 8 | nSetting % setting of the controlling node 9 | end 10 | 11 | properties (SetAccess = private) 12 | cIndex % index of control in .inp file 13 | cType % control type (0 BELOW, 1 ABOVE, ...) 14 | lIndex % index of link being controlled 15 | nIndex % index of the controlling node 16 | controlString % string version of the control 17 | end 18 | 19 | 20 | % public methods 21 | methods 22 | 23 | % constructor 24 | function self = EpanetControl(cIndex) 25 | % creates the control instance from the cIndex in the .inp file 26 | 27 | % initialize variables 28 | self.cIndex = int32(cIndex); 29 | self.cType = int32(0); 30 | self.lIndex = int32(0); 31 | self.nIndex = int32(0); 32 | self.lSetting = single(0); 33 | self.nSetting = single(0); 34 | 35 | self.isActive = false; 36 | 37 | % summon .dll 38 | [errorcode,self.cType,self.lIndex,... 39 | self.lSetting,self.nIndex,self.nSetting] =... 40 | calllib('epanet2','ENgetcontrol',... 41 | self.cIndex,self.cType,self.lIndex,... 42 | self.lSetting,self.nIndex,self.nSetting); 43 | 44 | % create controlString 45 | self = self.createControlString(); 46 | end 47 | 48 | % activate control 49 | function self = activate(self) 50 | % activate the control 51 | self.isActive = true; 52 | end 53 | 54 | function self = deactivate(self) 55 | % deactivate the control (lIndex = 0) 56 | % summon .dll 57 | errorcode =... 58 | calllib('epanet2','ENsetcontrol',... 59 | self.cIndex,self.cType,int32(0),... 60 | self.lSetting,self.nIndex,self.nSetting); 61 | self.isActive = false; 62 | end 63 | 64 | function self = deactivateWithValues(self,time) 65 | % deactivate the control (lIndex = 0) 66 | % summon .dll 67 | errorcode =... 68 | calllib('epanet2','ENsetcontrol',... 69 | self.cIndex,self.cType,self.lIndex,... 70 | single(0),self.nIndex,single(time)); 71 | self.isActive = false; 72 | end 73 | 74 | function self = activateWithValues(self,value,time) 75 | % activates control with external value (for dummy ones) 76 | errorcode =... 77 | calllib('epanet2','ENsetcontrol',... 78 | self.cIndex,self.cType,self.lIndex,... 79 | single(value),self.nIndex,single(time)); 80 | 81 | self.isActive = true; 82 | end 83 | 84 | function controlString = getControlString(self) 85 | [~,controlString] = self.createControlString(); 86 | end 87 | 88 | function self = overrideControl(self, symbolDict) 89 | % create override control string based on type of attack 90 | % TO DO: should we override only when attacks are in place to speed up computation? 91 | 92 | % TO DO: the control string can be determined at the beginning 93 | % and updated only for the setting, so we remove the switch 94 | HOURS_TO_SECONDS = 3600; 95 | 96 | switch self.cType 97 | case 0 98 | % BELOW 99 | % get sensor ID 100 | thisSensor = EpanetHelper.getComponentId(self.nIndex, 1); 101 | % create string 102 | overrideControlString = sprintf('symbolDict(''%s'') <= %f;',... 103 | thisSensor,self.nSetting); 104 | case 1 105 | % ABOVE 106 | % get sensor ID 107 | thisSensor = EpanetHelper.getComponentId(self.nIndex, 1); 108 | % create string 109 | overrideControlString = sprintf('symbolDict(''%s'') > %f;',... 110 | thisSensor,self.nSetting); 111 | case 2 112 | % TIME IN THE SIMULATION 113 | % get sensor ID 114 | thisSensor = 'TIME'; 115 | overrideControlString = sprintf('symbolDict(''TIME'') == %d;',... 116 | self.nSetting/HOURS_TO_SECONDS); 117 | case 3 118 | % CLOCKTIME 119 | % create string 120 | thisSensor = 'CLOCKTIME'; 121 | overrideControlString = sprintf('symbolDict(''TIME'') == %d;',... 122 | self.nSetting); 123 | end 124 | 125 | % create variable from dictionary 126 | % TO DO: remove this check when fully tested! 127 | try 128 | eval(sprintf('%s = symbolDict(''%s'');', thisSensor, thisSensor)); 129 | catch 130 | error('Failed creating variable from dictionary.') 131 | end 132 | 133 | % eval string and perform action 134 | if eval(overrideControlString) 135 | % change status of actuator 136 | errocode = calllib('epanet2','ENsetlinkvalue',... 137 | int32(self.lIndex), EpanetHelper.EPANETCODES('EN_STATUS'), single(self.lSetting>0)); 138 | end 139 | end 140 | 141 | end 142 | 143 | 144 | % private methods 145 | methods (Access = private) 146 | 147 | function self = createControlString(self) 148 | switch self.cType 149 | case 0 150 | % BELOW 151 | self.controlString = sprintf(... 152 | 'LINK %s %.3f IF %s BELOW %.3f',... 153 | EpanetHelper.getComponentId(self.lIndex,0),double(self.lSetting),... 154 | EpanetHelper.getComponentId(self.nIndex,1),double(self.nSetting)); 155 | case 1 156 | % ABOVE 157 | self.controlString = sprintf(... 158 | 'LINK %s %.3f IF %s ABOVE %.3f',... 159 | EpanetHelper.getComponentId(self.lIndex,1),double(self.lSetting),... 160 | EpanetHelper.getComponentId(self.nIndex,0),double(self.nSetting)); 161 | case 2 162 | % TIME IN THE SIMULATION 163 | self.controlString = sprintf(... 164 | 'LINK %s %.3f AT TIME %.3f',... 165 | EpanetHelper.getComponentId(self.lIndex,0),double(self.lSetting),... 166 | double(self.nSetting)); 167 | case 3 168 | % CLOCKTIME 169 | self.controlString = sprintf(... 170 | 'LINK %s %.3f AT CLOCKTIME %.3f',... 171 | EpanetHelper.getComponentId(self.lIndex,1),... 172 | double(self.lSetting),double(self.nSetting)); 173 | end 174 | end 175 | 176 | end 177 | 178 | end -------------------------------------------------------------------------------- /epanetCPA/epanet2.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** EPANET2.H 3 | ** 4 | ** C/C++ header file for EPANET Programmers Toolkit 5 | ** 6 | ** Last updated on 2/14/08 (2.00.12) 7 | */ 8 | 9 | #ifndef EPANET2_H 10 | #define EPANET2_H 11 | 12 | // --- Define the EPANET toolkit constants 13 | 14 | #define EN_ELEVATION 0 /* Node parameters */ 15 | #define EN_BASEDEMAND 1 16 | #define EN_PATTERN 2 17 | #define EN_EMITTER 3 18 | #define EN_INITQUAL 4 19 | #define EN_SOURCEQUAL 5 20 | #define EN_SOURCEPAT 6 21 | #define EN_SOURCETYPE 7 22 | #define EN_TANKLEVEL 8 23 | #define EN_DEMAND 9 24 | #define EN_HEAD 10 25 | #define EN_PRESSURE 11 26 | #define EN_QUALITY 12 27 | #define EN_SOURCEMASS 13 28 | #define EN_INITVOLUME 14 29 | #define EN_MIXMODEL 15 30 | #define EN_MIXZONEVOL 16 31 | 32 | #define EN_TANKDIAM 17 33 | #define EN_MINVOLUME 18 34 | #define EN_VOLCURVE 19 35 | #define EN_MINLEVEL 20 36 | #define EN_MAXLEVEL 21 37 | #define EN_MIXFRACTION 22 38 | #define EN_TANK_KBULK 23 39 | 40 | #define EN_DIAMETER 0 /* Link parameters */ 41 | #define EN_LENGTH 1 42 | #define EN_ROUGHNESS 2 43 | #define EN_MINORLOSS 3 44 | #define EN_INITSTATUS 4 45 | #define EN_INITSETTING 5 46 | #define EN_KBULK 6 47 | #define EN_KWALL 7 48 | #define EN_FLOW 8 49 | #define EN_VELOCITY 9 50 | #define EN_HEADLOSS 10 51 | #define EN_STATUS 11 52 | #define EN_SETTING 12 53 | #define EN_ENERGY 13 54 | 55 | #define EN_DURATION 0 /* Time parameters */ 56 | #define EN_HYDSTEP 1 57 | #define EN_QUALSTEP 2 58 | #define EN_PATTERNSTEP 3 59 | #define EN_PATTERNSTART 4 60 | #define EN_REPORTSTEP 5 61 | #define EN_REPORTSTART 6 62 | #define EN_RULESTEP 7 63 | #define EN_STATISTIC 8 64 | #define EN_PERIODS 9 65 | 66 | #define EN_NODECOUNT 0 /* Component counts */ 67 | #define EN_TANKCOUNT 1 68 | #define EN_LINKCOUNT 2 69 | #define EN_PATCOUNT 3 70 | #define EN_CURVECOUNT 4 71 | #define EN_CONTROLCOUNT 5 72 | 73 | #define EN_JUNCTION 0 /* Node types */ 74 | #define EN_RESERVOIR 1 75 | #define EN_TANK 2 76 | 77 | #define EN_CVPIPE 0 /* Link types */ 78 | #define EN_PIPE 1 79 | #define EN_PUMP 2 80 | #define EN_PRV 3 81 | #define EN_PSV 4 82 | #define EN_PBV 5 83 | #define EN_FCV 6 84 | #define EN_TCV 7 85 | #define EN_GPV 8 86 | 87 | #define EN_NONE 0 /* Quality analysis types */ 88 | #define EN_CHEM 1 89 | #define EN_AGE 2 90 | #define EN_TRACE 3 91 | 92 | #define EN_CONCEN 0 /* Source quality types */ 93 | #define EN_MASS 1 94 | #define EN_SETPOINT 2 95 | #define EN_FLOWPACED 3 96 | 97 | #define EN_CFS 0 /* Flow units types */ 98 | #define EN_GPM 1 99 | #define EN_MGD 2 100 | #define EN_IMGD 3 101 | #define EN_AFD 4 102 | #define EN_LPS 5 103 | #define EN_LPM 6 104 | #define EN_MLD 7 105 | #define EN_CMH 8 106 | #define EN_CMD 9 107 | 108 | #define EN_TRIALS 0 /* Misc. options */ 109 | #define EN_ACCURACY 1 110 | #define EN_TOLERANCE 2 111 | #define EN_EMITEXPON 3 112 | #define EN_DEMANDMULT 4 113 | 114 | #define EN_LOWLEVEL 0 /* Control types */ 115 | #define EN_HILEVEL 1 116 | #define EN_TIMER 2 117 | #define EN_TIMEOFDAY 3 118 | 119 | #define EN_AVERAGE 1 /* Time statistic types. */ 120 | #define EN_MINIMUM 2 121 | #define EN_MAXIMUM 3 122 | #define EN_RANGE 4 123 | 124 | #define EN_MIX1 0 /* Tank mixing models */ 125 | #define EN_MIX2 1 126 | #define EN_FIFO 2 127 | #define EN_LIFO 3 128 | 129 | #define EN_NOSAVE 0 /* Save-results-to-file flag */ 130 | #define EN_SAVE 1 131 | #define EN_INITFLOW 10 /* Re-initialize flow flag */ 132 | 133 | 134 | 135 | // --- define WINDOWS 136 | 137 | #undef WINDOWS 138 | #ifdef _WIN32 139 | #define WINDOWS 140 | #endif 141 | #ifdef __WIN32__ 142 | #define WINDOWS 143 | #endif 144 | 145 | // --- define DLLEXPORT 146 | 147 | #ifdef WINDOWS 148 | #ifdef __cplusplus 149 | #define DLLEXPORT extern "C" __declspec(dllexport) __stdcall 150 | #else 151 | #define DLLEXPORT __declspec(dllexport) __stdcall 152 | #endif 153 | #else 154 | #ifdef __cplusplus 155 | #define DLLEXPORT extern "C" 156 | #else 157 | #define DLLEXPORT 158 | #endif 159 | #endif 160 | 161 | 162 | // --- declare the EPANET toolkit functions 163 | 164 | int DLLEXPORT ENepanet(char *, char *, char *, void (*) (char *)); 165 | int DLLEXPORT ENopen(char *, char *, char *); 166 | int DLLEXPORT ENsaveinpfile(char *); 167 | int DLLEXPORT ENclose(void); 168 | 169 | int DLLEXPORT ENsolveH(void); 170 | int DLLEXPORT ENsaveH(void); 171 | int DLLEXPORT ENopenH(void); 172 | int DLLEXPORT ENinitH(int); 173 | int DLLEXPORT ENrunH(long *); 174 | int DLLEXPORT ENnextH(long *); 175 | int DLLEXPORT ENcloseH(void); 176 | int DLLEXPORT ENsavehydfile(char *); 177 | int DLLEXPORT ENusehydfile(char *); 178 | 179 | int DLLEXPORT ENsolveQ(void); 180 | int DLLEXPORT ENopenQ(void); 181 | int DLLEXPORT ENinitQ(int); 182 | int DLLEXPORT ENrunQ(long *); 183 | int DLLEXPORT ENnextQ(long *); 184 | int DLLEXPORT ENstepQ(long *); 185 | int DLLEXPORT ENcloseQ(void); 186 | 187 | int DLLEXPORT ENwriteline(char *); 188 | int DLLEXPORT ENreport(void); 189 | int DLLEXPORT ENresetreport(void); 190 | int DLLEXPORT ENsetreport(char *); 191 | 192 | int DLLEXPORT ENgetcontrol(int, int *, int *, float *, 193 | int *, float *); 194 | int DLLEXPORT ENgetcount(int, int *); 195 | int DLLEXPORT ENgetoption(int, float *); 196 | int DLLEXPORT ENgettimeparam(int, long *); 197 | int DLLEXPORT ENgetflowunits(int *); 198 | int DLLEXPORT ENgetpatternindex(char *, int *); 199 | int DLLEXPORT ENgetpatternid(int, char *); 200 | int DLLEXPORT ENgetpatternlen(int, int *); 201 | int DLLEXPORT ENgetpatternvalue(int, int, float *); 202 | int DLLEXPORT ENgetqualtype(int *, int *); 203 | int DLLEXPORT ENgeterror(int, char *, int); 204 | 205 | int DLLEXPORT ENgetnodeindex(char *, int *); 206 | int DLLEXPORT ENgetnodeid(int, char *); 207 | int DLLEXPORT ENgetnodetype(int, int *); 208 | int DLLEXPORT ENgetnodevalue(int, int, float *); 209 | 210 | int DLLEXPORT ENgetlinkindex(char *, int *); 211 | int DLLEXPORT ENgetlinkid(int, char *); 212 | int DLLEXPORT ENgetlinktype(int, int *); 213 | int DLLEXPORT ENgetlinknodes(int, int *, int *); 214 | int DLLEXPORT ENgetlinkvalue(int, int, float *); 215 | 216 | int DLLEXPORT ENgetversion(int *); 217 | 218 | int DLLEXPORT ENsetcontrol(int, int, int, float, int, float); 219 | int DLLEXPORT ENsetnodevalue(int, int, float); 220 | int DLLEXPORT ENsetlinkvalue(int, int, float); 221 | int DLLEXPORT ENaddpattern(char *); 222 | int DLLEXPORT ENsetpattern(int, float *, int); 223 | int DLLEXPORT ENsetpatternvalue(int, int, float); 224 | int DLLEXPORT ENsettimeparam(int, long); 225 | int DLLEXPORT ENsetoption(int, float); 226 | int DLLEXPORT ENsetstatusreport(int); 227 | int DLLEXPORT ENsetqualtype(int, char *, char *, char *); 228 | 229 | #endif 230 | -------------------------------------------------------------------------------- /epanetCPA/AttackOnSensor.m: -------------------------------------------------------------------------------- 1 | classdef AttackOnSensor < CyberPhysicalAttack 2 | % Class implementing a physical attack to a Sensor. 3 | 4 | properties 5 | alterMethod % how are readings altered by the attack? 6 | alteredReading % current altered reading of the sensor 7 | end 8 | 9 | % public methods 10 | methods 11 | 12 | function self = AttackOnSensor(... 13 | target, ini_condition, end_condition, args) 14 | 15 | % handle args 16 | alterMethod = args{1}; 17 | setting = []; 18 | switch alterMethod 19 | case 'DoS' 20 | % sensor returns last trainsmitted reading 21 | if numel(args) > 1 22 | error('Too many arguments for AttackOnSensor') 23 | end 24 | case 'constant' 25 | % subsitute reading with a constant value 26 | if numel(args) > 2 27 | error('Too many arguments for AttackOnSensor') 28 | else 29 | setting = str2num(args{2}); 30 | end 31 | case 'offset' 32 | % adds offset to reading 33 | if numel(args) > 2 34 | error('Too many arguments for AttackOnSensor') 35 | else 36 | setting = str2num(args{2}); 37 | end 38 | case 'custom' 39 | % substitute with custom readings 40 | if numel(args) == 2 41 | % check if file exists 42 | filename = args{2}; 43 | if ~exist(filename, 'file') 44 | error(' AttackOnSensor: File containing custom altered readings cannot be found!') 45 | end 46 | setting = csvread(filename); 47 | else 48 | error('Wrong number of arguments for AttackOnSensor') 49 | end 50 | otherwise 51 | error('not implemented yet!') 52 | end 53 | % call superclass constructor 54 | self@CyberPhysicalAttack(... 55 | 'PHY', target, ini_condition, end_condition, setting); 56 | % initialize other properties 57 | self.alterMethod = alterMethod; 58 | self.alteredReading = NaN; 59 | end 60 | 61 | 62 | function self = performAttack(self, varargin) 63 | % get arguments 64 | epanetSim = varargin{1}; 65 | % compute altered value of the sensor 66 | self = self.alterReading(epanetSim); 67 | end 68 | 69 | function self = stopAttack(self, varargin) 70 | % get arguments 71 | time = varargin{1}; 72 | % attack is off 73 | self.inplace = 0; 74 | self.endTime = time; 75 | % reset altered reading 76 | self.alteredReading = NaN; 77 | end 78 | 79 | function [self, epanetSim] = evaluateAttack(self, epanetSim) 80 | if self.inplace == 0 81 | % attack is not active, check if starting condition met 82 | flag = self.evaluateStartingCondition(epanetSim.symbolDict); 83 | if flag 84 | % start attack 85 | self = self.startAttack(epanetSim.symbolDict('TIME'),epanetSim); 86 | end 87 | else 88 | % attack is ongoing, check if ending condition is met for this attack 89 | flag = self.evaluateEndingCondition(epanetSim.symbolDict); 90 | if flag 91 | % stop attack 92 | self = self.stopAttack(epanetSim.symbolDict('TIME')); 93 | else 94 | % ...continue attack (needed for sensor alteration) 95 | self = self.performAttack(epanetSim); 96 | end 97 | end 98 | end 99 | 100 | end 101 | 102 | % private methods 103 | methods (Access = private) 104 | 105 | function self = alterReading(self, epanetSim) 106 | % get time vector 107 | T = epanetSim.T; 108 | % initialize to current time 109 | rowToCopyFrom = numel(T); 110 | % switch alter method 111 | switch self.alterMethod 112 | case 'DoS' 113 | % reading is not updated 114 | if isnan(self.alteredReading) 115 | % assign last reading if it's first time 116 | % otherwise leave unchanged. It's reset to NaN 117 | % when attack ceases. 118 | thisReading = getReading(self, rowToCopyFrom, epanetSim); 119 | self.alteredReading = thisReading; 120 | end 121 | case 'constant' 122 | % subsitute reading with a constant value 123 | self.alteredReading = self.setting; 124 | case 'offset' 125 | % adds offset to reading 126 | thisReading = getReading(self, rowToCopyFrom, epanetSim); 127 | self.alteredReading = thisReading + self.setting; 128 | case 'custom' 129 | % substitute with custom readings 130 | T = epanetSim.T(end); 131 | ix = find(self.setting(:,1)>=T,1); 132 | if ~isempty(ix) 133 | self.alteredReading = self.setting(ix,2); 134 | else 135 | self.alteredReading = self.setting(end,2); 136 | end 137 | otherwise 138 | error('not implemented yet!') 139 | end 140 | end 141 | 142 | function thisReading = getReading(self, rowToCopyFrom, epanetSim) 143 | 144 | time = epanetSim.T(rowToCopyFrom); 145 | % get attacked component and index 146 | thisComponent = self.target; 147 | 148 | % remove prefix 149 | temp = regexp(thisComponent,'_','split'); 150 | thisVariable = temp{1}; 151 | thisComponent = temp{2}; 152 | if ~ismember(thisVariable,'PFS') 153 | error('Attacks targeting %s not implemented yet.',temp{1}); 154 | end 155 | 156 | [thisIndex,~,isNode] = EpanetHelper.getComponentIndex(thisComponent); 157 | if isNode 158 | thisIndex = find(ismember(epanetSim.whatToStore.nodeIdx,thisIndex)); 159 | else 160 | thisIndex = find(ismember(epanetSim.whatToStore.linkIdx,thisIndex)); 161 | end 162 | % check if variable has been stored, otherwise return error 163 | if isempty(thisIndex) 164 | error('Variable %s is not among those being stored during the simulation',... 165 | thisComponent); 166 | end 167 | 168 | % If it has been already modified, return the modified value 169 | % if not, return physical layer reading. 170 | % TO DO: check if it works in everycase 171 | try 172 | ix = find(([epanetSim.alteredReadings.time] == time) & ... 173 | strcmp({epanetSim.alteredReadings.layer},'PLC') & ... 174 | strcmp({epanetSim.alteredReadings.sensorId},thisComponent)); 175 | catch 176 | % TODO: should substitute this catch statement 177 | ix = []; 178 | end 179 | 180 | if ~isempty(ix) 181 | thisReading = epanetSim.alteredReadings(ix).reading; 182 | else 183 | % check if it's node (if so return pressure) 184 | if isNode 185 | thisReading = epanetSim.readings.PRESSURE(rowToCopyFrom,thisIndex); 186 | else 187 | % return flow if it's a link 188 | thisReading = epanetSim.readings.FLOW(rowToCopyFrom,thisIndex); 189 | end 190 | end 191 | end 192 | 193 | end 194 | 195 | end 196 | 197 | -------------------------------------------------------------------------------- /epanetCPA/EpanetCPAMap.m: -------------------------------------------------------------------------------- 1 | classdef EpanetCPAMap 2 | % Class that implements an EPANET map (.inp file) and extends it to feature a cyber layer. 3 | 4 | properties 5 | originalFilePath % original file path of .inp file 6 | 7 | modifiedFilePath % modified file path of .inp file 8 | 9 | components % dictionary containing map components 10 | 11 | controls % list of control objects 12 | 13 | dummyControls % list of control objects (dummies) 14 | 15 | cyberlayer % cyberlayer (sensors, actuators, PLCs and SCADA) 16 | 17 | duration % duration of simulation 18 | 19 | baseDemand % base demand of all nodal junctions 20 | 21 | patterns % demand patterns 22 | 23 | tankIniLevels % tankInitiaLevels 24 | 25 | h_tstep % hydraulic time step 26 | 27 | usePDA % flag for pressure driven analysis 28 | 29 | pdaDict % dictionary containing infos on artificial strings for PDA 30 | 31 | pdaOptions % options for PDA (emitter exponent, pressures, HFR) 32 | 33 | end 34 | 35 | 36 | % public methods 37 | methods 38 | 39 | function self = EpanetCPAMap(mapFile, cybernodes, cyberlinks, cyberoptions, PDA_ENABLED) 40 | 41 | % original map file 42 | self.originalFilePath = mapFile; 43 | 44 | % store properties 45 | self.patterns = cyberoptions.patterns; 46 | self.tankIniLevels = cyberoptions.initial_conditions; 47 | self.pdaOptions = cyberoptions.pda_options; 48 | self.usePDA = PDA_ENABLED; 49 | 50 | % original map file 51 | self.originalFilePath = mapFile; 52 | 53 | % initialize map and modify .inp file 54 | self = self.initializeMap(); 55 | 56 | % open modified file 57 | EpanetHelper.epanetloadfile(self.modifiedFilePath); 58 | 59 | % get hydraulic time step 60 | self = getHydraulicTimeStep(self); 61 | 62 | % get all components 63 | self = getAllComponents(self); 64 | 65 | % get all controls 66 | self = self.getControls(); 67 | 68 | % create cyber layer 69 | self = self.createCyberLayer(cybernodes, cyberlinks); 70 | 71 | % close modified .inp file 72 | EpanetHelper.epanetclose(); 73 | end 74 | 75 | function self = getControls(self) 76 | 77 | % get number of controls 78 | nControls = int32(0); 79 | [~,nControls] = ... 80 | calllib('epanet2','ENgetcount',... 81 | EpanetHelper.EPANETCODES('EN_CONTROLCOUNT'),nControls); 82 | 83 | % get total number of actionable components 84 | % (here normal PIPES are not considered actionable) 85 | nComponents =... 86 | numel(EpanetHelper.getComponents('PUMPS')) + ... 87 | numel(EpanetHelper.getComponents('VALVES')) + ... 88 | numel(EpanetHelper.getComponents('OF_PIPES')); 89 | 90 | % retrieve controls 91 | self.controls = []; 92 | for i = 1 : nControls - nComponents 93 | self.controls = cat(1,self.controls,EpanetControl(i)); 94 | end 95 | 96 | % retrieve dummy controls 97 | self.dummyControls = []; 98 | for i = numel(self.controls)+1 : nControls 99 | self.dummyControls = cat(1,self.dummyControls,EpanetControl(i)); 100 | end 101 | end 102 | 103 | function self = deactivateControls(self) 104 | % cycle through controls and deactivate them 105 | for i = 1 : numel(self.controls) 106 | self.controls(i).deactivate(); 107 | end 108 | end 109 | 110 | function patterns = setPatterns(self) 111 | % get number of patterns 112 | n_patterns = int32(0); 113 | [~, n_patterns] = calllib('epanet2', 'ENgetcount',... 114 | EpanetHelper.EPANETCODES('EN_PATCOUNT'), n_patterns); 115 | 116 | if isempty(self.patterns) 117 | % store patterns anyway for PDA (and consistency) 118 | patterns = []; 119 | for i = 1 : n_patterns 120 | pattern = EpanetHelper.getPattern(i); 121 | patterns = cat(2,patterns,pattern'); 122 | end 123 | else 124 | for i = 1 : n_patterns 125 | errorcode = EpanetHelper.setPattern(i, self.patterns(:,i)); 126 | end 127 | self.setDuration() 128 | patterns = self.patterns; 129 | end 130 | end 131 | 132 | function [] = setInitialTankLevels(self) 133 | % exit if initial conditions have not been specified in 134 | % cyberoptions section of .cpa file 135 | if isempty(self.tankIniLevels) 136 | return 137 | end 138 | % cycle through all tanks and set initial level 139 | TANKS = sort(self.components('TANKS')); 140 | EN_TANKLEVEL = EpanetHelper.EPANETCODES('EN_TANKLEVEL'); 141 | for i = 1 : numel(TANKS) 142 | tankIndex = EpanetHelper.getComponentIndex(TANKS{i}); 143 | errorcode = EpanetHelper.setComponentValue(... 144 | tankIndex,self.tankIniLevels(i),EN_TANKLEVEL); 145 | end 146 | end 147 | 148 | end 149 | 150 | 151 | % private methods 152 | methods (Access = private) 153 | 154 | function self = initializeMap(self) 155 | 156 | % open original file 157 | EpanetHelper.epanetloadfile(self.originalFilePath); 158 | 159 | % set modified file path 160 | [~,ix] = regexp(self.originalFilePath,'\.inp','match'); 161 | self.modifiedFilePath = [self.originalFilePath(1:ix),'inpx']; 162 | 163 | % get sections 164 | sections = EpanetHelper.divideInpInSections(self.originalFilePath); 165 | 166 | % if PDA, add dummy components 167 | if self.usePDA 168 | P_min = self.pdaOptions.Pmin; 169 | junctions = EpanetHelper.getComponents('JUNCTIONS'); 170 | [sections,self.pdaDict] = EpanetHelper.addDummyComponents(junctions, sections, P_min); 171 | end 172 | 173 | % add dummy tanks, lines and controls for the tanks which can 174 | % overflow (here all are supposed to possibily overflow). 175 | % TODO: modify this when if you plat to add new sections 176 | % to original .inp file. 177 | 178 | % get tanks that can overflow (all as for now) 179 | tanks = EpanetHelper.getComponents('TANKS'); 180 | sections = EpanetHelper.addDummyTanks(tanks, sections); 181 | 182 | % create temp file 183 | EpanetHelper.createInpFileFromSections(sections,self.modifiedFilePath) 184 | 185 | % close original file 186 | EpanetHelper.epanetclose(); 187 | 188 | % open temp file for inizialization and to in include additional controls 189 | EpanetHelper.epanetloadfile(self.modifiedFilePath); 190 | 191 | % retrieve pdaDict if PDA 192 | % TO DO: check if it can be put right after addDummyComponents 193 | if self.usePDA 194 | self.createPDAdictionary(); 195 | end 196 | % Add additional controls for attack 197 | components = EpanetHelper.getComponents('PUMPS'); 198 | components = cat(1,components,EpanetHelper.getComponents('VALVES')); 199 | components = cat(1,components,EpanetHelper.getComponents('OF_PIPES')); 200 | 201 | attackControls = {}; 202 | for i = 1 : numel(components) 203 | thisControl = sprintf('LINK %-6s CLOSED AT TIME 999999999',components{i}); 204 | attackControls{i,1} = thisControl; 205 | end 206 | 207 | ixControls = find(... 208 | cellfun(@(x) strcmp(x,'[CONTROLS]'),{sections.name})); 209 | sections(ixControls).text = cat(1,sections(ixControls).text,' ',attackControls); 210 | 211 | % close temporary file 212 | EpanetHelper.epanetclose(); 213 | 214 | % add controls to temp file (used for simulations) 215 | EpanetHelper.createInpFileFromSections(sections,self.modifiedFilePath); 216 | 217 | end 218 | 219 | function self = createCyberLayer(self, cybernodes, cyberlinks) 220 | 221 | % CYBERNODES 222 | % check for duplicates 223 | if ~isequal(unique({cybernodes.name}), sort({cybernodes.name})) 224 | error('Duplicate names in cybernodes. Check your .cpa file.') 225 | end 226 | 227 | % get SCADA 228 | scadaIndex = find(ismember({cybernodes.name},'SCADA')); 229 | if isempty(scadaIndex) 230 | scadaIndex = -1; 231 | warning(['SCADA cybernode not found. Adding SCADA node']); 232 | end 233 | 234 | % check PLC names 235 | for i = 1 :numel(cybernodes) 236 | if i~=scadaIndex && strcmp(cybernodes(i).name(1:3),'PLC') == false 237 | error('Name of PLC node must start with "PLC"'); 238 | end 239 | end 240 | 241 | % verify if sensors/actuators are directly connected only to one PLC (or SCADA) 242 | % and all the sensors/actuators in the controls are connected to cybernodes 243 | 244 | % this checks for uniqueness... 245 | if isequal(unique([cybernodes.sensors]), sort([cybernodes.sensors])) && ... 246 | isequal(unique([cybernodes.actuators]), sort([cybernodes.actuators])) 247 | 248 | % ... now get all actuators and sensors in controls 249 | % (add P to water evels and pressures) 250 | sensors = {}; actuators = {}; 251 | for i = 1 : numel(self.controls) 252 | sensors = cat(2,sensors,... 253 | ['P_',EpanetHelper.getComponentId(self.controls(i).nIndex, 1)]); 254 | actuators = cat(2,actuators,... 255 | EpanetHelper.getComponentId(self.controls(i).lIndex, 0)); 256 | end 257 | 258 | % ... and check if are all connected to cybernodes 259 | if isempty(setdiff(unique(sensors), sort([cybernodes.sensors]))) && ... 260 | isempty(setdiff(unique(actuators), sort([cybernodes.actuators]))); 261 | % check if some specified sensors/actuators do not exists 262 | % sensors 263 | sensor_list = sort([cybernodes.sensors]); 264 | for i=1 : numel(sensor_list) 265 | thisSensor = sensor_list{i}; 266 | temp = regexp(thisSensor,'_','split'); 267 | if temp{1} == 'P' 268 | % it's a junction or a tank 269 | if ~ismember(temp{2},cat(1,self.components('JUNCTIONS'),... 270 | self.components('TANKS'))) 271 | error('Problem with %s. Component %s does not exist',thisSensor,temp{2}); 272 | end 273 | elseif ismember(temp{1},['F','S','SE']) 274 | % it's a pump, valve or pipe 275 | if ~ismember(temp{2},cat(1,self.components('PUMPS'),... 276 | self.components('VALVES'),self.components('PIPES'))) 277 | error('Problem with %s. Component %s does not exist',thisSensor,temp{2}); 278 | end 279 | else 280 | error('Variable %s not recognized',temp{1}); 281 | end 282 | end 283 | 284 | % actutators 285 | actuator_list = sort([cybernodes.actuators]); 286 | if min(ismember(actuator_list,... 287 | cat(1,self.components('PUMPS'),... 288 | self.components('VALVES'),... 289 | self.components('PIPES')))) == 0 290 | error('Some actuators do not exist. Check your .cpa file.'); 291 | end 292 | 293 | 294 | fprintf('PLC and controls are consistent. Check PASSED!\n'); 295 | else 296 | error('Some sensors/actuators in the controls are not linked to cybernodes. Check FAILED!'); 297 | end 298 | else 299 | error('Sensors and actuators can only be linked to one PLC or to SCADA. Check FAILED!'); 300 | end 301 | 302 | % if all is ok, then construct cybernodes struct 303 | self.cyberlayer.sensors = unique([cybernodes.sensors]); % sensors 304 | self.cyberlayer.actuators = unique([cybernodes.actuators]); % actuators 305 | 306 | % PLCs 307 | self.cyberlayer.systems = []; 308 | for i = 1 : numel(cybernodes) 309 | if i==scadaIndex, continue, end; 310 | self.cyberlayer.systems = cat(1,self.cyberlayer.systems,PLC(cybernodes(i),self.controls)); 311 | end 312 | 313 | % SCADA (THIS NEEDS TO BE CHECKED!) 314 | if scadaIndex== -1 315 | % SCADA sees only 316 | self.cyberlayer.systems = cat(1,self.cyberlayer.systems,SCADA([],self.controls,self.cyberlayer.sensors)); 317 | else 318 | % SCADA does something more... 319 | self.cyberlayer.systems = cat(1,self.cyberlayer.systems,... 320 | SCADA(cybernodes(scadaIndex),self.controls,self.cyberlayer.sensors)); 321 | end 322 | 323 | % CYBERLINKS 324 | temp = []; 325 | for i = 1 : numel(cyberlinks) 326 | thisLink = cyberlinks(i); 327 | cyberlinkInfo.sender = thisLink.sender; 328 | cyberlinkInfo.receiver = thisLink.receiver; 329 | for j = 1 : numel(thisLink.signals) 330 | cyberlinkInfo.signal = thisLink.signals{j}; 331 | temp = [temp, Cyberlink(cyberlinkInfo)]; 332 | end 333 | end 334 | % check for duplicates 335 | if ~isequal(unique({temp.name}), sort({temp.name})) 336 | error('Duplicate names in cyberlinks. Check your .cpa file.') 337 | end 338 | 339 | self.cyberlayer.cyberlinks = temp; 340 | 341 | % check cybernodes vs cyberlinks 342 | for i = 1:numel(self.cyberlayer.cyberlinks) 343 | % check if senders are correct 344 | thisLink = self.cyberlayer.cyberlinks(i); 345 | ixSender = find(strcmp(thisLink.sender,{self.cyberlayer.systems.name})); 346 | if ~ismember(thisLink.signal,self.cyberlayer.systems(ixSender).sensors) 347 | error('%s does not read %s, so it cannot send it to %s. Check your .cpa file.',... 348 | thisLink.sender,thisLink.signal,thisLink.receiver); 349 | end 350 | 351 | % write sensorsIn (do this better later) 352 | ixReceiver = find(strcmp(thisLink.receiver,{self.cyberlayer.systems.name})); 353 | self.cyberlayer.systems(ixReceiver) =... 354 | self.cyberlayer.systems(ixReceiver).addSensorIn(thisLink.signal); 355 | end 356 | 357 | end 358 | 359 | function self = getAllComponents(self) 360 | % initialize and fill the components dictionary 361 | componentsTypes = {... 362 | 'TANKS','OF_TANKS','JUNCTIONS','OF_JUNCTIONS',... 363 | 'PUMPS','VALVES','PIPES','OF_PIPES','RESERVOIRS'}; 364 | 365 | self.components = containers.Map; 366 | for i = 1 : numel(componentsTypes) 367 | fprintf('%s\n',componentsTypes{i}) 368 | self.components(componentsTypes{i}) = EpanetHelper.getComponents(componentsTypes{i}); 369 | end 370 | 371 | % check that all labels are unique 372 | temp = cat(1,self.components.values); 373 | allComponents = cat(1,temp{:}); 374 | duplicates = setdiff(allComponents,unique(allComponents)); 375 | if ~isempty(duplicates) 376 | dupLabels = duplicates{1}; 377 | for i = 2 : numel(duplicates) 378 | dupLables = strcat(... 379 | dupLabels,sprintf('\t%s',duplicates{i})); 380 | end 381 | error('ERROR: components labels are not unique!!\n%s\n',dupLabels); 382 | end 383 | end 384 | 385 | function [] = setDuration(self) 386 | patStep = single(0); 387 | [~,patStep] = calllib('epanet2', 'ENgettimeparam',... 388 | EpanetHelper.EPANETCODES('EN_PATTERNSTEP'), patStep); 389 | self.duration = size(self.patterns,1) * patStep; 390 | calllib('epanet2', 'ENsettimeparam',... 391 | EpanetHelper.EPANETCODES('EN_DURATION'), single(self.duration)); 392 | end 393 | 394 | function self = getHydraulicTimeStep(self) 395 | h_tstep = single(0); 396 | [~,h_tstep] = calllib('epanet2', 'ENgettimeparam',... 397 | EpanetHelper.EPANETCODES('EN_HYDSTEP'), h_tstep); 398 | self.h_tstep = h_tstep; 399 | end 400 | 401 | function [] = createPDAdictionary(self) 402 | junctions = keys(self.pdaDict); 403 | for i = 1:length(junctions) 404 | thisJunction = junctions{i}; 405 | temp = self.pdaDict(thisJunction); 406 | % get junction, FCV and emitter indexes 407 | temp.ixFCV = EpanetHelper.getComponentIndex(['v',thisJunction]); 408 | temp.ixEmit = EpanetHelper.getComponentIndex(['e',thisJunction]); 409 | % get pattern index 410 | ixPattern = EpanetHelper.getComponentValue(temp.ixJunction,1,EpanetHelper.EPANETCODES('EN_PATTERN')) 411 | temp.ixPattern = ixPattern; 412 | % update (in this away to avoid error "Only one level of 413 | % indexing...) 414 | self.pdaDict(thisJunction) = temp; 415 | end 416 | end 417 | 418 | end 419 | end -------------------------------------------------------------------------------- /epanetCPA/AttackOnCommunication.m: -------------------------------------------------------------------------------- 1 | classdef AttackOnCommunication < CyberPhysicalAttack 2 | % Class for attack targeting a communication channel between cyber components. 3 | 4 | properties 5 | alterMethod % how is the reading altered (DoS, constant, 6 | % offset, custom values, replay attack) 7 | 8 | alteredReading % current altered reading of the sensor 9 | 10 | sender % starting point of the communication, can be 11 | % NULL, a PLC or SCADA 12 | 13 | receiver % ending point of tshe communication, can be 14 | % NULL, a PLC or SCADA 15 | 16 | targetIsSensor % TRUE if target is a node (sensor) 17 | % FALSE if it is a link (actuator). 18 | % TO DO: Ideally this value should be 19 | % stored when calling constructor. At the moment, 20 | % it is stored during validation instead as it 21 | % needs interfacing with list of controllers. 22 | end 23 | 24 | % public methods 25 | methods 26 | 27 | function self = AttackOnCommunication(... 28 | str, ini_condition, end_condition, args) 29 | 30 | % parse str (sender-target-receiver) 31 | temp = regexp(str,'-','split'); 32 | if numel(temp)~= 3 33 | % raise error 34 | error('AttackOnCommunication: need to correctly specify sender-target-receiver.'); 35 | else 36 | sender = temp{1}; target = temp{2}; receiver = temp{3}; 37 | end 38 | 39 | % handle args for alteration method 40 | alterMethod = args{1}; 41 | setting = []; 42 | switch alterMethod 43 | case 'DoS' 44 | if numel(args) > 1 45 | error('Too many arguments for AttackOnCommunication') 46 | end 47 | case 'constant' 48 | % subsitute reading with a constant value 49 | if numel(args) == 2 50 | setting = str2num(args{2}); 51 | else 52 | error('Wrong number of arguments for AttackOnCommunication') 53 | end 54 | case 'offset' 55 | % adds offset to reading 56 | if numel(args) == 2 57 | setting = str2num(args{2}); 58 | else 59 | error('Wrong number of arguments for AttackOnCommunication') 60 | end 61 | case 'custom' 62 | % substitute with custom readings 63 | if numel(args) == 2 64 | % check if file exists 65 | filename = args{2}; 66 | if ~exist(filename, 'file') 67 | error(' AttackOnCommunication: File containing custom altered readings cannot be found!') 68 | end 69 | setting = csvread(filename); 70 | else 71 | error('Wrong number of arguments for AttackOnCommunication') 72 | end 73 | case 'replay' 74 | % replay attack 75 | if numel(args) ~= 5 76 | error('Wrong number of arguments for AttackOnCommunication') 77 | else 78 | setting(1) = str2num(args{2}); 79 | setting(2) = str2num(args{3}); 80 | setting(3) = str2num(args{4}); 81 | setting(4) = str2num(args{5}); 82 | end 83 | otherwise 84 | error('not implemented yet!') 85 | end 86 | 87 | % summon superclass constructor 88 | layer = receiver; 89 | self@CyberPhysicalAttack(... 90 | layer, target, ini_condition, end_condition, setting); 91 | 92 | % store properties 93 | self.setting = setting; 94 | self.sender = sender; 95 | self.receiver = receiver; 96 | self.alterMethod = alterMethod; 97 | 98 | % initialize alteredReading 99 | self.alteredReading = NaN; 100 | end 101 | 102 | function self = performAttack(self, varargin) 103 | % get arguments 104 | epanetSim = varargin{1}; 105 | if self.targetIsSensor 106 | % alter sensor reading 107 | self = self.alterSensorReading(epanetSim); 108 | else 109 | % alter transmission to actuator 110 | self = self.alterTransmissionToActuator(epanetSim); 111 | end 112 | end 113 | 114 | function self = stopAttack(self, varargin) 115 | % get arguments 116 | time = varargin{1}; 117 | 118 | % attack is off 119 | self.inplace = 0; 120 | self.endTime = time; 121 | 122 | % reset altered reading 123 | self.alteredReading = NaN; 124 | self.setting = []; 125 | end 126 | 127 | function [self, epanetSim] = evaluateAttack(self, epanetSim) 128 | if self.inplace == 0 129 | % attack is not active, check if starting condition met 130 | flag = self.evaluateStartingCondition(epanetSim.symbolDict); 131 | if flag 132 | % start attack 133 | self = self.startAttack(epanetSim.symbolDict('TIME'),epanetSim); 134 | if ~self.targetIsSensor 135 | % TO DO: should find a better way to perform dummy 136 | % control activation, outside of this class. 137 | epanetSim.epanetMap.dummyControls(self.actControl).isActive = 1; 138 | end 139 | end 140 | else 141 | % attack is ongoing 142 | % check if ending condition is met for this attack 143 | flag = self.evaluateEndingCondition(epanetSim.symbolDict); 144 | if flag 145 | % stop attack 146 | self = self.stopAttack(epanetSim.symbolDict('TIME')); 147 | else 148 | % ...continue attack (needed for sensor alteration) 149 | self = self.performAttack(epanetSim); 150 | end 151 | end 152 | end 153 | 154 | function self = validateAttack(self, PLCs) 155 | % Validate attacks, i.e. is sensor connected to that PLC? 156 | % TO DO: it shouldn't be a public method. 157 | 158 | % check if sender and receiver are the same 159 | if strcmp(self.sender,self.receiver) == 1 160 | error('AttackOnCommunication: sender and receiver cannot be the same.'); 161 | end 162 | 163 | % check if target exists 164 | if sum(ismember(PLCs.sensors,self.target)) +... 165 | sum(ismember(PLCs.actuators,self.target)) == 0 166 | error('AttackOnCommunication: target %s does not exist.', self.target); 167 | else 168 | self.targetIsSensor = sum(ismember(PLCs.sensors,self.target))==1; 169 | end 170 | 171 | % check if connection is inplace: sender has to be either NULL or have target in sensors; 172 | % receiver has to be either NULL (for actuators) or have sensor in sensorsIn 173 | ixSender = find(ismember({PLCs.systems.name},self.sender)); 174 | ixReceiver = find(ismember({PLCs.systems.name},self.receiver)); 175 | if isempty(ixSender) && ~strcmp(self.sender,'NULL') 176 | error('AttackOnCommunication: sender %s does not exist.', self.sender); 177 | end 178 | 179 | if isempty(ixReceiver) && ~strcmp(self.receiver,'NULL') 180 | error('AttackOnCommunication: receiver %s does not exist.', self.receiver); 181 | end 182 | 183 | if self.targetIsSensor 184 | % target is a sensor 185 | if strcmp(self.sender,'NULL') 186 | % receiver must have sensor in his sensor list 187 | if sum(ismember(PLCs.systems(ixReceiver).sensors,self.target)) == 0 188 | error('AttackOnCommunication: receiver %s is not linked to %s sensor.',... 189 | self.receiver, self.target); 190 | end 191 | else 192 | % receiver must have sensor in his sensorsIn list 193 | if sum(ismember(PLCs.systems(ixReceiver).sensorsIn,self.target)) == 0 194 | error('AttackOnCommunication: receiver %s is not linked to %s sensor.',... 195 | self.receiver, self.target); 196 | % and sender has to read the sensor 197 | elseif sum(ismember(PLCs.systems(ixSender).sensors,self.target)) == 0 198 | error('AttackOnCommunication: sender %s is not directly linked to %s sensor.',... 199 | self.sender, self.target); 200 | end 201 | % TO DO: what to do if target is in sensorsIn of sender? 202 | end 203 | else 204 | % target is an actuator 205 | if strcmp(self.receiver,'NULL') 206 | % sender must have actuator in his actuator list 207 | if sum(ismember(PLCs.systems(ixSender).actuators,self.target)) == 0 208 | error('AttackOnCommunication: sender %s is not linked to %s actuator.',... 209 | self.sender, self.target); 210 | end 211 | 212 | % check if alterMethod is DoS or constant (OFF=0,ON=1,values in between for speed/valve setting); 213 | % other alterMethods are not available when target is an actuator 214 | % TO DO: currently we do not check if "constant" values are between 0 and 1 215 | if sum(ismember({'DoS','constant'},self.alterMethod)) == 0 216 | error(['AttackOnCommunication: outgoing communications',... 217 | 'to actuators can only be DoS-ed or altered with a constant value [0 or 1].']); 218 | elseif strcmp('constant', self.alterMethod) 219 | % check if values are 0 or 1 220 | if self.setting~=0 && self.setting~=1 221 | error(['AttackOnCommunication: outgoing communications',... 222 | ' to actuators can only be altered with 0 or 1']) 223 | end 224 | end 225 | 226 | elseif strcmp(self.receiver,'SCADA') 227 | % we are actually reporting the FLOW of through valve or pump back to SCADA. 228 | % it's a sensor reading, targetIsSensor set to 1 229 | self.targetIsSensor = 1; 230 | 231 | % sender must have actuator in his actuator list 232 | if sum(ismember(PLCs.systems(ixSender).actuators,self.target)) == 0 233 | error('AttackOnCommunication: sender %s is not linked to %s actuator.',... 234 | self.sender, self.target); 235 | end 236 | else 237 | % receiver must be NULL or SCADA 238 | error('AttackOnCommunication: if target is an actuator, receiver can only be NULL or SCADA.') 239 | end 240 | end 241 | end 242 | 243 | end 244 | 245 | % private methods 246 | methods (Access = private) 247 | 248 | function self = alterSensorReading(self, epanetSim) 249 | % get time vector 250 | T = epanetSim.T; 251 | rowToCopyFrom = numel(T); % initialize to current time 252 | 253 | % switch alter method 254 | switch self.alterMethod 255 | 256 | case 'DoS' 257 | % reading is not updated 258 | if isnan(self.alteredReading) 259 | thisReading = getReading(self, rowToCopyFrom, epanetSim); 260 | self.alteredReading = thisReading; 261 | else 262 | % no need to update 263 | end 264 | 265 | case 'constant' 266 | % subsitute reading with a constant value 267 | self.alteredReading = self.setting; 268 | 269 | case 'offset' 270 | % adds offset to reading 271 | thisReading = getReading(self, rowToCopyFrom, epanetSim); 272 | self.alteredReading = thisReading + self.setting; 273 | 274 | case 'custom' 275 | % substitute with custom readings 276 | T = epanetSim.T(end); 277 | ix = find(self.setting(:,1)>=T,1); 278 | if ~isempty(ix) 279 | self.alteredReading = self.setting(ix,2); 280 | else 281 | self.alteredReading = self.setting(end,2); 282 | end 283 | 284 | case 'replay' 285 | % replay attack 286 | 287 | % get parameters 288 | delay = self.setting(1); 289 | noiseIntensity = self.setting(2); 290 | maxValue = self.setting(3); 291 | minValue = self.setting(4); 292 | 293 | sPoint = (self.iniTime-delay); % initial copying point 294 | timeRef = sPoint + mod(T(end)-sPoint,delay); 295 | rowToCopyFrom = find(T>=timeRef,1); 296 | % get past reading to repeat 297 | thisReading = getReading(self, rowToCopyFrom, epanetSim); 298 | % only pressure and water level 299 | delta = noiseIntensity * (2*rand(1)-1); 300 | if thisReading + delta > maxValue 301 | self.alteredReading = maxValue; 302 | elseif thisReading + delta < minValue 303 | self.alteredReading = minValue; 304 | else 305 | self.alteredReading = thisReading + delta; 306 | end 307 | 308 | otherwise 309 | error('not implemented yet!') 310 | end 311 | end 312 | 313 | function self = alterTransmissionToActuator(self, epanetSim) 314 | % get time vector 315 | T = epanetSim.T; 316 | rowToCopyFrom = numel(T); % initialize to current time 317 | 318 | % switch alter method 319 | if strcmp(self.alterMethod,'DoS') && isempty(self.setting) 320 | % DoS 321 | self.setting = getReading(self, rowToCopyFrom, epanetSim)>0; 322 | else 323 | % Otherwise is replaced with constant value (0, 1, or anything in between) 324 | end 325 | % Alter transmission from controller to actuator 326 | % (works like AttackOnActuator) 327 | 328 | % get dummy controls 329 | dummyControls = epanetSim.epanetMap.dummyControls; 330 | 331 | % get attacked component and index 332 | thisIndex = EpanetHelper.getComponentIndex(self.target); 333 | 334 | % activate dummy control, save and exit 335 | for i = 1 : numel(dummyControls) 336 | if dummyControls(i).lIndex == thisIndex 337 | self.actControl = i; 338 | return 339 | end 340 | end 341 | end 342 | 343 | function thisReading = getReading(self, rowToCopyFrom, epanetSim) 344 | time = epanetSim.T(rowToCopyFrom); 345 | 346 | % get attacked component and index 347 | thisComponent = self.target; 348 | % remove prefix 349 | temp = regexp(thisComponent,'_','split'); 350 | thisVariable = temp{1}; 351 | thisComponent = temp{2}; 352 | if ~ismember(thisVariable,'PFS') 353 | error('Attacks targeting %s not implemented yet.',temp{1}); 354 | end 355 | 356 | 357 | 358 | [thisIndex,~,isNode] = EpanetHelper.getComponentIndex(thisComponent); 359 | 360 | if isNode 361 | thisIndex = find(ismember(epanetSim.whatToStore.nodeIdx,thisIndex)); 362 | else 363 | thisIndex = find(ismember(epanetSim.whatToStore.linkIdx,thisIndex)); 364 | end 365 | 366 | % check if variable has been stored, otherwise return error 367 | if isNode 368 | if isempty(epanetSim.readings.PRESSURE) || (sum(ismember(epanetSim.whatToStore.sensors, thisComponent))==0) 369 | error(['Cannot perform attack as PRESSURE(TANK LEVEL) variable for %s is not being stored during the simulation.\n',... 370 | 'Modify .cpa file accordingly and run simulation again.'],thisComponent); 371 | end 372 | else 373 | if isempty(epanetSim.readings.FLOW) || (sum(ismember(epanetSim.whatToStore.sensors, thisComponent))==0) 374 | error(['Cannot perform attack as FLOW variable for %s is not being stored during the simulation.\n',... 375 | 'Modify .cpa file accordingly and run simulation again.'],thisComponent); 376 | end 377 | 378 | end 379 | 380 | % if it has been already modified, return the modified value 381 | % if not, return physical layer reading. 382 | try 383 | ix = find(([epanetSim.alteredReadings.time] == time) & ... 384 | strcmp({epanetSim.alteredReadings.layer},'PLC') & ... 385 | strcmp({epanetSim.alteredReadings.sensorId},thisComponent)); 386 | catch 387 | % TODO: should substitute this catch statement 388 | ix = []; 389 | end 390 | 391 | if ~isempty(ix) 392 | % value has been altered already 393 | thisReading = epanetSim.alteredReadings(ix).reading; 394 | else 395 | % check if it's node (if so return pressure) 396 | if isNode 397 | thisReading = epanetSim.readings.PRESSURE(rowToCopyFrom,thisIndex); 398 | else 399 | % return flow if it's a link 400 | switch thisVariable 401 | case 'F' 402 | thisReading = epanetSim.readings.FLOW(rowToCopyFrom,thisIndex); 403 | case 'S' 404 | thisReading = epanetSim.readings.STATUS(rowToCopyFrom,thisIndex); 405 | case 'SE' 406 | thisReading = epanetSim.readings.SETTING(rowToCopyFrom,thisIndex); 407 | otherwise 408 | error('How did I get here?') 409 | end 410 | end 411 | end 412 | end 413 | 414 | end 415 | 416 | end 417 | -------------------------------------------------------------------------------- /minitown_map.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | 3 | 4 | [JUNCTIONS] 5 | ;ID Elev Demand Pattern 6 | ;NFLOURS Value 7 | ;PRESS_DRIVEN Ref.press Tresh.press 8 | J421 37.09 15.04425 DMA1_pat ; 9 | ; NFLOURS 1 10 | ; PRESS_DRIVEN 14.276 -1.0 11 | J332 44.18 8.609582 DMA1_pat ; 12 | ; NFLOURS 1 13 | ; PRESS_DRIVEN 14.276 -1.0 14 | J156 56.16 21.7867 DMA1_pat ; 15 | ; NFLOURS 1 16 | ; PRESS_DRIVEN 14.276 -1.0 17 | J39 45.88 62.41494 DMA1_pat ; 18 | ; NFLOURS 1 19 | ; PRESS_DRIVEN 14.276 -1.0 20 | J269 56 0 ; 21 | ; NFLOURS 1 22 | ; PRESS_DRIVEN 14.276 -1.0 23 | J273 56 0 ; 24 | ; NFLOURS 1 25 | ; PRESS_DRIVEN 14.276 -1.0 26 | J280 56 0 ; 27 | ; NFLOURS 1 28 | ; PRESS_DRIVEN 14.276 -1.0 29 | J285 56 0 ; 30 | ; NFLOURS 1 31 | ; PRESS_DRIVEN 14.276 -1.0 32 | 33 | [RESERVOIRS] 34 | ;ID Head Pattern 35 | R1 59 ; 36 | 37 | [TANKS] 38 | ;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve 39 | TANK 71.5 3 0 6.5 31.3 0 ; 40 | 41 | [PIPES] 42 | ;ID Node1 Node2 Length Diameter Roughness MinorLoss Status 43 | ; SIZINGDATA Bulk coeff. Wall coeff. Diam. Zone Material Is Sizable D. Coeff. Demand Patt Loss Coeff. Loss Patt 44 | P15 J39 TANK 395.29 406 90.48302 0 open ; 45 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 46 | P310 J269 J156 9 610 87.26803 0 open ; 47 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 48 | P316 R1 J280 11.62 610 70.45195 0 open ; 49 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 50 | P320 J273 J269 11.47 610 76.51881 0 open ; 51 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 52 | P322 J280 J285 11.5 610 89.43312 0 open ; 53 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 54 | New82 J156 J421 4296.2 433.2777 110 0 open ; 55 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 56 | New95 J332 J39 698.54 228.8557 110 0 open ; 57 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 58 | New110 J156 J332 5910.1 146.4083 110 0 open ; 59 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 60 | New112 J421 J332 2527.37 90.10659 110 0 open ; 61 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 62 | New113 J421 J39 3205.47 551.204 110 0 open ; 63 | ; SIZINGDATA "" "" 0 Yes 1.0 "" 1.0 "" ;Used in Network Sizing 64 | 65 | [PUMPS] 66 | ;ID Node1 Node2 Parameters 67 | PUMP1 J285 J273 HEAD 8 ; 68 | PUMP2 J280 J269 HEAD 8 ; 69 | 70 | [VALVES] 71 | ;ID Node1 Node2 Diameter Type Setting MinorLoss 72 | 73 | [TAGS] 74 | 75 | [DEMANDS] 76 | ;Junction Demand Pattern Category 77 | J421 15.04425 DMA1_pat ; 78 | J421 1 J14 ; 79 | J421 0 ;Pipe: New82 80 | J421 0 ;Loss: New82 81 | J421 0 ;Pipe: New112 82 | J421 0 ;Loss: New112 83 | J421 0 ;Pipe: New113 84 | J421 0 ;Loss: New113 85 | J332 8.609582 DMA1_pat ; 86 | J332 1 J301 ; 87 | J332 0 ;Pipe: New95 88 | J332 0 ;Loss: New95 89 | J332 0 ;Pipe: New110 90 | J332 0 ;Loss: New110 91 | J332 0 ;Pipe: New112 92 | J332 0 ;Loss: New112 93 | J156 21.7867 DMA1_pat ; 94 | J156 0 ;Pipe: P310 95 | J156 0 ;Loss: P310 96 | J156 0 ;Pipe: New82 97 | J156 0 ;Loss: New82 98 | J156 0 ;Pipe: New110 99 | J156 0 ;Loss: New110 100 | J39 62.41494 DMA1_pat ; 101 | J39 0 ;Pipe: P15 102 | J39 0 ;Loss: P15 103 | J39 0 ;Pipe: New95 104 | J39 0 ;Loss: New95 105 | J39 0 ;Pipe: New113 106 | J39 0 ;Loss: New113 107 | J269 0 ; 108 | J269 0 ;Pipe: P310 109 | J269 0 ;Loss: P310 110 | J269 0 ;Pipe: P320 111 | J269 0 ;Loss: P320 112 | J273 0 ; 113 | J273 0 ;Pipe: P320 114 | J273 0 ;Loss: P320 115 | J280 0 ; 116 | J280 0 ;Pipe: P316 117 | J280 0 ;Loss: P316 118 | J280 0 ;Pipe: P322 119 | J280 0 ;Loss: P322 120 | J285 0 ; 121 | J285 0 ;Pipe: P322 122 | J285 0 ;Loss: P322 123 | 124 | ; ---------------------- JUNCS LEAKAGES ---------------------- 125 | ; ---------------------- PIPES LEAKAGES ---------------------- 126 | J39 0 ; LeakP: P15 0.0 0.0 0.0 0.0 127 | J269 0 ; LeakP: P310 0.0 0.0 0.0 0.0 128 | J280 0 ; LeakP: P316 0.0 0.0 0.0 0.0 129 | J273 0 ; LeakP: P320 0.0 0.0 0.0 0.0 130 | J280 0 ; LeakP: P322 0.0 0.0 0.0 0.0 131 | J156 0 ; LeakP: New82 0.0 0.0 0.0 0.0 132 | J332 0 ; LeakP: New95 0.0 0.0 0.0 0.0 133 | J156 0 ; LeakP: New110 0.0 0.0 0.0 0.0 134 | J421 0 ; LeakP: New112 0.0 0.0 0.0 0.0 135 | J421 0 ; LeakP: New113 0.0 0.0 0.0 0.0 136 | [STATUS] 137 | ;ID Status/Setting 138 | PUMP1 Closed 139 | 140 | [PATTERNS] 141 | ;ID Multipliers 142 | ; 143 | DMA1_pat 0.5691504 0.46466868 0.43854825 0.3604161 0.309779115 0.334524795 144 | DMA1_pat 0.384703515 0.471542475 0.55013289 0.680505915 0.70364769 0.67088259 145 | DMA1_pat 0.79690221 0.719915685 0.770552655 0.73847493 0.81477408 0.72426909 146 | DMA1_pat 0.806983785 0.779259465 0.866785815 0.740766195 0.67042434 0.632389335 147 | DMA1_pat 0.52252044 0.506709855 0.445588455 0.45465063 0.47547432 0.53389635 148 | DMA1_pat 0.662501925 0.62239704 0.67599876 0.72593706 0.803061855 0.70144995 149 | DMA1_pat 0.69335184 0.653825385 0.621432975 0.65228289 0.571880295 0.57901434 150 | DMA1_pat 0.58209933 0.711668985 0.70453494 0.665779725 0.741747645 0.692194965 151 | DMA1_pat 0.72708954 0.619247205 0.46304505 0.45095511 0.309260895 0.282421215 152 | DMA1_pat 0.433303785 0.40670589 0.545014905 0.69928266 0.739904895 0.6434271 153 | DMA1_pat 0.680905935 0.691061505 0.67727895 0.626259375 0.626742975 0.7142742 154 | DMA1_pat 0.681873135 0.788264685 0.75779802 0.788264685 0.802047225 0.845571045 155 | DMA1_pat 0.604884255 0.443633715 0.5131584 0.36476118 0.31782714 0.463887405 156 | DMA1_pat 0.39572595 0.64149669 0.671098245 0.668761275 0.79456785 0.674603685 157 | DMA1_pat 0.754255215 0.808394895 0.713747835 0.68570427 0.68122509 0.549576105 158 | DMA1_pat 0.68219883 0.569829795 0.74334939 0.832154025 0.67888812 0.746270595 159 | DMA1_pat 0.601325505 0.503529285 0.51908778 0.35946171 0.29924835 0.453216765 160 | DMA1_pat 0.42957594 0.539495655 0.896330625 0.89673474 0.682552935 0.57990732 161 | DMA1_pat 0.428969775 0.4200792 0.62941161 0.691039395 0.670631505 0.618904575 162 | DMA1_pat 0.682552935 0.832884315 0.90279648 0.821771115 0.84945309 0.691039395 163 | DMA1_pat 0.716292135 0.54775281 0.53349507 0.419645895 0.35601804 0.33814266 164 | DMA1_pat 0.45241743 0.509874015 0.56456418 0.760767795 0.62159517 0.67820055 165 | DMA1_pat 0.6928839 0.639257745 0.619892745 0.659686755 0.65628192 0.631809675 166 | DMA1_pat 0.604145385 0.70394961 0.772471905 0.88546986 0.88334184 0.752042895 167 | DMA1_pat 0.71409849 0.457925235 0.446544345 0.42936957 0.40184853 0.43309422 168 | DMA1_pat 0.469926885 0.52227894 0.590771145 0.754862745 0.76996827 0.644778585 169 | DMA1_pat 0.73210098 0.70954614 0.548144565 0.577734855 0.592426545 0.624499935 170 | DMA1_pat 0.55973238 0.766864395 0.716995455 0.77452062 0.90446958 0.85749759 171 | ; 172 | J301 65.9725 64.445 63.5875 35.8425 35.9075 15.92 173 | J301 32.01 30.79 0 24.63 50.03 67.525 174 | J301 67.32 67.225 66.91 66.94 67.2825 66.1775 175 | J301 64.835 64.8275 32.1475 31.68 31.9375 39.5925 176 | J301 63.385 55.935 35.9925 42.9275 64.075 31.29 177 | J301 31.1175 15.71 16.3175 57.915 66.0275 66.015 178 | J301 65.76 64.92 64.4275 64.035 55.8275 30.9925 179 | J301 7.6975 8.07 32.4475 58.6975 66.8575 66.915 180 | J301 66.725 66.475 65.4325 57.105 0 0 181 | J301 16.165 32.2875 31.2275 7.9275 49.4925 66.3525 182 | J301 65.885 65.1475 64.97 64.01 63.4425 63.875 183 | J301 31.165 31.845 31.565 49.04 65.5 64.645 184 | J301 64.615 56.305 36.19 41.6825 31.4575 22.57 185 | J301 0 16.43 33.035 58.9 66.855 66.515 186 | J301 66.87 66.7925 65.9025 65.3975 65.2725 23.7925 187 | J301 8.18 32.775 32.84 49.59 66.6425 66.185 188 | J301 65.945 64.9975 64.26 33.8875 0 16.4575 189 | J301 32.8475 32.205 32.5 58.49 66.88 66.24 190 | J301 43.945 36.5825 57.48 40.2975 31.6275 31.41 191 | J301 31.44 48.1 64.3625 64.7425 65.475 65.0175 192 | J301 64.4375 63.665 49.1725 26.6875 23.6475 30.945 193 | J301 15.14 0 32.21 49.6575 66.33 65.49 194 | J301 66.3275 66.12 66.2075 51.4175 26.6475 32.6975 195 | J301 32.385 32.285 49.5375 66.7425 66.63 66.54 196 | J301 64.9625 63.9675 56.6525 18.05 16.0125 32.11 197 | J301 31.0425 0 16.6225 67.6275 67.395 66.9 198 | J301 66.295 65.835 64.3425 63.6075 39.35 15.53 199 | J301 8.065 32.34 57.205 65.3975 65.485 65.69 200 | ; 201 | J14 98.2825 103.6625 102.3575 93.75 80.8075 89.0825 202 | J14 89.3975 70.69 78.005 80.1625 77.48 19.4475 203 | J14 0.83 0.75 0.8 0.77 0.85 64.975 204 | J14 79.7075 76.9425 78.3875 78.3625 77.715 78.1375 205 | J14 74.5125 78.7225 88.735 70.6425 75.34 79.045 206 | J14 72.3025 65.3675 73.96 69.6175 70.595 72.645 207 | J14 73.1775 74.255 75.13 70.61 70.6425 71.71 208 | J14 63.335 14.5825 0.73 0.69 0.77 0.72 209 | J14 0.75 27.1325 103.6575 89.025 84.375 86.6 210 | J14 92.6575 82.28 73.765 83.62 40.47 0.67 211 | J14 0.71 0.72 0.7 0.65 44.92 81.595 212 | J14 69.31 78.6475 76.3175 68.9225 65.615 63.4025 213 | J14 86.325 88.4875 87.06 84.1625 73.625 66.1925 214 | J14 83.6225 76.2675 73.385 71.19 68.0875 72.515 215 | J14 64.1775 68.9475 71.1 70.3325 67.595 71.945 216 | J14 70.8325 31.8275 0.77 0.86 0.7 0.77 217 | J14 0.62 0.52 52.6125 101.6925 91.01 80.435 218 | J14 93.89 91.0125 85.54 80.825 86.505 87.6775 219 | J14 85.405 79.5575 58.22 0.72 0.7 0.64 220 | J14 0.71 0.86 0.94 0.85 0.88 67.08 221 | J14 86.2525 89.0725 84.4375 86.445 84.65 68.9275 222 | J14 60.8125 74.85 68.8075 58.3325 66.585 68.345 223 | J14 71.7225 74.5025 75.1775 71.5475 72.465 71.6675 224 | J14 71.565 69.17 66.3175 67.135 67.6975 70.3125 225 | J14 71.3875 71.965 56.9875 74.5925 75.1425 43.5625 226 | J14 0.49 0.54 0.61 0.78 0.8 0.67 227 | J14 0.76 0.74 109.53 99.2375 97.77 95.975 228 | J14 91.245 76.5525 86.11 85.8775 83.2575 83.2675 229 | J14 22.08 230 | 231 | [CURVES] 232 | ;ID X-Value Y-Value 233 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 234 | 6 16.88889 156.7 235 | 6 19.5 146.5 236 | 6 22.13889 136.2 237 | 6 25.94445 117.9 238 | 6 33.33334 50 239 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 240 | 7 18.25 75 241 | 7 23.38889 64 242 | 7 30.63889 46 243 | 7 38.88889 0 244 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 245 | 4 30.12222 221.76 246 | 4 37.63612 202.41 247 | 4 43.73056 182.16 248 | 4 47.22223 160 249 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 250 | 5 31.91389 217.27 251 | 5 37.87778 201.91 252 | 5 42.55278 187.15 253 | 5 50 140 254 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 255 | 2 9.888889 72.2 256 | 2 12.52778 62.3 257 | 2 13.33333 57.9 258 | 2 18.05556 0 259 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 260 | 3 11.30278 76.59 261 | 3 12.65833 68.19 262 | 3 14.03056 60.29 263 | 3 18.05556 0 264 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 265 | 1 3.997222 64.86 266 | 1 41.90556 48.48 267 | 1 56.13611 43.96 268 | 1 69.46667 39.56 269 | 1 77.29445 34.53 270 | 1 97.66111 28.83 271 | 1 138.8889 1 272 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 273 | 8 0 70 274 | 8 60 50 275 | 8 100 30 276 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 277 | 9 0 90 278 | 9 30 70 279 | 9 50 30 280 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 281 | 10 0 120 282 | 10 30 110 283 | 10 70 30 284 | ;PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: PUMP: 285 | 11 0 90 286 | 11 30 50 287 | 11 40 10 288 | 289 | [CONTROLS] 290 | ; ----------- Tank 1 --------------- 291 | LINK PUMP1 OPEN IF NODE TANK BELOW 4 292 | LINK PUMP1 CLOSED IF NODE TANK ABOVE 6.3 293 | 294 | LINK PUMP2 OPEN IF NODE TANK BELOW 1 295 | LINK PUMP2 CLOSED IF NODE TANK ABOVE 4.5 296 | 297 | 298 | 299 | 300 | 301 | [RULES] 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | [ENERGY] 325 | Global Efficiency 70.0000 326 | Global Price 0 327 | Demand Charge 0.0000 328 | Pump PUMP1 Price 1.0000 329 | Pump PUMP2 Price 1.0000 330 | 331 | [EMITTERS] 332 | ;Junction Coefficient 333 | 334 | [QUALITY] 335 | ;Node InitQual 336 | 337 | [SOURCES] 338 | ;Node Type Quality Pattern 339 | 340 | [REACTIONS] 341 | ;Type Pipe/Tank Coefficient 342 | 343 | 344 | [REACTIONS] 345 | Order Bulk 1.00 346 | Order Tank 1.00 347 | Order Wall 1 348 | Global Bulk 0.000000 349 | Global Wall 0.000000 350 | Limiting Potential 0 351 | Roughness Correlation 0 352 | 353 | [MIXING] 354 | ;Tank Model 355 | 356 | [TIMES] 357 | Duration 168:00 358 | Hydraulic Timestep 0:15 359 | Quality Timestep 0:05 360 | Pattern Timestep 1:00 361 | Pattern Start 0:00 362 | Report Timestep 0:15 363 | Report Start 0:00 364 | Start ClockTime 0:00:00 365 | Statistic NONE 366 | 367 | [REPORT] 368 | Status No 369 | Summary No 370 | Page 0 371 | 372 | [OPTIONS] 373 | Units LPS 374 | Headloss H-W 375 | Specific Gravity 1.000000 376 | Viscosity 1.000000 377 | Trials 100 378 | Accuracy 0.01000000 379 | CHECKFREQ 2 380 | MAXCHECK 10 381 | DAMPLIMIT 0 382 | Unbalanced Continue 10 383 | Pattern 1 384 | Demand Multiplier 1 385 | Emitter Exponent 0.5000 386 | Quality NONE mg/L 387 | Diffusivity 1.000000 388 | Tolerance 0.01000000 389 | 390 | [COORDINATES] 391 | ;Node X-Coord Y-Coord 392 | J421 -246663.51 148880.20 393 | J332 -247095.24 149459.02 394 | J156 -247645.59 148633.50 395 | J39 -245980.32 149691.49 396 | J269 -248385.72 148495.91 397 | J273 -248385.72 148339.35 398 | J280 -248635.39 148489.98 399 | J285 -248637.17 148334.60 400 | R1 -249268.17 148491.17 401 | TANK -244675.61 150607.16 402 | 403 | [VERTICES] 404 | ;Link X-Coord Y-Coord 405 | P15 -245116.84 150118.49 406 | New110 -248067.84 149302.45 407 | New113 -245633.98 149141.15 408 | 409 | [LABELS] 410 | ;X-Coord Y-Coord Label & Anchor Node 411 | -249234.96 148633.50 "Source" R1 412 | -248770.01 148253.95 "Pumping Station S1" J285 413 | -244561.75 150725.77 "TANK" TANK 414 | 415 | [BACKDROP] 416 | DIMENSIONS -249469.32 147238.65 -245209.84 151508.59 417 | UNITS Meters 418 | FILE 419 | OFFSET 0.00 0.00 420 | 421 | [END] 422 | -------------------------------------------------------------------------------- /epanetCPA/EpanetCPA.m: -------------------------------------------------------------------------------- 1 | 2 | classdef EpanetCPA 3 | % Main class for epanetCPA. 4 | 5 | properties 6 | simulation % EpanetCPASimulation instance; 7 | 8 | inpFile % .inp file of epanet map; 9 | 10 | cpaFile % .cpa file with info on cyber layer and attacks; 11 | 12 | cybernodes % stores info on cyber network of sensors, actuators, plcs and SCADA 13 | cyberlinks % stores info on links between cybernodes 14 | cyberattacks % list of cyber-physical attacks featured in the simulation 15 | cyberoptions % options for epanetCPA run 16 | end 17 | 18 | 19 | % public methods 20 | methods 21 | 22 | function self = EpanetCPA(inpFile, cpaFile, varargin) 23 | % Constructor class for EpanetCPASimulation. 24 | % 25 | % Usage: 26 | % 27 | % EpanetCPA(inpFile, cpaFile, varargin) 28 | % 29 | % where 30 | % 31 | % > inpFile is the .inp file for creating the EpanetCPAMap object 32 | % > cpaFile is the .cpa file containing the CPA additional parameters 33 | % > varargin{1} is a boolean for specifying if simulation has attacks 34 | % > varargin{2} is a boolean for enabling PDA simulations 35 | % 36 | % If varargin is empty, attacks and PDA are inferred from .cpa file 37 | % 38 | % Returns an EpanetCPA object. 39 | % 40 | 41 | % store input files 42 | self.inpFile = inpFile; 43 | self.cpaFile = cpaFile; 44 | 45 | % only want 2 optional inputs at most 46 | numvarargs = length(varargin); 47 | if (numvarargs ~= 0) && (numvarargs ~= 2) 48 | error('EpanetCPA requires at most 2 optional inputs'); 49 | end 50 | 51 | % parse .cpa file 52 | [self, optargs] = self.readCpaFile(); 53 | 54 | % overwrite the optargs specified in varargin. 55 | optargs(1:numvarargs) = varargin; 56 | 57 | % get flags from optional args 58 | [ATTACKS_ENABLED, PDA_ENABLED] = optargs{:}; 59 | 60 | % create Map 61 | theMap = EpanetCPAMap(inpFile, self.cybernodes, self.cyberlinks, self.cyberoptions, PDA_ENABLED); 62 | 63 | if ATTACKS_ENABLED 64 | % create and run simulation with attacks 65 | self.simulation = EpanetCPASimulation(... 66 | theMap,self.cyberattacks,self.cyberoptions); 67 | else 68 | % create and run simulation with no attacks 69 | self.simulation = EpanetCPASimulation(... 70 | theMap,[],self.cyberoptions); 71 | end 72 | end 73 | 74 | function self = run(self) 75 | self.simulation = self.simulation.run(); 76 | end 77 | 78 | function [] = outputResults(self, filename_prefix) 79 | 80 | % Ground truth values 81 | filename = [filename_prefix,'.csv']; 82 | fprintf('Writing %s (ground truth).\n',filename) 83 | % get header and data 84 | [header,table] = self.prepareDataForOutput(); 85 | % write header to file 86 | fid = fopen(filename,'w'); 87 | fprintf(fid,'%s\n',header); 88 | fclose(fid); 89 | % write data to end of file 90 | dlmwrite(filename,table,'-append','precision','%10.4f'); 91 | 92 | % If any, write altered readings 93 | if ~isempty(self.simulation.alteredReadings) 94 | filename = [filename_prefix,'_altered_readings.csv']; 95 | fprintf('Writing %s (altered readings).\n',filename) 96 | % open file 97 | fid = fopen(filename,'w'); 98 | % write header to file 99 | header = {'timestamp','layer','sensor','reading','variable'}; 100 | % rework header 101 | header = [header;repmat({','},1,numel(header))]; 102 | header = header(:)'; 103 | header = cell2mat(header(1:end-1)); 104 | fprintf(fid,'%s\n',header); 105 | % write data 106 | for i = 1 : size(self.simulation.alteredReadings,1) 107 | thisEntry = self.simulation.alteredReadings(i); 108 | line = {num2str(thisEntry.time),... 109 | thisEntry.layer, thisEntry.sensorId,... 110 | num2str(thisEntry.reading),thisEntry.variable}; 111 | % rework header 112 | line = [line;repmat({','},1,numel(line))]; 113 | line = line(:)'; 114 | line = cell2mat(line(1:end-1)); 115 | fprintf(fid,'%s\n',line); 116 | end 117 | fclose(fid); 118 | end 119 | end 120 | 121 | % end of public methods 122 | end 123 | 124 | % private methods 125 | methods (Access = private) 126 | 127 | % this reads network.cpa file 128 | function [self, optargs] = readCpaFile(self) 129 | % reads content 130 | fileId = fopen(self.cpaFile); 131 | sections = []; 132 | thisSection = []; 133 | while ~feof(fileId) 134 | thisLine = fgetl(fileId); 135 | temp = regexp(thisLine,'\[(.*?)\]','tokens'); 136 | if ~isempty(temp) 137 | section_name = temp{1}{1}; 138 | 139 | % add previous section 140 | sections = cat(1,sections,thisSection); 141 | 142 | % start of a section 143 | thisSection.name = section_name; 144 | thisSection.text = {}; 145 | else 146 | % add text to section 147 | thisSection.text = cat(1,thisSection.text,thisLine); 148 | end 149 | end 150 | % add last section 151 | sections = cat(1,sections,thisSection); 152 | fclose(fileId); 153 | 154 | % iterate sections and retrieve cyber info 155 | for i = 1 : numel(sections) 156 | % TO DO: insert control if some sections are not specified! 157 | section_name = sections(i).name; 158 | section_text = sections(i).text; 159 | switch section_name 160 | case 'CYBERNODES' 161 | 162 | % TO DO: should IDEALLY raise error if first line doesn't start with ; (header) 163 | 164 | if size(section_text,1) == 1 165 | % EXIT< no cybernodes have been defined 166 | error('ERROR: no cybernodes defined in %s. Did you include the header for this section?', cpaFile) 167 | end 168 | 169 | % TO DO: should IDEALLY initialize cybernodes structarray 170 | 171 | % initialize array of cybernodes 172 | cybernodes = []; 173 | % loop through all cybernodes 174 | for j = 2 : size(section_text,1) 175 | 176 | % check if comment first... 177 | temp = strtrim(section_text{j}); 178 | if temp(1)~= ';' 179 | % get \t separator positions 180 | temp = regexp(section_text{j},','); 181 | nsep = numel(temp); 182 | if isempty(temp) 183 | error('Cybernode string %d in %s file has no details.', j-1, self.cpaFile); 184 | elseif nsep > 2 185 | error('Problem with format of cybernode string %d in %s file. Check README.md',... 186 | j-1, self.cpaFile); 187 | else 188 | 189 | % initialize cybernode struct 190 | thisNode.name = self.cleanField(section_text{j}(1:temp(1))); 191 | thisNode.sensors = {}; 192 | thisNode.actuators = {}; 193 | 194 | % extend separator array if < 3 to avoid error in 195 | % following section 196 | if nsep < 2 197 | temp(nsep+1:3) = length(section_text{j}); 198 | end 199 | 200 | % get sensors... 201 | temp_ = self.cleanField(section_text{j}(temp(1):temp(2))); 202 | if ~iscell(temp_) 203 | temp_ = {temp_}; 204 | end 205 | thisNode.sensors = temp_; 206 | 207 | % ... actuators... 208 | temp_ = self.cleanField(section_text{j}(temp(2):end)); 209 | if ~isempty(temp_) 210 | if ~iscell(temp_) 211 | temp_ = {temp_}; 212 | end 213 | thisNode.actuators = temp_; 214 | end 215 | 216 | % concatenate 217 | cybernodes = cat(1,cybernodes,thisNode); 218 | end 219 | end 220 | end 221 | 222 | case 'CYBERLINKS' 223 | 224 | if size(section_text,1) == 1 225 | % EXIT< no cyberlinks have been defined 226 | error('ERROR: no cyberlinks defined in %s. Did you include the header for this section?', cpaFile) 227 | end 228 | 229 | % initialize array of cyberlinks 230 | cyberlinks = []; 231 | % loop through all cybernodes 232 | for j = 2 : size(section_text,1) 233 | 234 | % check if comment first... 235 | temp = strtrim(section_text{j}); 236 | if temp(1)~= ';' 237 | % get \t separator positions 238 | temp = regexp(section_text{j},','); 239 | nsep = numel(temp); 240 | if isempty(temp) 241 | error('Cyberlink string %d in %s file has no details.', j-1, self.cpaFile); 242 | elseif nsep > 2 243 | error('Problem with format of cyberlink string %d in %s file. Check README.md',... 244 | j-1, self.cpaFile); 245 | else 246 | 247 | % fill cyberlink struct 248 | thisLink.sender = self.cleanField(section_text{j}(1:temp(1))); 249 | thisLink.receiver = self.cleanField(section_text{j}(temp(1):temp(2))); 250 | 251 | temp_ = self.cleanField(section_text{j}(temp(2):end)); 252 | if ~iscell(temp_) 253 | temp_ = {temp_}; 254 | end 255 | thisLink.signals = temp_; 256 | 257 | % concatenate 258 | cyberlinks = cat(1,cyberlinks,thisLink); 259 | end 260 | end 261 | end 262 | 263 | case 'CYBERATTACKS' 264 | % TO DO: should IDEALLY raise error if first line doesn't start with ; (header) 265 | if size(section_text,1) == 0 266 | % EXIT< no cyberattacks have been defined 267 | warning('WARNING: no cyberattacks defined in %s. Did you include the header for this section?', cpaFile) 268 | end 269 | 270 | % initialize 271 | cyberattacks = []; 272 | 273 | % TO DO: should IDEALLY initialize cyberattacks structarray 274 | % loop through all cyberattacks 275 | for j = 2 : size(section_text,1) 276 | % check if comment first... 277 | temp = strtrim(section_text{j}); 278 | if temp(1)~= ';' 279 | % get \t separator positions 280 | temp = regexp(section_text{j},','); 281 | nsep = numel(temp); 282 | if nsep ~= 4 283 | disp(section_text{j}); 284 | error('Problem with format of cyberattack %d string in %s file', j-1, self.cpaFile); 285 | end 286 | 287 | % fill cyberattack struct 288 | text = section_text{j}; 289 | 290 | % type 291 | thisAttack.type = self.cleanField(strtrim(text(1:temp(1)))); 292 | 293 | % target 294 | thisAttack.target = self.cleanField(text(temp(1):temp(2))); 295 | 296 | % conditions 297 | thisAttack.init_cond = self.cleanField(strtrim(text(temp(2):temp(3)))); 298 | thisAttack.end_cond = self.cleanField(strtrim(text(temp(3):temp(4)))); 299 | 300 | % get arguments (space separated) 301 | thisAttack.arguments = {}; 302 | args = self.cleanField(text(temp(4):end)); 303 | if ~iscell(args) 304 | % single argument 305 | thisAttack.arguments = {args}; 306 | else 307 | for k = 1 : numel(args) 308 | thisAttack.arguments(k) = strtrim(args(k)); 309 | end 310 | end 311 | 312 | % copncatenate 313 | cyberattacks = cat(1,cyberattacks,thisAttack); 314 | end 315 | end 316 | 317 | case 'CYBEROPTIONS' 318 | % define default values 319 | cyberoptions.verbosity = 1; 320 | cyberoptions.what_to_store = {{'everything'},{},{}}; 321 | cyberoptions.initial_conditions = []; 322 | cyberoptions.patterns = []; 323 | cyberoptions.pda_options = []; 324 | 325 | 326 | % loop through all cyberoptions 327 | for j = 1 : size(section_text,1) 328 | 329 | % get \t separator positions 330 | temp = regexp(section_text{j},','); 331 | if isempty(temp) 332 | error('Problem with format of cyberoption string #%d in %s', j, self.cpaFile); 333 | end 334 | 335 | nsep = numel(temp); 336 | text = section_text{j}; 337 | option = self.cleanField(text(1:temp(1))); 338 | 339 | % check if comment first... 340 | temp_ = strtrim(text); 341 | if temp_(1)~=';' 342 | switch option 343 | case 'verbosity' 344 | % after how many steps do you want echo on 345 | % screen? 346 | cyberoptions.verbosity = str2num(text(temp(1):end)); 347 | case 'what_to_store' 348 | % which nodes/links and variables to store? 349 | % if nsep == 1 350 | % cyberoptions.what_to_store(1) = text(temp(1):end); 351 | % elseif nsep == 2 352 | % % all links or all nodes 353 | % cyberoptions.what_to_store(1) = text(temp(1):temp(2)); 354 | % cyberoptions.what_to_store(2) = text(temp(2)+1:end); 355 | % 356 | % elseif nsep == 3 357 | temp = regexp(text(temp(1):end),',','split'); 358 | temp(1) = []; 359 | for k = 1 : numel(temp) 360 | temp_ = self.cleanField(temp{k}); 361 | if ~iscell(temp_) 362 | % handle single case 363 | temp_ = {temp_}; 364 | end 365 | cyberoptions.what_to_store{k} = temp_; 366 | end 367 | % else 368 | % error('Error in what_to_store option format. Check README.md') 369 | % end 370 | case 'initial_conditions' 371 | % initial tank conditions 372 | % set of n comma separated values, where n 373 | % in the number of tanks in the network 374 | if nsep > 1 375 | error('Problem with format of %s cyberoption.',option); 376 | else 377 | temp = self.cleanField(text(temp(1):end)); 378 | 379 | if ~iscell(temp) 380 | % handle single tank case 381 | temp = {temp}; 382 | end 383 | 384 | for k = 1 : numel(temp) 385 | cyberoptions.initial_conditions(k) = str2num(temp{k}); 386 | end 387 | end 388 | case 'patterns_file' 389 | % Absolute path of csv file containing data 390 | % patterns. The file has number of rows 391 | % equal to the total steps of the 392 | % simulation and number of columns equal to 393 | % the number of patterns specified in the 394 | % .inp file. 395 | if nsep > 1 396 | error('Problem with format of %s cyberoption.',option); 397 | else 398 | filename = self.cleanField(text(temp(1):end)); 399 | cyberoptions.patterns = csvread(filename); 400 | end 401 | case 'pda_options' 402 | if nsep ~= 1 403 | error('Problem with format of %s cyberoption.',option); 404 | else 405 | temp_ = self.cleanField(text(temp(1):end)); 406 | pda_options.emitterExponent = str2num(temp_{1}); 407 | pda_options.Pmin = str2num(temp_{2}); 408 | pda_options.Pdes = str2num(temp_{3}); 409 | pda_options.HFR = temp_{4}; 410 | cyberoptions.pda_options = pda_options; 411 | end 412 | otherwise 413 | if option(1)~= ';' 414 | error('Option %s not recognized!',option) 415 | end 416 | end 417 | end 418 | end 419 | otherwise 420 | error('Section %s not recognized!',section_name) 421 | end 422 | end 423 | 424 | % create cell array of attacks 425 | nAttacks = size(cyberattacks,1); 426 | attacks = cell(1,nAttacks); 427 | for i = 1 : nAttacks 428 | thisAttack = cyberattacks(i); 429 | eval_string = sprintf(['attacks{%d} = AttackOn%s('... 430 | 'thisAttack.target,thisAttack.init_cond,thisAttack.end_cond,thisAttack.arguments);'],i,thisAttack.type); 431 | evalc(eval_string); 432 | end 433 | cyberattacks = attacks; 434 | 435 | % modify cyberoptions.what_to_store for EpanetCPASimulation 436 | whatToStore.sensors = cyberoptions.what_to_store{1}; 437 | whatToStore.nodeVars = cyberoptions.what_to_store{2}; 438 | whatToStore.linkVars = cyberoptions.what_to_store{3}; 439 | cyberoptions.what_to_store = whatToStore; 440 | 441 | % return 442 | self.cybernodes = cybernodes; 443 | self.cyberlinks = cyberlinks; 444 | self.cyberattacks = cyberattacks; 445 | self.cyberoptions = cyberoptions; 446 | optargs = {~isempty(cyberattacks),~isempty(cyberoptions.pda_options)}; 447 | end 448 | 449 | function [header,table] = prepareDataForOutput(self) 450 | % initialize 451 | sim = self.simulation; 452 | header = {'timestamp'}; % maybe add time of the day? 453 | table = cat(2, sim.T); 454 | 455 | % do nodes 456 | if ~isempty(sim.whatToStore.nodeIdx) 457 | for i=1:numel(sim.whatToStore.nodeVars) 458 | thisVar = sim.whatToStore.nodeVars{i}; 459 | for j = 1 : numel(sim.whatToStore.nodeID) 460 | thisNode = sim.whatToStore.nodeID{j}; 461 | % extend header 462 | header = cat(2,header,[thisVar,'_',thisNode]); 463 | end 464 | table = cat(2,table,sim.readings.(thisVar)); 465 | end 466 | end 467 | 468 | % do links 469 | if ~isempty(sim.whatToStore.linkIdx) 470 | for i=1:numel(sim.whatToStore.linkVars) 471 | thisVar = sim.whatToStore.linkVars{i}; 472 | for j = 1 : numel(sim.whatToStore.linkID) 473 | thisLink = sim.whatToStore.linkID{j}; 474 | % extend header 475 | header = cat(2,header,[thisVar,'_',thisLink]); 476 | end 477 | table = cat(2,table,sim.readings.(thisVar)); 478 | end 479 | end 480 | 481 | % do attack track 482 | for i=1:size(sim.attackTrack,2) 483 | header = cat(2,header,sprintf('Attack#%02d',i)); 484 | end 485 | table = cat(2,table,sim.attackTrack); 486 | 487 | 488 | % rework header 489 | header = [header;repmat({','},1,numel(header))]; 490 | header = header(:)'; 491 | header = cell2mat(header(1:end-1)); 492 | end 493 | 494 | function clean_text = cleanField(self, text) 495 | % this cleans the fields of the .CPA file 496 | text(regexp(text,','))=[]; % remove commas 497 | if ~isempty(regexp(strtrim(text),' ','match')) 498 | % split if needed 499 | clean_text = strsplit(strtrim(text),' '); 500 | else 501 | clean_text = strtrim(text); 502 | end 503 | end 504 | 505 | % end of private methods 506 | end 507 | end 508 | -------------------------------------------------------------------------------- /epanetCPA/EpanetHelper.m: -------------------------------------------------------------------------------- 1 | classdef EpanetHelper 2 | % Class that contains generic functions to interface with the EPANET toolkit and 3 | % modify Epanet maps. All the methods are Static so that they can be called without 4 | % instantiating an object. 5 | 6 | 7 | %% TODO: ALL ERRORCODES (in all code) SHOULD HAVE SAME SPELLING FOR CONSISTENCY 8 | 9 | 10 | % Properties 11 | properties(Constant) 12 | % list of epanet parameter codes (dictionary) 13 | % TO DO: order them, maybe? 14 | EPANETCODES = containers.Map(... 15 | {... 16 | 'EN_DEMAND', 'EN_HEAD',... 17 | 'EN_PRESSURE', 'EN_FLOW',... 18 | 'EN_STATUS', 'EN_SETTING',... 19 | 'EN_ENERGY', 'EN_TANKLEVEL',... 20 | 'EN_PATCOUNT', 'EN_DURATION',... 21 | 'EN_CONTROLCOUNT', 'EN_BASEDEMAND',... 22 | 'EN_DIAMETER', 'EN_LENGTH', 'EN_ROUGHNESS',... 23 | 'EN_MINLEVEL','EN_MAXLEVEL',... 24 | 'EN_REPORTSTART', ... 25 | 'EN_NODECOUNT', 'EN_LINKCOUNT',... 26 | 'EN_PATTERNSTEP', 'EN_HYDSTEP',... 27 | 'EN_PATTERN', 'EN_EMITTER'},... 28 | [9, 10, 11, 8, 11, 12, ... 29 | 13, 8, 3, 0, 5, 40, 0, ... 30 | 1, 2, 20, 21, 6, 0, 2, ... 31 | 3, 1, 2, 3]); 32 | end 33 | 34 | % Public methods 35 | methods 36 | function self = EpanetHelper() 37 | % Empty constructor 38 | end 39 | end 40 | 41 | % Static methods 42 | methods(Static) 43 | 44 | function errorcode = epanetclose() 45 | % This code is modified from that of Philip Jonkergouw 46 | % 47 | % EPANETCLOSE - close the dll library 48 | % 49 | % Syntax: [errorcode] = epanetclose() 50 | % 51 | % Inputs: 52 | % none 53 | % Outputs: 54 | % errorcode - Fault code according to EPANET. 55 | % 56 | % Example: 57 | % [errorcode]=epanetclose() 58 | % 59 | % Original version 60 | % Author: Philip Jonkergouw 61 | % Email: pjonkergouw@gmail.com 62 | % Date: July 2007 63 | 64 | % Close EPANET ... 65 | [errorcode] = calllib('epanet2', 'ENclose'); 66 | if (errorcode) fprintf('EPANET error occurred. Code %g\n',... 67 | num2str(errorcode)); end 68 | if libisloaded('epanet2') unloadlibrary('epanet2'); end 69 | end 70 | 71 | function errorcode = epanetloadfile(inpFile) 72 | % This code is modified from that of Philip Jonkergouw and Demetrios Eliades 73 | % 74 | % EPANETLOADFILE - Loads the dll library and the network INP file. 75 | % 76 | % Syntax: [errorcode] = epanetloadfile(inpFile) 77 | % 78 | % Inputs: 79 | % inpFile - A string, name of the INP file 80 | % 81 | % Outputs: 82 | % errorcode - Fault code according to EPANET. 83 | % 84 | % Example: 85 | % [errorcode]=epanetloadfile('Net1.inp') 86 | % 87 | % Original version 88 | % Author: Philip Jonkergouw 89 | % Email: pjonkergouw@gmail.com 90 | % Date: July 2007 91 | % 92 | % Minor changes by 93 | % Author: Demetrios Eliades 94 | % University of Cyprus, KIOS Research Center for Intelligent Systems and Networks 95 | % email: eldemet@gmail.com 96 | % Website: http://eldemet.wordpress.com 97 | % August 2009; Last revision: 21-August-2009 98 | 99 | %------------- BEGIN CODE -------------- 100 | 101 | % Load the EPANET 2 dynamic link library ... 102 | if ~libisloaded('epanet2') 103 | loadlibrary('epanet2', 'epanet2.h'); 104 | end 105 | 106 | % Open the water distribution system ... 107 | s = which(inpFile); 108 | if ~isempty(s) inpFile = s; end 109 | 110 | [errorcode] = calllib('epanet2', 'ENopen', inpFile, 'temp1.$$$', 'temp2.$$$'); 111 | if (errorcode) 112 | error('Could not open network ''%s''.\nReturned empty array.\n', inpFile); 113 | return; 114 | else 115 | end 116 | end 117 | 118 | function list = getComponents(type) 119 | % Get list of Map components according to type 120 | list = {}; 121 | 122 | nComponents = int32(0); isNode = 0; 123 | switch type 124 | % junctions and dummy junctions (for overflow simulation) 125 | case {'JUNCTIONS', 'OF_JUNCTIONS'} 126 | [~,nComponents] =... 127 | calllib('epanet2','ENgetcount',0,nComponents); 128 | getTypeFunction = 'ENgetnodetype'; 129 | componentCode = 0; 130 | isNode = 1; 131 | 132 | % reservoirs 133 | case {'RESERVOIRS'} 134 | [~,nComponents] =... 135 | calllib('epanet2','ENgetcount',0,nComponents); 136 | getTypeFunction = 'ENgetnodetype'; 137 | componentCode = 1; 138 | isNode = 1; 139 | 140 | % tanks and dummy tanks 141 | case {'TANKS', 'OF_TANKS'} 142 | [~,nComponents] =... 143 | calllib('epanet2','ENgetcount',0,nComponents); 144 | getTypeFunction = 'ENgetnodetype'; 145 | componentCode = 2; 146 | isNode = 1; 147 | 148 | % pipes and dummy pipes (overflow) 149 | case {'PIPES','OF_PIPES'} 150 | [~,nComponents] =... 151 | calllib('epanet2','ENgetcount',2,nComponents); 152 | getTypeFunction = 'ENgetlinktype'; 153 | componentCode = 1; 154 | 155 | % pumps 156 | case 'PUMPS' 157 | [~,nComponents] =... 158 | calllib('epanet2','ENgetcount',2,nComponents); 159 | getTypeFunction = 'ENgetlinktype'; 160 | componentCode = 2; 161 | 162 | % valves 163 | case 'VALVES' 164 | [~,nComponents] =... 165 | calllib('epanet2','ENgetcount',2,nComponents); 166 | getTypeFunction = 'ENgetlinktype'; 167 | componentCode = 3:8; 168 | 169 | otherwise 170 | error('Search for RESERVOIRS, TANKS, OF_TANKS, JUNCTIONS, OF_JUNCTIONS, PUMPS, VALVES, PIPES, OF_PIPES or all. No %s',type); 171 | end 172 | 173 | componentType = int32(0); 174 | for i = 1 : nComponents 175 | % retrieve component type 176 | index = int32(i); 177 | [~,componentType] =... 178 | calllib('epanet2',getTypeFunction,index,componentType); 179 | 180 | if ismember(componentType,componentCode) 181 | % found component, retrieve its id 182 | [id,~] = EpanetHelper.getComponentId(index,isNode); 183 | 184 | % This part of code is to tell apart normal components from 185 | % dummy components according to how they are named. 186 | if strcmp('TANKS',type) 187 | % storage tanks 188 | if numel(id) >= 2 && ~strcmp(id(1:2),'OF') 189 | list = cat(1,list,id); 190 | end 191 | elseif strcmp('OF_TANKS',type) 192 | % dummy tanks 193 | if numel(id) >= 2 && strcmp(id(1:2),'OF') 194 | list = cat(1,list,id); 195 | end 196 | elseif strcmp('JUNCTIONS',type) 197 | % junctions 198 | if numel(id) >= 2 && strcmp(id(1),'J') 199 | list = cat(1,list,id); 200 | end 201 | elseif strcmp('OF_JUNCTIONS',type) 202 | % dummy junctions 203 | if numel(id) >= 3 && strcmp(id(1:3),'OFj') 204 | list = cat(1,list,id); 205 | end 206 | elseif strcmp('PIPES',type) 207 | % pipes 208 | if numel(id) >= 3 && ~strcmp(id(1:3),'OFp') 209 | list = cat(1,list,id); 210 | end 211 | elseif strcmp('OF_PIPES',type) 212 | % dummy pipes 213 | if numel(id) >= 3 && strcmp(id(1:3),'OFp') 214 | list = cat(1,list,id); 215 | end 216 | else 217 | % reservoirs, valves and pumps 218 | list = cat(1,list,id); 219 | end 220 | end 221 | end 222 | end 223 | 224 | function sections = addDummyTanks(tanks, sections) 225 | % Add dummy tanks to epanet .inp file to simulate overflow. 226 | % TO DO: not properly tested. 227 | 228 | % cycle through all the links and find pipes connected to tanks 229 | nLinks = int32(0); 230 | [errorcode, nLinks] = calllib('epanet2', 'ENgetcount', 2, nLinks); % get number of links 231 | 232 | node1 = int32(0); node2 = int32(0); % initialize nodes at link's ends 233 | 234 | for i = 1 : nLinks 235 | % current link 236 | thisLink = int32(i); 237 | 238 | % get nodes connected to link 239 | [~, node1,node2] = calllib(... 240 | 'epanet2', 'ENgetlinknodes', thisLink, node1,node2); 241 | ID1 = EpanetHelper.getComponentId(node1,1); 242 | ID2 = EpanetHelper.getComponentId(node2,1); 243 | 244 | % check if there is a tank, if so then store tank ID and the ID 245 | % of the connecting junction 246 | if ismember(ID1,tanks) 247 | thisTank = ID1; 248 | thisJunc = ID2; 249 | elseif ismember(ID2,tanks) 250 | thisJunc = ID1; 251 | thisTank = ID2; 252 | else 253 | % no tank connected, skip 254 | continue; 255 | end 256 | 257 | % create labels if there is a tank 258 | OFtank = ['OF', thisTank]; 259 | OFjunc = ['OFj',thisTank]; 260 | 261 | % add lines to sections 262 | 263 | % [COORDS] 264 | sectionIx = find(cellfun(@(x) strcmp(x,'[COORDINATES]'),{sections.name})); % get section index 265 | coordLine = EpanetHelper.findLineInSection(... 266 | sections(sectionIx),thisTank); % find tank coordinates (for placing dummy tank on the map) 267 | 268 | % extend section with additional lines 269 | sections(sectionIx).text = cat(1,sections(sectionIx).text,... 270 | sprintf(' %s\t %3.3f\t %3.3f\t;',... 271 | OFtank,str2num(coordLine{2})+10,str2num(coordLine{3})+10)); % dummy tank coords line 272 | 273 | sections(sectionIx).text = cat(1,sections(sectionIx).text,... 274 | sprintf(' %s\t %3.3f\t %3.3f\t;',... 275 | OFjunc,str2num(coordLine{2})+10,str2num(coordLine{3}))); % dummy junction coords line 276 | 277 | % [JUNC] 278 | sectionIx = find(cellfun(@(x) strcmp(x,'[JUNCTIONS]'),{sections.name})); % get section index 279 | 280 | % extend section with additional lines 281 | sections(sectionIx).text = cat(1,sections(sectionIx).text,... 282 | sprintf(' %s\t %d\t %d\t %s\t;',... 283 | OFjunc,0,0,' ')); 284 | 285 | 286 | % [TANKS] 287 | sectionIx = find(cellfun(@(x) strcmp(x,'[TANKS]'),{sections.name})); % get section index 288 | 289 | % extend section with additional lines 290 | sections(sectionIx).text = cat(1,sections(sectionIx).text,... 291 | sprintf(' %s\t %d\t %d\t %d\t %d\t %3.3f\t %d\t %s\t;',... 292 | OFtank,0,0,0,1,2*sqrt(1/pi)*10^3,0,' ')); 293 | 294 | % [PIPES] 295 | 296 | % get existing pipe settings 297 | pDiam = EpanetHelper.getComponentValue(thisLink, 0, EpanetHelper.EPANETCODES('EN_DIAMETER')); 298 | pLength = EpanetHelper.getComponentValue(thisLink, 0, EpanetHelper.EPANETCODES('EN_LENGTH')); 299 | pRough = EpanetHelper.getComponentValue(thisLink, 0, EpanetHelper.EPANETCODES('EN_ROUGHNESS')); 300 | 301 | sectionIx = find(cellfun(@(x) strcmp(x,'[PIPES]'),{sections.name})); % get section index 302 | 303 | % extend section with additional lines 304 | OFp = ['OFp',thisTank]; % twin pipe label 305 | sections(sectionIx).text = cat(1,sections(sectionIx).text,... 306 | sprintf(' %s\t %s\t %s\t %0.3f\t %0.3f\t %0.3f\t %d\t %s\t;',... 307 | OFp, thisJunc, OFjunc, pLength, pDiam, pRough, 0,'Closed')); % twin pipe 308 | 309 | CVp = ['CVp',thisTank]; % name of CV pipe connecting to dummy tank 310 | sections(sectionIx).text = cat(1,sections(sectionIx).text,... 311 | sprintf(' %s\t %s\t %s\t %0.3f\t %0.3f\t %0.3f\t %d\t %s\t;',... 312 | [CVp,'b'],OFjunc,OFtank,1,pDiam,pRough,0,'CV')); % CV pipe 313 | end 314 | 315 | 316 | end 317 | 318 | function [sections, pdaDict] = addDummyComponents(components,sections,P_min) 319 | % Add artificial string of FCV, CV and emitter to embed head-flow relationship 320 | 321 | % temp emitter coeff/FCV setting 322 | tempSetting = 1.0; 323 | 324 | % initialize 325 | linesToAdd_COORDS = cell(2*numel(components),1); %1 junction, 1 emitter node 326 | linesToAdd_JUNCS = cell(2*numel(components),1); 327 | linesToAdd_EMITS = cell(numel(components),1); 328 | linesToAdd_PIPES = cell(numel(components),1); 329 | linesToAdd_VALVES = cell(numel(components),1); 330 | 331 | nNodes = numel(components); 332 | 333 | % get section indexes 334 | ixJuncs = find(... 335 | cellfun(@(x) strcmp(x,'[JUNCTIONS]'),{sections.name})); 336 | ixCoord = find(... 337 | cellfun(@(x) strcmp(x,'[COORDINATES]'),{sections.name})); 338 | ixValves = find(... 339 | cellfun(@(x) strcmp(x,'[VALVES]'),{sections.name})); 340 | ixEmits = find(... 341 | cellfun(@(x) strcmp(x,'[EMITTERS]'),{sections.name})); %emitters section will probably be empty, but that's okay 342 | ixPipes = find(... 343 | cellfun(@(x) strcmp(x,'[PIPES]'),{sections.name})); 344 | 345 | % cycle through all the nodes and find nonzero demand nodes 346 | nCount = 0; 347 | 348 | % add string of artificial components only if demand > 0 349 | pdaDict = containers.Map(); 350 | EN_BASEDEMAND = 1; 351 | 352 | for i = 1 : nNodes 353 | % current node 354 | thisNodeIndex = int32(i); 355 | thisNode = EpanetHelper.getComponentId(thisNodeIndex,true); 356 | 357 | thisDemand = EpanetHelper.getComponentValue(thisNodeIndex, 1, EN_BASEDEMAND); 358 | % skip if base demand == 0 359 | if thisDemand == 0 360 | continue 361 | end 362 | 363 | % Store demand and node index. Put Placeholder for artificial components, to be filled 364 | % later with indexes for PDA settings update. 365 | temp.baseDemand = thisDemand; 366 | temp.ixJunction = thisNodeIndex; 367 | temp.ixFCV = -1; temp.ixEmit = -1; temp.ixPattern = -1; 368 | pdaDict(thisNode) = temp; 369 | 370 | % get existing node values 371 | EN_ELEVATION = 0; 372 | thisElev = EpanetHelper.getComponentValue(thisNodeIndex, 1, EN_ELEVATION); 373 | 374 | % create additional lines for the .inp file 375 | nCount = nCount + 1; 376 | % [COORDS] 377 | % find node coordinates (for placing new junction and emitter node on the map) 378 | coordLine = EpanetHelper.findLineInSection(sections(ixCoord),thisNode); 379 | PDjunc = ['j',thisNode]; % names the new artificial junction. Keep the name short to prevent stability issues. 380 | PDemit = ['e', thisNode]; % names the new artificial emitter node. 381 | 382 | % lines to add in ".inp" file 383 | linesToAdd_COORDS{(nCount-1)*2+1} = ... 384 | sprintf(' %s\t %3.3f\t %3.3f\t;',... 385 | PDjunc,str2num(coordLine{2})+20,str2num(coordLine{3})); 386 | 387 | linesToAdd_COORDS{nCount*2} = ... 388 | sprintf(' %s\t %3.3f\t %3.3f\t;',... 389 | PDemit,str2num(coordLine{2})+40,str2num(coordLine{3})); 390 | 391 | % [JUNC] 392 | % line to add in ".inp" file 393 | linesToAdd_JUNCS{(nCount-1)*2+1} = ... 394 | sprintf(' %s\t %d\t %3.3f\t %s\t;',... 395 | PDjunc,(thisElev+P_min),0,' '); 396 | 397 | linesToAdd_JUNCS{nCount*2} = ... 398 | sprintf(' %s\t %d\t %3.3f\t %s\t;',... 399 | PDemit,(thisElev+P_min),0,' '); % elevation is set at the nodal elevation + P_min to account for possible nonzero P_min 400 | 401 | % [EMITTERS] 402 | % line to add in ".inp" file 403 | linesToAdd_EMITS{nCount} = ... 404 | sprintf(' %s\t %3.3f\t %s\t;',... 405 | PDemit,tempSetting,' '); 406 | 407 | % [VALVES] 408 | % name of FCV new junction to new emitter 409 | PDv = ['v',thisNode]; 410 | 411 | pDiam = 100; %Arbitrary value. Because valves have no minor losses, I think this value shouldn't affect anything 412 | linesToAdd_VALVES{nCount} = ... 413 | sprintf(' %s\t %s\t %s\t %3.3f\t %s\t %3.3f\t %3.3f\t;',... 414 | PDv,PDjunc,PDemit,pDiam,'FCV',tempSetting,0); % need to set the FCV setting to the base demand * the pattern 415 | 416 | % [PIPES] 417 | PDp = ['p',thisNode]; 418 | 419 | % use generic values 420 | % TO DO: this should be down by maybe checking the values of original pipes connected to junction, or can be specified in .cpa file 421 | EN_DIAMETER = 0; EN_LENGTH = 1; EN_ROUGHNESS = 2; 422 | pDiam = 50.8 ;%EpanetHelper.getComponentValue(thisLink, 0, EN_DIAMETER); 423 | pLength = 0.001 ;% EpanetHelper.getComponentValue(thisLink, 0,EN_LENGTH); 424 | pRough = 140 ;%EpanetHelper.getComponentValue(thisLink, 0,EN_ROUGHNESS); 425 | 426 | % line to add in ".inp" file 427 | linesToAdd_PIPES{nCount} = ... 428 | sprintf(' %s\t %s\t %s\t %0.3f\t %0.3f\t %0.3f\t %d\t %s\t;',... 429 | PDp,thisNode,PDjunc,... 430 | pLength,pDiam,pRough,0,'CV'); % zero minor losses 431 | end 432 | 433 | % extend sections with additional lines 434 | % [COORDS] 435 | sections(ixCoord).text = cat(1,sections(ixCoord).text,linesToAdd_COORDS); 436 | 437 | % [JUNCS] 438 | sections(ixJuncs).text = cat(1,sections(ixJuncs).text,linesToAdd_JUNCS); 439 | 440 | % [EMITTERS] 441 | sections(ixEmits).text = cat(1,sections(ixEmits).text,linesToAdd_EMITS); 442 | 443 | % [VALVES] 444 | sections(ixValves).text = cat(1,sections(ixValves).text,linesToAdd_VALVES); 445 | 446 | % [PIPES] 447 | sections(ixPipes).text = cat(1,sections(ixPipes).text,linesToAdd_PIPES); 448 | end 449 | 450 | function [] = createInpFileFromSections(sections,inpFile) 451 | % Creates EPANET input file from section structarray 452 | 453 | % open file (write) 454 | fileId = fopen(inpFile,'w'); 455 | for i = 1 : numel(sections) 456 | if i > 1 457 | % add space 458 | fprintf(fileId, '\n\n'); 459 | end 460 | 461 | % write section name 462 | fprintf(fileId, '%s\n', sections(i).name); 463 | 464 | % insert section text 465 | nLines = numel(sections(i).text); 466 | for j = 1 : nLines 467 | thisLine = sections(i).text{j}; 468 | fprintf(fileId, '%s\n', thisLine); 469 | end 470 | end 471 | fclose(fileId); 472 | end 473 | 474 | function sections = divideInpInSections(inpFile) 475 | % Reads a .inp file and divides it into sections 476 | 477 | fileId = fopen(inpFile); 478 | sections = []; 479 | thisSection = []; 480 | while ~feof(fileId) 481 | thisLine = fgetl(fileId); 482 | temp = regexp(thisLine,'\[(.*?)\]','match'); 483 | if ~isempty(temp) 484 | 485 | % add previous section 486 | sections = cat(1,sections,thisSection); 487 | 488 | % start of a section 489 | thisSection.name = temp{1}; 490 | thisSection.text = {}; 491 | else 492 | % add text to section 493 | thisSection.text = cat(1,thisSection.text,thisLine); 494 | end 495 | end 496 | % add last section 497 | sections = cat(1,sections,thisSection); 498 | fclose(fileId); 499 | end 500 | 501 | function line = findLineInSection(section, id) 502 | % retrieves line in a [SECTION] pertaining to a given component (id) 503 | text = section.text; 504 | nLines = numel(text); 505 | line = {}; 506 | for i = 1 : nLines 507 | thisLine = text{i}; 508 | temp = regexp(strtrim(thisLine),'\s*','split'); 509 | if strcmp(temp{1},id) 510 | line = temp; 511 | return; 512 | end 513 | end 514 | 515 | if isempty(line) 516 | error('COMPONENT NOT FOUND IN SECTION TEXT'); 517 | end 518 | end 519 | 520 | function [id,errorcode] = getComponentId(index,isNode) 521 | % Get the ID of an EPANET component, whether node or link 522 | 523 | % initialize 524 | index = int32(index); 525 | id = ''; 526 | if isNode 527 | [errorcode,id] =... 528 | calllib('epanet2', 'ENgetnodeid', index, id); 529 | else 530 | [errorcode,id] =... 531 | calllib('epanet2', 'ENgetlinkid', index, id); 532 | end 533 | end 534 | 535 | function [index,errorcode,isNode] = getComponentIndex(id) 536 | % Get the index of an EPANET component, whether node or link 537 | isNode = true; 538 | [errorcode,id,index] = calllib('epanet2', 'ENgetnodeindex', id, 0); 539 | 540 | if errorcode 541 | % ... maybe is a link 542 | [errorcode,~,index] = calllib('epanet2', 'ENgetlinkindex', id, 0); 543 | isNode = false; 544 | end 545 | end 546 | 547 | function [value, errorcode] = getComponentValue(index, isNode, code) 548 | % Get values (according to code) of node or link 549 | 550 | % check if is a node 551 | value = single(0); 552 | if isNode 553 | [errorcode,value] = ... 554 | calllib('epanet2', 'ENgetnodevalue',... 555 | int32(index), code, value); 556 | else 557 | [errorcode,value] = ... 558 | calllib('epanet2', 'ENgetlinkvalue',... 559 | int32(index), code, value); 560 | end 561 | end 562 | 563 | function errorcode = setComponentValue(index, value, code) 564 | % Set values (according to code) of node or link 565 | 566 | % set value, try node first 567 | errorcode = calllib('epanet2', 'ENsetnodevalue',... 568 | int32(index), code, value); 569 | 570 | if errorcode 571 | % ... maybe is a link 572 | errorcode = calllib('epanet2', 'ENsetlinkvalue',... 573 | int32(index), code, value); 574 | end 575 | 576 | end 577 | 578 | function errorcode = setPattern(index, multipliers) 579 | % set the pattern identified by index 580 | pPointer = libpointer('singlePtr',multipliers); 581 | pLength = length(multipliers); 582 | errorcode = calllib('epanet2','ENsetpattern',... 583 | int32(index),pPointer,int32(pLength)); 584 | end 585 | 586 | function pattern = getPattern(index) 587 | % get pattern length 588 | P_length = int32(0); 589 | [~,P_length] = calllib('epanet2', 'ENgetpatternlen',index, P_length); 590 | P_length = int32(P_length); 591 | % loop to fill pattern array 592 | pattern = zeros(1,P_length); 593 | for i = 1 : P_length % CHECK IF it starts from 0 or 1 594 | PM_value = double(0); 595 | [~,PM_value] = calllib('epanet2', 'ENgetpatternvalue',index, i, PM_value); 596 | pattern(i) = PM_value; 597 | end 598 | end 599 | 600 | function value = getComponentValueForAttacks(id) 601 | % returns sensor reading for selected component 602 | % i.e. Tank = water level, Pipe = flow rate, junction = pressure 603 | 604 | % remove prefix 605 | [id, temp] = regexp(id,'_','split'); 606 | if ~isempty(temp) 607 | id = id{2}; 608 | else 609 | id = id{1}; 610 | end 611 | % get index and type 612 | [index,errorcode,isNode] = EpanetHelper.getComponentIndex(id); 613 | 614 | if ~errorcode 615 | % check if is a node or link 616 | if isNode 617 | %% NODE 618 | % is it a junction, reservoir or tank? 619 | nodeType = int32(0); 620 | [~,nodeType] = calllib('epanet2','ENgetnodetype',index,nodeType); 621 | % retrieve value 622 | value = single(0); 623 | switch nodeType 624 | case 0 625 | % junction --> PRESSURE 626 | [~,value] = calllib(... 627 | 'epanet2','ENgetnodevalue',index,EpanetHelper.EPANETCODES('EN_PRESSURE'),value); 628 | case 1 629 | % reservoir --> HEAD 630 | [~,value] = calllib(... 631 | 'epanet2','ENgetnodevalue',index,EpanetHelper.EPANETCODES('EN_HEAD'),value); 632 | case 2 633 | % tank --> Water level 634 | [~,value] = calllib(... 635 | 'epanet2','ENgetnodevalue',index,EpanetHelper.EPANETCODES('EN_PRESSURE'),value); 636 | end 637 | else 638 | %% LINK 639 | % is it a pipe, pump or valve? 640 | % actually doesn't matter, just return the flow rate 641 | % retrieve value 642 | value = single(0); 643 | [~,value] = calllib(... 644 | 'epanet2','ENgetlinkvalue',index,EpanetHelper.EPANETCODES('EN_FLOW'),value); 645 | end 646 | else 647 | error('ERROR %d returned', errorcode); 648 | end 649 | end 650 | 651 | %% END OF CLASS 652 | end 653 | end -------------------------------------------------------------------------------- /epanetCPA/EpanetCPASimulation.m: -------------------------------------------------------------------------------- 1 | classdef EpanetCPASimulation 2 | % Class for running a step-by-step EPANET simulation with (and without) cyber-attacks. 3 | 4 | properties 5 | epanetMap % EpanetMap instance contaning the modified .inp file; 6 | 7 | attacks % List of cyber-physical attacks (if empty, load original map) 8 | 9 | display_every % 0 = no display, otherwise display_ever x iterations 10 | 11 | startTime % startTime of the simulation 12 | 13 | simTime % current time into simulation 14 | 15 | readings % readings of the simulation 16 | 17 | symbolDict % dictionary with all symbols needed attack begin/stop 18 | 19 | T, cT % array of times and clocktimes 20 | 21 | attackTrack % track attack history (on/off for each attack, in time) 22 | 23 | alteredReadings % here are the information regarding the altered readings at PLC and SCADA layer 24 | 25 | whatToStore % readings and variables to store 26 | 27 | tstep % time step (STILL TO DO) 28 | 29 | patternStepLength % step length of demand pattern 30 | 31 | dds % NEW FOR OPTIMISIMUL desider demands cell array 32 | 33 | end 34 | 35 | 36 | % public methods 37 | methods 38 | 39 | function self = EpanetCPASimulation(epanetMap, attacks, cyberoptions) 40 | % Constructor class for EpanetCPASimulation. 41 | 42 | % fill properties 43 | self.epanetMap = epanetMap; 44 | self.attacks = attacks; 45 | self.display_every = cyberoptions.verbosity; 46 | self.whatToStore = cyberoptions.what_to_store; 47 | 48 | %% NEW FOR OPTIMISIMUL 49 | % store desired demand to compute unmet demands 50 | self.dds = {}; 51 | 52 | % load map file 53 | EpanetHelper.epanetloadfile(self.epanetMap.modifiedFilePath); 54 | 55 | % validate attacks (TO DO: make sure all attacks have a validation method) 56 | for i = 1 : numel(self.attacks) 57 | if ismethod(self.attacks{i}, 'validateAttack') 58 | % check for attack where validation has been enabled 59 | if isa(self.attacks{i},'AttackOnCommunication') 60 | % if AttackOnCommunication we need to send systems 61 | self.attacks{i} = self.attacks{i}.validateAttack(self.epanetMap.cyberlayer); 62 | end 63 | else 64 | warning('Class %s has no validateAttack method yet!', class(self.attacks{i})); 65 | end 66 | end 67 | 68 | 69 | % get nNodes and nLinks (TO DO: maybe you should store this somewhere...) 70 | nNodes = 0; nLinks = 0; 71 | [~,nNodes] = calllib(... 72 | 'epanet2','ENgetcount', EpanetHelper.EPANETCODES('EN_NODECOUNT'),nNodes); 73 | [~,nLinks] = calllib(... 74 | 'epanet2','ENgetcount', EpanetHelper.EPANETCODES('EN_LINKCOUNT'),nLinks); 75 | 76 | % get all node and link indexes to store 77 | % TO DO: create a dedicated function for this 78 | self.whatToStore.nodeIdx = []; 79 | self.whatToStore.linkIdx = []; 80 | switch self.whatToStore.sensors{1} 81 | case 'everything' 82 | % we will store readings for all nodes 83 | self.whatToStore.nodeIdx = 1 : nNodes; 84 | % ... and all links 85 | self.whatToStore.linkIdx = 1 : nLinks; 86 | % store all variables as well 87 | self.whatToStore.nodeVars = {'PRESSURE','HEAD','DEMAND'}; 88 | self.whatToStore.linkVars = {'FLOW', 'STATUS', 'SETTING','ENERGY'}; 89 | case 'all' 90 | % we will store selected readings for all nodes 91 | self.whatToStore.nodeIdx = 1 : nNodes; 92 | % ... and all links 93 | self.whatToStore.linkIdx = 1 : nLinks; 94 | case 'all nodes' 95 | % we will store selected readings for all nodes 96 | self.whatToStore.nodeIdx = 1 : nNodes; 97 | case 'all links' 98 | % we will store selected readings for all links 99 | self.whatToStore.linkIdx = 1 : nLinks; 100 | % case 'SCADA' 101 | % % store ground truth of all variables seen by SCADA 102 | % retrieve 103 | % self.epanetMap.cyberlayer.systems(3) 104 | % 105 | otherwise 106 | for i = 1 : numel(self.whatToStore.sensors) 107 | thisId = self.whatToStore.sensors{i}; 108 | [thisIdx, ~, isNode] = EpanetHelper.getComponentIndex(thisId); 109 | if isNode 110 | self.whatToStore.nodeIdx = cat(1,self.whatToStore.nodeIdx,thisIdx); 111 | else 112 | self.whatToStore.linkIdx = cat(1,self.whatToStore.linkIdx,thisIdx); 113 | end 114 | end 115 | end 116 | 117 | % get stored nodes and links IDs 118 | self.whatToStore.nodeID = cell(size(self.whatToStore.nodeIdx,1),1); 119 | self.whatToStore.linkID = cell(size(self.whatToStore.linkIdx,1),1); 120 | for j = 1 : numel(self.whatToStore.nodeIdx) 121 | thisIdx = self.whatToStore.nodeIdx(j); 122 | self.whatToStore.nodeID{j} = EpanetHelper.getComponentId(thisIdx,1); 123 | end 124 | 125 | for j = 1 : numel(self.whatToStore.linkIdx) 126 | thisIdx = self.whatToStore.linkIdx(j); 127 | self.whatToStore.linkID{j} = EpanetHelper.getComponentId(thisIdx,0); 128 | end 129 | % this is needed for stuff like everything, all links, all .... 130 | % TO DO: improve the concatenation bit 131 | try 132 | self.whatToStore.sensors = cat(2,self.whatToStore.nodeID,self.whatToStore.linkID); 133 | catch 134 | self.whatToStore.sensors = cat(1,self.whatToStore.nodeID,self.whatToStore.linkID); 135 | end 136 | 137 | 138 | % initialize time 139 | self.simTime = 0; 140 | 141 | % get starttime constant 142 | HOURS_TO_SECONDS = 3600; 143 | STARTTIME = int64(0); 144 | [~,STARTTIME] = calllib(... 145 | 'epanet2','ENgettimeparam', ... 146 | EpanetHelper.EPANETCODES('EN_REPORTSTART'),STARTTIME); 147 | self.startTime = double(STARTTIME/HOURS_TO_SECONDS); 148 | 149 | % initialize readings arrays 150 | self.readings.PRESSURE = []; self.readings.DEMAND = []; 151 | self.readings.FLOW = []; self.readings.SETTING = []; 152 | self.readings.ENERGY = []; self.readings.HEAD = []; 153 | self.readings.STATUS = []; 154 | 155 | % initialize time, clocktime and attack track arrays 156 | self.T = []; self.cT = []; self.attackTrack = []; 157 | 158 | % get pattern step length 159 | EN_PATTERNSTEP = EpanetHelper.EPANETCODES('EN_PATTERNSTEP'); 160 | self.patternStepLength = double(0); 161 | [errorcode, self.patternStepLength] = calllib(... 162 | 'epanet2', 'ENgettimeparam',... 163 | EN_PATTERNSTEP, self.patternStepLength); 164 | self.patternStepLength = double(self.patternStepLength); 165 | 166 | % initialize symbol dictionary 167 | self = initializeSymbolDictionary(self); 168 | 169 | % initialize altered readings 170 | self.alteredReadings = []; 171 | 172 | EpanetHelper.epanetclose(); 173 | end 174 | 175 | function self = run(self) 176 | % runs the hydraulic simulation. 177 | 178 | % open simulation 179 | EpanetHelper.epanetloadfile(self.epanetMap.modifiedFilePath); 180 | 181 | % set patterns 182 | self.epanetMap.patterns = self.epanetMap.setPatterns(); 183 | 184 | % set initial tank levels 185 | self.epanetMap.setInitialTankLevels(); 186 | 187 | % deactivate all map controls 188 | self.epanetMap.deactivateControls(); 189 | 190 | % zero based demands (for pda) 191 | if self.epanetMap.usePDA 192 | self.zeroBaseDemands(); 193 | end 194 | 195 | %% MAIN LOOP 196 | % open the hydraulic solver 197 | errorcode = calllib('epanet2', 'ENopenH'); 198 | 199 | % initialize the hydraulic solver 200 | INITFLAG = 0; 201 | errorcode = calllib('epanet2', 'ENinitH', INITFLAG); 202 | HOURS_TO_SECONDS = 3600; 203 | 204 | % simulation loop 205 | self.tstep = self.epanetMap.h_tstep; 206 | tstep = self.tstep; 207 | 208 | while tstep && ~errorcode 209 | 210 | TIME = double(self.simTime)/HOURS_TO_SECONDS; 211 | if (TIME>0) && mod(TIME,self.display_every)<=0.00001% TODO: make this clearer 212 | echoString = sprintf('TIME: %.3f\n',TIME); 213 | fprintf(echoString); 214 | end 215 | 216 | % update component settings (for pda) 217 | if self.epanetMap.usePDA 218 | %% NEW FOR OPTIMISIMUL 219 | self = self.updateComponentSettings(); 220 | end 221 | 222 | % run hydraulic simulation step 223 | [self,tstep] = self.hydraulicStep(tstep); 224 | self.tstep = tstep; 225 | [~, self.simTime] = calllib('epanet2', 'ENrunH', self.simTime); 226 | 227 | % update simulation state 228 | self = self.getCurrentState(); 229 | 230 | % continue to the next time step ... 231 | [errorcode, tstep] = calllib('epanet2', 'ENnextH', tstep); 232 | self.tstep = tstep; 233 | end 234 | 235 | % close simulation 236 | EpanetHelper.epanetclose(); 237 | end 238 | 239 | % end of public methods 240 | end 241 | 242 | 243 | % private methods 244 | methods (Access = private) 245 | 246 | function [self,tstep] = hydraulicStep(self, tstep) 247 | % execute hydraulic step using the epanet toolkit. 248 | 249 | %% Work with symbol dictionaries 250 | % update dictionary 251 | self = updateSymbolDictionary(self); 252 | 253 | % get all systems 254 | systems = self.epanetMap.cyberlayer.systems; 255 | for j = 1 : numel(systems) 256 | % get PLC name 257 | controllerName = systems(j).name; 258 | % intialize dict for this PLC 259 | eval(sprintf('%sdict = containers.Map();', controllerName)); 260 | % sensors read 261 | sensors = cat(2,systems(j).sensors,systems(j).sensorsIn); 262 | for k = 1 : numel(sensors) 263 | % remove previx (P_, F_ or S_...) 264 | sensor = sensors{k}; 265 | sensor = regexp(sensor,'_','split'); 266 | sensor = sensor{2}; 267 | try 268 | reading = self.symbolDict(sensor); 269 | eval(sprintf('%sdict(sensor) = reading;',controllerName)); 270 | catch 271 | % fprintf('Sensor %s not used in control logic: 272 | % skipping.',sensor) 273 | % fprintf('Sensor %s non used in control logic\n', sensor); 274 | end 275 | 276 | end 277 | end 278 | 279 | % create SCADA dictionary 280 | SCADAdict = [self.symbolDict;containers.Map()]; % concatenation so it's deep-copy 281 | 282 | %% Evaluate, perform and track attacks 283 | 284 | % get number of attacks 285 | nAttacks = numel(self.attacks); 286 | 287 | % counter for attacks in place 288 | attacksInPlace = 0; 289 | 290 | % Cycle through attacks 291 | for i = 1 : nAttacks 292 | % for each attack, check their conditions and see whether they 293 | % have to start, end or continue... 294 | [attack, self] = self.attacks{i}.evaluateAttack(self); 295 | 296 | % alter readings if attack is in place and alters readings, 297 | % i.e. AttackOnSensor & AttackOnCommunication. 298 | attackAltersReading = isprop(attack,'alteredReading'); 299 | if isa(attack,'AttackOnCommunicationNew') && ~attack.targetIsSensor 300 | % readings are not altered if AttackOnCommunication targets 301 | % incoming actuator transmission. 302 | attackAltersReading = false; 303 | end 304 | 305 | if attack.inplace && attackAltersReading 306 | % flag this if SCADA readings are modified by attack due to 307 | % cascade effect 308 | doesPropagate = false; 309 | 310 | % get layer and target 311 | layer = attack.layer; 312 | % (first remove prefix from target) 313 | sensor = attack.target; 314 | temp = regexp(sensor,'_','split'); 315 | variable = temp{1}; 316 | target = temp{2}; 317 | 318 | alteredReading = attack.alteredReading; 319 | 320 | % create new entry for altered readings 321 | self = self.storeAlteredReadingEntry(... 322 | layer, variable, target, alteredReading); 323 | 324 | if strcmp(layer,'PHY') 325 | % direct attack to sensor 326 | controllerName = 'NO PLC'; 327 | doesPropagate = true; % sensor manipulation always alters SCADA 328 | else 329 | % attack to connection 330 | thisController = systems(ismember({systems.name},layer)); 331 | controllerName = thisController.name; 332 | % modify PLC dictionary 333 | eval(sprintf('%sdict(target) = alteredReading;', controllerName)); 334 | 335 | % alter downstream if sensor directly connected to 336 | % controller 337 | if ismember(sensor, thisController.sensors) 338 | doesPropagate = true; 339 | end 340 | end 341 | 342 | % Altered signal may affect other systems down the 343 | % line. We need to alter these readings too. 344 | 345 | % if attack at physical layer, then alter readings of PLC 346 | % reading the sensor. 347 | if strcmp(layer,'PHY') 348 | for j = 1 : size(systems,1) 349 | controllerName_ = systems(j).name; 350 | if (strcmp(controllerName_,controllerName) == 0) && ... 351 | (sum(ismember(systems(j).sensors,sensor)) > 0) 352 | % Modify PLC dictionary and create new 353 | % entry for altered readings 354 | eval(sprintf('%sdict(target) = alteredReading;', controllerName_)); 355 | self = self.storeAlteredReadingEntry(... 356 | controllerName_, variable, target, alteredReading); 357 | end 358 | end 359 | end 360 | 361 | % check for further propagation, including SCADA 362 | if doesPropagate 363 | for j = 1 : size(systems,1) 364 | controllerName_ = systems(j).name; 365 | if (strcmp(controllerName_,controllerName) == 0) && ... 366 | (sum(ismember(systems(j).sensorsIn,sensor))>0) 367 | % Modify PLC dictionary and create new 368 | % entry for altered readings 369 | eval(sprintf('%sdict(target) = alteredReading;', controllerName_)); 370 | self = self.storeAlteredReadingEntry(... 371 | controllerName_, variable, target, alteredReading); 372 | end 373 | end 374 | end 375 | end 376 | 377 | % store attacks 378 | self.attacks{i} = attack; 379 | 380 | % update count of attacks in place 381 | attacksInPlace = attacksInPlace + attack.inplace; 382 | end 383 | 384 | 385 | %% Override control logic 386 | for i = 1 : numel(systems) 387 | thisController = systems(i); 388 | try 389 | eval(sprintf('PLCdict = %sdict;',thisController.name)); 390 | catch 391 | disp('error') 392 | end 393 | % this doesn't look as nice as if it were a method within the 394 | % EpanetMap class, or PLC better 395 | % MAXLEVELS and CLOCKTIME, TIME should be sent along too 396 | % add generic variables such as TIME or MAXLEVELs from SCADAdict 397 | if ~strcmp(thisController.name,'SCADA') 398 | % add generic variables such as TIME or MAXLEVELs from SCADAdict 399 | scada_keys = SCADAdict.keys; 400 | for i = 1 : numel(scada_keys) 401 | thisKey = scada_keys{i}; 402 | isMaxLevel = strfind(thisKey,'MAXLEVEL')==1; 403 | if isempty(isMaxLevel) 404 | isMaxLevel = 0; 405 | end 406 | if ismember(thisKey,['TIME','CLOCKTIME']) | isMaxLevel 407 | PLCdict(thisKey) = SCADAdict(thisKey); 408 | end 409 | end 410 | end 411 | self.overrideControls(thisController, PLCdict); 412 | end 413 | 414 | % activate dummy controls if needed 415 | self = self.activateDummyControls(tstep); 416 | 417 | %% Track attack history 418 | attackFlag = zeros(1,nAttacks); 419 | for i = 1 : nAttacks 420 | attackFlag(i) = self.attacks{i}.inplace; 421 | end 422 | self.attackTrack = cat(1,self.attackTrack,attackFlag); 423 | 424 | end 425 | 426 | function self = getCurrentState(self) 427 | % store time, nodes/links readings 428 | 429 | % TO DO: rename the method? Put HOURS_TO_SECONDS somewhere else? 430 | 431 | % store time vars 432 | HOURS_TO_SECONDS = 3600; 433 | TIME = double(self.simTime)/HOURS_TO_SECONDS; 434 | % time 435 | self.T = cat(1,self.T,TIME); 436 | % ... and clocktime 437 | self.cT = cat(1,self.cT,mod(self.startTime+TIME,24)); 438 | 439 | % store nodal readings (if there are nodes) 440 | if ~isempty(self.whatToStore.nodeIdx) 441 | variables = self.whatToStore.nodeVars; 442 | nNodes = numel(self.whatToStore.nodeIdx); 443 | index = self.whatToStore.nodeIdx; 444 | for j = 1 : numel(variables) 445 | this_var = variables{j}; 446 | value = repmat(0.0,1,nNodes); 447 | for n = 1:nNodes 448 | try 449 | [errorcode, value(n)] = calllib('epanet2', 'ENgetnodevalue', index(n),... 450 | EpanetHelper.EPANETCODES(['EN_',variables{j}]), value(n)); 451 | catch EPANET_VARIABLE_EXCEPTION 452 | if isempty(this_var) 453 | error('No node variables specified in CPA file.') 454 | else 455 | error('Variable %s does not exist for epanet nodes.', this_var) 456 | end 457 | end 458 | end 459 | self.readings.(this_var) = cat(1,self.readings.(this_var),double(value)); 460 | end 461 | end 462 | 463 | % store link readings (if there are links) 464 | if ~isempty(self.whatToStore.linkIdx) 465 | variables = self.whatToStore.linkVars; 466 | nLinks = numel(self.whatToStore.linkIdx); 467 | index = self.whatToStore.linkIdx; 468 | for j = 1 : numel(variables) 469 | this_var = variables{j}; 470 | value = repmat(0.0,1,nLinks); 471 | for n = 1:nLinks 472 | try 473 | [errorcode, value(n)] = calllib('epanet2', 'ENgetlinkvalue', index(n),... 474 | EpanetHelper.EPANETCODES(['EN_',variables{j}]), value(n)); 475 | catch EPANET_VARIABLE_EXCEPTION 476 | if isempty(this_var) 477 | error('No link variables specified in CPA file.') 478 | else 479 | error('Variable %s does not exist for epanet link.', this_var) 480 | end 481 | end 482 | end 483 | self.readings.(this_var) = cat(1,self.readings.(this_var),double(value)); 484 | end 485 | end 486 | end 487 | 488 | function self = initializeSymbolDictionary(self) 489 | % initialize symbol dictionary 490 | 491 | % TO DO: merge it with in some initializeSimulation of sorts?? 492 | 493 | % initialize 494 | self.symbolDict = containers.Map(); 495 | 496 | % add TANKS max levels (these won't change during simulation) 497 | EN_MAXLEVEL = EpanetHelper.EPANETCODES('EN_MAXLEVEL'); 498 | TANKS = self.epanetMap.components('TANKS'); 499 | for i = 1 : numel(TANKS) 500 | thisTankIndex = EpanetHelper.getComponentIndex(TANKS{i}); 501 | maxTankLevel = EpanetHelper.getComponentValue(thisTankIndex, true, EN_MAXLEVEL); 502 | maxTankLevel = maxTankLevel - 10^-3; % ... minus a bit or it won't work 503 | % create parsing CONSTANT 504 | eval(sprintf('self.symbolDict(''%s%s'') = %d;',... 505 | 'MAXLEVEL',TANKS{i},maxTankLevel)); 506 | end 507 | clear TANKS 508 | end 509 | 510 | function self = updateSymbolDictionary(self) 511 | % update the symbol dictionary used for controls and attacks. 512 | HOURS_TO_SECONDS = 3600; 513 | nAttacks = numel(self.attacks); 514 | 515 | % get time into the simulation and clocktime and insert into dictionary 516 | TIME = double(self.simTime)/HOURS_TO_SECONDS; 517 | self.symbolDict('TIME') = TIME; 518 | self.symbolDict('CLOCKTIME') = mod(self.startTime+TIME,24); 519 | 520 | % put attack status symbols in dictionary 521 | %(i.e. ATT1 = 1, then 1st attack is currently ON) 522 | for i = 1 : nAttacks 523 | eval(sprintf('self.symbolDict(''%s%d'') = %d;',... 524 | 'ATT', i,self.attacks{i}.inplace)); 525 | end 526 | 527 | % put component symbols in dictionary for each control 528 | % TO DO: need to avoid multiple check of the same sensors! 529 | for i = 1 : numel(self.epanetMap.controls) 530 | % get the control sensor ID 531 | thisSensor = EpanetHelper.getComponentId(self.epanetMap.controls(i).nIndex, 1); 532 | % if it's a time-based control nIndex == 0. if prevents error 533 | if self.epanetMap.controls(i).nIndex > 0 534 | eval(sprintf('self.symbolDict(''%s'') = EpanetHelper.getComponentValueForAttacks(''%s'');',... 535 | thisSensor,thisSensor)); 536 | end 537 | end 538 | 539 | 540 | % put component symbols in dictionary for each attack for both 541 | % initial... 542 | for i = 1 : nAttacks 543 | % initial condition 544 | thisCondition = self.attacks{i}.ini_condition; 545 | % retrieve vars 546 | try 547 | vars = symvar(thisCondition); 548 | catch 549 | error(['Problem with the format of initial or ending', ... 550 | 'conditions of the attacks.',...\ 551 | 'Try removing all whitespaces: example TIME == 20 --> TIME==20']); 552 | end 553 | for j = 1 : numel(vars) 554 | thisVar = vars{j}; 555 | % check if symbol has already been included (search dict 556 | % keys) MAXLEVELS are never updated, TIME, CLOCKTIME and 557 | % ATTx are evaluated beforehad. 558 | if ~strcmp(thisVar,'TIME') && ~strcmp(thisVar,'CLOCKTIME') &&... 559 | ~strncmp(thisVar,'ATT',3) && ~strncmp(thisVar,'MAXLEVEL',8) 560 | eval(sprintf('self.symbolDict(''%s'') = EpanetHelper.getComponentValueForAttacks(''%s'');',... 561 | thisVar,thisVar)); 562 | end 563 | end 564 | end 565 | 566 | % ... and ending conditions 567 | for i = 1 : nAttacks 568 | % initial condition 569 | thisCondition = self.attacks{i}.end_condition; 570 | % retrieve vars 571 | vars = symvar(thisCondition); 572 | for j = 1 : numel(vars) 573 | thisVar = vars{j}; 574 | % check if symbol has already been included (search dict 575 | % keys) MAXLEVELS are never update, TIME, CLOCKTIME and 576 | % ATTx are evaluated beforehad. 577 | if ~strcmp(thisVar,'TIME') && ~strcmp(thisVar,'CLOCKTIME') &&... 578 | ~strncmp(thisVar,'ATT',3) && ~strncmp(thisVar,'MAXLEVEL',8) 579 | eval(sprintf('self.symbolDict(''%s'') = EpanetHelper.getComponentValueForAttacks(''%s'');',... 580 | thisVar,thisVar)); 581 | end 582 | end 583 | end 584 | 585 | % Add attack targets (needed for junctions and attacks to 586 | % communications) 587 | for i = 1 : nAttacks 588 | % target 589 | if ~strcmp(self.attacks{i}.layer,'CTRL') 590 | thisVar = self.attacks{i}.target; 591 | eval(sprintf('self.symbolDict(''%s'') = EpanetHelper.getComponentValueForAttacks(''%s'');',... 592 | thisVar,thisVar)); 593 | end 594 | end 595 | 596 | end 597 | 598 | function [] = overrideControls(self,thisController,PLCdict) 599 | % override control logic by calling control objects in 600 | % the epanetMap 601 | 602 | % retrieve controls and call overide method 603 | controls = self.epanetMap.controls; 604 | for i = 1 : numel(thisController.controlsID) 605 | ix = thisController.controlsID(i); 606 | controls(ix).overrideControl(PLCdict); 607 | end 608 | end 609 | 610 | function self = activateDummyControls(self, tstep) 611 | % complete override by activating dummy controls 612 | % TO DO: merge with overrideControls? 613 | 614 | % retrieve active dummy controls 615 | if sum([self.epanetMap.dummyControls.isActive]) > 0 616 | for i = 1 : numel(self.attacks) 617 | if self.attacks{i}.inplace == 1 618 | ix = self.attacks{i}.actControl; 619 | if ~isempty(ix) 620 | lS = self.attacks{i}.setting; 621 | time = int64(self.simTime + tstep); 622 | self.epanetMap.dummyControls(ix) = ... 623 | self.epanetMap.dummyControls(ix).activateWithValues(lS,time); 624 | end 625 | end 626 | end 627 | end 628 | end 629 | 630 | function self = storeAlteredReadingEntry(self, layer, variable, target, alteredReading) 631 | 632 | % creates and store new entry for altered readings 633 | thisEntry.time = self.T(end); 634 | thisEntry.layer = layer; 635 | thisEntry.sensorId = target; 636 | thisEntry.reading = alteredReading; 637 | switch variable 638 | case 'P' 639 | thisEntry.variable = 'PRESSURE'; 640 | case 'F' 641 | thisEntry.variable = 'FLOW'; 642 | case 'S' 643 | thisEntry.variable = 'STATUS'; 644 | case 'SE' 645 | thisEntry.variable = 'STATUS'; 646 | otherwise 647 | error('Why did I get here?') 648 | end 649 | % check if target is node or link to see if reading is pressure or flow 650 | % 651 | % [~, ~, isNode] = EpanetHelper.getComponentIndex(target); 652 | % if isNode 653 | % thisEntry.variable = 'PRESSURE'; 654 | % else 655 | % thisEntry.variable = 'FLOW'; 656 | % end 657 | self.alteredReadings = cat(1,self.alteredReadings,thisEntry); 658 | end 659 | 660 | function self = zeroBaseDemands(self) 661 | % TO DO: this can be moved to EpanetHelper.addDummyComponents() 662 | % cycle through all nodes with base demand > 0 and zero them 663 | EN_BASEDEMAND = 1; 664 | junctions = keys(self.epanetMap.pdaDict); 665 | for i = 1:length(junctions) 666 | ix_dummy = EpanetHelper.getComponentIndex(junctions{i}); 667 | EpanetHelper.setComponentValue(ix_dummy,0.0,EN_BASEDEMAND); 668 | end 669 | end 670 | 671 | function self = updateComponentSettings(self) 672 | 673 | % loop through demand nodes 674 | pdaDict = self.epanetMap.pdaDict; 675 | junctions = keys(pdaDict); 676 | 677 | % get pattern length 678 | patterns = self.epanetMap.patterns; 679 | [P_length,~] = size(patterns); 680 | 681 | %% NEW FOR OPTIMISIMUL 682 | % store desired demand to compute unmet demands 683 | dd = containers.Map(); 684 | for i = 1:length(junctions) 685 | thisJunction = junctions{i}; 686 | 687 | % get current pattern timestep 688 | timeNow = double(self.simTime); 689 | patternStep = rem((floor(timeNow/self.patternStepLength)+1),P_length); 690 | if patternStep == 0 691 | patternStep = P_length; 692 | end 693 | % get pattern multiplier value for junction 694 | ixPattern = pdaDict(thisJunction).ixPattern; 695 | PM_value = patterns(patternStep,ixPattern); 696 | 697 | % calculate actual demand 698 | BD_value = pdaDict(thisJunction).baseDemand; 699 | FCV_setting = BD_value*PM_value; 700 | 701 | % set FCV setting to actual demand 702 | valve_ix = pdaDict(thisJunction).ixFCV; 703 | EpanetHelper.setComponentValue(... 704 | valve_ix,FCV_setting,EpanetHelper.EPANETCODES('EN_SETTING')); 705 | 706 | % set emitter coefficient 707 | emitter_ix = pdaDict(thisJunction).ixEmit; 708 | emitter_coef = self.calcEmitterCoef(FCV_setting,emitter_ix); 709 | % if length(P_des) == 1 710 | % 711 | % else 712 | % error('Different values of Pdes not supported yet'); 713 | % % emitter_coef = calcEmitterCoef(self,... 714 | % % self.epanetMap.HFR,P_min,P_des(i),... 715 | % % self.epanetMap.emitterExponent,FCV_setting,emitter_ix); 716 | % end 717 | 718 | %set emitter coefficient value 719 | EpanetHelper.setComponentValue(... 720 | emitter_ix,emitter_coef,EpanetHelper.EPANETCODES('EN_EMITTER')); 721 | 722 | %% NEW FOR OPTIMISIMUL 723 | % store desired demand to compute unmet demands 724 | dd(thisJunction) = FCV_setting; 725 | end 726 | 727 | %% NEW FOR OPTIMISIMUL 728 | % concatenate des_demands 729 | self.dds = cat(1,self.dds,{dd}); 730 | 731 | end 732 | 733 | function eCoef = calcEmitterCoef(self,demand,emitter_ix) 734 | % calculates emitter coefficient using the specified head-flow-relationship 735 | % equation specified by the user (currently stored in the map) 736 | 737 | % get pda options 738 | emitterExp = self.epanetMap.pdaOptions.emitterExponent; % emitter exp 739 | P_min = self.epanetMap.pdaOptions.Pmin; % minimum pressure 740 | P_des = self.epanetMap.pdaOptions.Pdes; % desired pressure(s) 741 | HFR = self.epanetMap.pdaOptions.HFR; % head flow relationship 742 | 743 | % TO DO: substitute with switch statement 744 | if strcmp(HFR,'Wagner') 745 | eCoef = demand/((P_des-P_min)^emitterExp); 746 | elseif strcmp(HFR,'Salgado-Castro') 747 | if emitterExp == 1.0 748 | eCoef = demand/((P_des-P_min)^emitterExp); 749 | else 750 | error('When using Salgado-Castro HFR, emitter exponent must be 1.0') 751 | end 752 | elseif strcmp(HFR,'Bhave') 753 | eCoef = 1e9; %this is an arbitrarily large value 754 | elseif strcmp(HFR,'Fujiwara') 755 | %This HFR can't be simplified to match the emitter equation 756 | %without reading the pressure value (one timestep prior) 757 | if emitterExp==2.0 758 | EN_PRESSURE = 11; 759 | EN_ELEVATION = 0; 760 | thisPres = EpanetHelper.getComponentValue(... 761 | emitter_ix,1,EN_PRESSURE); 762 | thisElev = EpanetHelper.getComponentValue(... 763 | emitter_ix,1,EN_ELEVATION); 764 | H_des = P_des+thisElev; 765 | H_min = P_min+thisElev; 766 | H = thisPres+thisElev; 767 | if H < H_des 768 | eCoef = demand*((3*P_des-2*thisPres-P_min)/((P_des-P_min)^3)*... 769 | (1+(P_min^2-2*thisPres*P_min)/(thisPres^2))); 770 | else 771 | eCoef = demand/(P_des^2); 772 | end 773 | else 774 | error('When using Fujiwara HFR, emitter exponent must be 2.0') 775 | end 776 | else 777 | error('No valid head-flow relationship specified. Using default. Options: Wagner, Salgado-Castro, Bhave, Fujiwara') 778 | end 779 | end 780 | % end of private methods 781 | end 782 | end 783 | --------------------------------------------------------------------------------