├── README.md ├── backups └── .gitignore ├── configs ├── new_config_gen.py ├── plc_config_gen.py ├── test_config.yaml └── water_treatment_config.yaml ├── logging └── .gitignore ├── plc ├── async_plc.py ├── datastore.py └── helper.py └── startup ├── README_startup_service.md ├── config_file_name.txt ├── master.py ├── plc_startup.service └── startup_plc.sh /README.md: -------------------------------------------------------------------------------- 1 | SCADA Simulator 2 | 3 | Copyright 2018 Carnegie Mellon University. All Rights Reserved. 4 | 5 | NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 6 | 7 | Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 8 | 9 | [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 10 | This Software includes and/or makes use of the following Third-Party Software subject to its own license: 11 | 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 12 | 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 13 | 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 14 | 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 15 | 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 16 | 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 17 | 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 18 | 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 19 | 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 20 | 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 21 | 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 22 | 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 23 | 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 24 | DM18-1351 25 | 26 | 27 | # scadasim_pymodbus_plc 28 | 29 | ## Brief: 30 | - Simulates a SCADA system 31 | - Uses PyModbus to create custom PLC devices 32 | - Simulates Modbus TCP/RTU traffic 33 | 34 | ## Installation: 35 | Experienced issues installing pymodbus using pip, so instead the github repo was used for pymodbus and pip install for any additional dependencies 36 | - `git clone git://github.com/bashwork/pymodbus.git` 37 | - `cd pymodbus` 38 | - `python setup.py install` 39 | - `pip install twisted cryptography bcrypt pyasn1 service_identity` 40 | - Note that for the systemd service, you want sudo permissions to be able to start the PyModbus server and do other things. In that case, you probably have two primary options: 41 | - pip install globally so that root will have access to the required packages (not recommended due to security concerns if there is a bad package) 42 | - Specify in the systemd service config that you want to use a non-root user to run the program (need to figure out how to get that user the privilege to start running a server and other actions) 43 | 44 | ## Initial Setup 45 | - You will first want to generate a master config file to customize the functionality of your PLC devices 46 | - `cd //configs` 47 | - `python plc_config_gen.py` 48 | 49 | - After you have created your master config file, add the full filepath to `config_file_name.txt` 50 | 51 | - The filepaths used throughout the project begin with `/usr/local/bin`. To use this with your current working directory, run the following to make a symlink to this project 52 | - `ln -s /usr/local/bin/scadasim_pymodbus_plc` 53 | 54 | - Next, run your async plc server/client 55 | - `cd //startup` 56 | - `sudo ./startup_plc.sh` 57 | - startup_plc.sh has a while loop with an empty echo to be used as a systemd service without ending prematurely. If you wish to run the .sh script manually, remove the loop 58 | 59 | - Finally, if you want to use a new config file or start your PLCs from scratch, make sure you clear your backups. 60 | - `sudo rm //backups/backup_*` 61 | 62 | - Follow the README_startup_service.md instructions on setting up the systemd job to avoid manually running ./startup_plc.sh each time. 63 | - Make sure that the while loop with the empty echo at the end is there if you are setting it up using systemd 64 | 65 | ## Description: 66 | 67 | ### Uses: 68 | - Python 2.7.10 69 | - Pymodbus 2.2.0 70 | - twisted 19.7.0 71 | - cryptography 2.7 72 | - bcrypt 3.1.7 73 | - pyasn1 0.4.6 74 | 75 | ### Project Overview: 76 | 77 | #### backups 78 | - contain(s) backup_[n].yaml files - to store up to date values for n PLC devices to be able to restart and not start over again 79 | 80 | #### configs 81 | 82 | ##### plc_config_gen.py 83 | - This is used to generate a master config file, accomplished by command line questions 84 | 85 | ##### *_config.yaml 86 | - Master yaml config files to be used in configuring the PLC devices to simulate 87 | 88 | 89 | #### logging 90 | - contain(s) logging_[n].yaml files - to log based on the logging configuration specified in the currently used master yaml configuration 91 | 92 | #### old 93 | - store old files that are not used anymore but good for reference 94 | 95 | #### plc 96 | 97 | ##### async_plc.py 98 | - async_plc.py serves as the main asynchronous Pymodbus server with client functionality 99 | 100 | ##### datastore.py 101 | - datastore.py has wrapper functions that are used to read from/write to the datastore 102 | 103 | ##### helper.py 104 | - helper.py has wrapper functions that are used to generate data variance for the plc devices and to reduce code in async_plc.py 105 | 106 | 107 | #### startup 108 | 109 | ##### README_startup_service.md 110 | - Read this to get this code working on startup using systemd! 111 | 112 | ##### config_file_name.txt 113 | - If no config file name is provided using the vmx template_var, it will look for the file name in this text file 114 | 115 | ##### master.py 116 | - master.py initializes the backup files and returns config information to startup_plc.sh 117 | 118 | ##### plc_startup_service.servcie 119 | - This is used to run the plc devices on startup 120 | - Refer to the README on adding this to your local systemd 121 | 122 | ##### startup_plc.sh 123 | - startup_plc.sh serves as the main startup script to initialize the PLC devices based off of the master config file 124 | 125 | -------------------------------------------------------------------------------- /backups/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmu-sei/SCADASim/d9e4b0147567899fa25c633ea2fd23f03ec809f5/backups/.gitignore -------------------------------------------------------------------------------- /configs/new_config_gen.py: -------------------------------------------------------------------------------- 1 | # SCADA Simulator 2 | # 3 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 4 | # 5 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 6 | # 7 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 8 | # 9 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 10 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 11 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 12 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 13 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 14 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 15 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 16 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 17 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 18 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 19 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 20 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 21 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 22 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 23 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 24 | # DM18-1351 25 | # 26 | 27 | import tkinter as tk 28 | 29 | class ScrolledFrame(tk.Frame): 30 | 31 | def __init__(self, parent, vertical=True, horizontal=False): 32 | super().__init__(parent) 33 | 34 | # canvas for inner frame 35 | self._canvas = tk.Canvas(self) 36 | self._canvas.grid(row=0, column=0, sticky='news') # changed 37 | 38 | # create right scrollbar and connect to canvas Y 39 | self._vertical_bar = tk.Scrollbar(self, orient='vertical', command=self._canvas.yview) 40 | if vertical: 41 | self._vertical_bar.grid(row=0, column=1, sticky='ns') 42 | self._canvas.configure(yscrollcommand=self._vertical_bar.set) 43 | 44 | # create bottom scrollbar and connect to canvas X 45 | self._horizontal_bar = tk.Scrollbar(self, orient='horizontal', command=self._canvas.xview) 46 | if horizontal: 47 | self._horizontal_bar.grid(row=1, column=0, sticky='we') 48 | self._canvas.configure(xscrollcommand=self._horizontal_bar.set) 49 | 50 | # inner frame for widgets 51 | self.inner = tk.Frame(self._canvas) 52 | self._window = self._canvas.create_window((0, 0), window=self.inner, anchor='nw') 53 | 54 | # autoresize inner frame 55 | self.columnconfigure(0, weight=1) # changed 56 | self.rowconfigure(0, weight=1) # changed 57 | 58 | # resize when configure changed 59 | self.inner.bind('', self.resize) 60 | self._canvas.bind('', self.frame_width) 61 | 62 | def frame_width(self, event): 63 | # resize inner frame to canvas size 64 | canvas_width = event.width 65 | self._canvas.itemconfig(self._window, width = canvas_width) 66 | 67 | def resize(self, event=None): 68 | self._canvas.configure(scrollregion=self._canvas.bbox('all')) 69 | 70 | class PLC: 71 | 72 | def __init__(self, parent, num): 73 | self.parent = parent 74 | self.title = "PLC " + str(num + 1) 75 | self.create_widgets() 76 | 77 | def create_widgets(self): 78 | self.labelframe = tk.LabelFrame(self.parent, text=self.title) 79 | self.labelframe.pack(fill="both", expand=True) 80 | 81 | self.label = tk.Label(self.labelframe, text="properties") 82 | self.label.pack(expand=True, fill='both') 83 | 84 | self.entry = tk.Entry(self.labelframe) 85 | self.entry.pack() 86 | 87 | #Creates PLC devices 88 | def build_plc(window, num_of_plc): 89 | destroy_plc(window) 90 | for i in range(int(num_of_plc)): 91 | PLC(window.inner, i) 92 | 93 | #Destroys PLC devices 94 | def destroy_plc(window): 95 | children = window.inner.winfo_children() 96 | for child in children: 97 | if str(type(child)) == "": 98 | child.destroy() 99 | 100 | def main(): 101 | #Create main tkinter frame 102 | root = tk.Tk() 103 | root.title("Config Generator") 104 | #root.geometry("400x300") 105 | 106 | #Create new ScrolledFrame 107 | window = ScrolledFrame(root) 108 | window.pack(expand=True, fill='both') 109 | 110 | #User specifies number of plc devices 111 | label = tk.Label(window.inner, text="Number of PLC devices?") 112 | label.pack(expand=True, fill='both') 113 | 114 | num_of_plc = tk.Entry(window.inner) 115 | num_of_plc.pack(fill=tk.X, padx=5) 116 | 117 | submit_btn = tk.Button(window.inner, text="Submit", command=lambda: build_plc(window, num_of_plc.get())) 118 | submit_btn.pack() 119 | 120 | reset_btn = tk.Button(window.inner, text="Reset", command=lambda: destroy_plc(window)) 121 | reset_btn.pack() 122 | 123 | #start GUI 124 | root.mainloop() 125 | 126 | if __name__ == '__main__': 127 | main() 128 | -------------------------------------------------------------------------------- /configs/plc_config_gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # SCADA Simulator 4 | # 5 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 6 | # 7 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 8 | # 9 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 10 | # 11 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 12 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 13 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 14 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 15 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 16 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 17 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 18 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 19 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 20 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 21 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 22 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 23 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 24 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 25 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 26 | # DM18-1351 27 | # 28 | 29 | 30 | import yaml, sys 31 | 32 | ''' 33 | @brief obtain input on parameters for linear behavior 34 | ''' 35 | def linear_behavior_setup(): 36 | dict = {} 37 | dict['variance'] = int(raw_input("Variance of linear function:\n")) 38 | dict['address'] = raw_input("Starting address for register(s) to control:\n") 39 | dict['time'] = int(raw_input("Frequency to update register values (in seconds):\n")) 40 | dict['count'] = int(raw_input("Number of registers to control:\n")) 41 | return dict 42 | 43 | 44 | def linear_coil_dependent_setup(): 45 | dict = {} 46 | dict = linear_behavior_setup() 47 | dict['max'] = int(raw_input("Max value of the register(s):\n")) 48 | dict['coil_address'] = int(raw_input("Address of the coil it is dependent on:\n")) 49 | dict['default_coil_value'] = int(raw_input("Default coil value - default state that would mean normal behavior:\n")) 50 | return dict 51 | 52 | ''' 53 | @brief obtain input on parameters for random behavior 54 | ''' 55 | def random_behavior_setup(): 56 | dict = {} 57 | dict['min'] = int(raw_input("Minimum value the register can hold:\n")) 58 | dict['max'] = int(raw_input("Maximum value the register can hold:\n")) 59 | dict['address'] = raw_input("Starting address for register(s) to control:\n") 60 | dict['time'] = int(raw_input("Frequency to update register values (in seconds):\n")) 61 | dict['count'] = int(raw_input("Number of registers to control:\n")) 62 | return dict 63 | 64 | def constant_behavior_setup(): 65 | dict = {} 66 | dict['num'] = int(raw_input("Value that the coil register should try to stay constant at:\n")) 67 | dict['address'] = raw_input("Starting address for register(s) to control:\n") 68 | dict['time'] = int(raw_input("Frequency to update register values (in seconds):\n")) 69 | dict['count'] = int(raw_input("Number of registers to control:\n")) 70 | return dict 71 | ''' 72 | @brief obtain input to setup the datastore 73 | ''' 74 | def datastore_setup(): 75 | datastore_dict = {'hr': {'start_addr': 1, 'values': [1, 2, 3]}, 'ir': {'start_addr': 1, 'values': [4, 4, 4]}, 'co': {'start_addr': 1, 'values': [0, 0, 0]}, 'di': {'start_addr': 1, 'values': [100, 250, 0]}} 76 | print("\n\nConfiguring Datastore\n") 77 | # holding reg setup 78 | start_addr = int(raw_input("Start addr for hr?\n")) 79 | values = raw_input("Initial values for hr?\n").split() 80 | values = map(int, values) 81 | datastore_dict['hr']['start_addr'] = start_addr 82 | datastore_dict['hr']['values'] = values 83 | for i in range(len(values)): 84 | datastore_dict['hr']['behavior_' + str(i+1)] = {} 85 | cur_behavior = raw_input("Linear, linear_coil_dependent, or random behavior?\n") 86 | datastore_dict['hr']['behavior_' + str(i+1)]['type'] = cur_behavior 87 | if cur_behavior == "linear": 88 | behavior_sub_dict = linear_behavior_setup() 89 | elif cur_behavior == "linear_coil_dependent": 90 | behavior_sub_dict = linear_coil_dependent_setup() 91 | elif cur_behavior == "random": 92 | behavior_sub_dict = random_behavior_setup() 93 | datastore_dict['hr']['behavior_' + str(i+1)].update(behavior_sub_dict) 94 | 95 | # input reg setup 96 | start_addr = int(raw_input("Start addr for ir?\n")) 97 | values = raw_input("Initial values for ir?\n").split() 98 | values = map(int, values) 99 | datastore_dict['ir']['start_addr'] = start_addr 100 | datastore_dict['ir']['values'] = values 101 | 102 | # coil reg setup 103 | start_addr = int(raw_input("Start addr for co?\n")) 104 | values = raw_input("Initial values for co?\n").split() 105 | values = map(int, values) 106 | datastore_dict['co']['start_addr'] = start_addr 107 | datastore_dict['co']['values'] = values 108 | for i in range(len(values)): 109 | datastore_dict['co']['behavior_' + str(i+1)] = {} 110 | cur_behavior = raw_input("constant or none behavior?\n") 111 | datastore_dict['co']['behavior_' + str(i+1)]['type'] = cur_behavior 112 | if cur_behavior == "constant": 113 | behavior_sub_dict = constant_behavior_setup() 114 | datastore_dict['co']['behavior_' + str(i+1)].update(behavior_sub_dict) 115 | 116 | # di reg setup 117 | start_addr = int(raw_input("Start addr for di?\n")) 118 | values = raw_input("Initial values for di?\n").split() 119 | values = map(int, values) 120 | datastore_dict['di']['start_addr'] = start_addr 121 | datastore_dict['di']['values'] = values 122 | return datastore_dict 123 | 124 | ''' 125 | @brief obtain input on logging setup 126 | ''' 127 | def logging_setup(): 128 | logging_dict = {'logging_level': 'DEBUG', 'file': 'STDOUT', 'format': '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s'} 129 | def_format = '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s' 130 | print("\n\nConfiguring Logging\n") 131 | logging_dict['logging_level'] = raw_input("Enter logging level (CRITICAL, ERROR, WARNING, INFO, DEBUG, or NOTSET) :\n") 132 | logging_dict['file'] = raw_input("Enter STDOUT or valid filepath for logging destination:\n") 133 | logging_dict['format'] = raw_input("Enter NONE, DEFAULT (for '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s'), or a valid format string:\n") 134 | if logging_dict['format'] == 'DEFAULT': 135 | logging_dict['format'] = def_format 136 | return logging_dict 137 | 138 | ''' 139 | @brief obtain input on PyModbus Server setup 140 | ''' 141 | def server_setup(): 142 | server_dict = {'framer': 'RTU', 'type': 'serial', 'port': '/dev/ttyS1'} 143 | print("\n\nConfiguring Server\n") 144 | server_dict['type'] = raw_input("Enter type of PyModbus server (tcp, serial, etc):\n") 145 | server_dict['framer'] = raw_input("Enter type of framer (TCP, RTU, ASCII, etc):\n") 146 | server_dict['port'] = raw_input("Enter port used for server:\n") 147 | server_dict['address'] = raw_input("Enter address used for server (NONE if using serial server):\n") 148 | return server_dict 149 | 150 | ''' 151 | @brief calls all of the other functions - encapsulates the setup of a PLC device 152 | ''' 153 | def plc_setup(): 154 | plc_config = {} 155 | plc_config['DATASTORE'] = datastore_setup() 156 | plc_config['LOGGING'] = logging_setup() 157 | plc_config['SERVER'] = server_setup() 158 | return plc_config 159 | 160 | ''' 161 | @brief generates a master config file in YAML format 162 | - Determines the number of PLC devices then calls plc_setup(), which in turn calls other functions, in order to finish it up and yaml.dump it to the config yaml file for later use 163 | ''' 164 | def main(): 165 | print("SCADASim 2.0 PLC config generator\n") 166 | 167 | config_dict = {'MASTER': {'num_of_PLC': 1}} 168 | num_devices = input("How many PLC devices? ") 169 | config_dict['MASTER']['num_of_PLC'] = int(num_devices) 170 | output_filename = raw_input("Enter the full path of the file the config should yaml.dump to OR enter 'DEFAULT': ") 171 | if output_filename == "DEFAULT": 172 | dump_filename = '/usr/local/bin/scadasim_pymodbus_plc/configs/test_generator_dump.yaml' 173 | else: 174 | dump_filename = output_filename 175 | 176 | if len(sys.argv) == 2: 177 | dump_filename = sys.argv[1] 178 | for i in range(int(num_devices)): 179 | print("\n\nConfiguring PLC " + str(i)) 180 | config_dict["PLC " + str(i)] = plc_setup() 181 | print(config_dict) 182 | stream = open(dump_filename, 'w+') 183 | yaml.dump(config_dict, stream) 184 | stream.close() 185 | 186 | if __name__ == "__main__": 187 | main() 188 | -------------------------------------------------------------------------------- /configs/test_config.yaml: -------------------------------------------------------------------------------- 1 | # SCADA Simulator 2 | # 3 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 4 | # 5 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 6 | # 7 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 8 | # 9 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 10 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 11 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 12 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 13 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 14 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 15 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 16 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 17 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 18 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 19 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 20 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 21 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 22 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 23 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 24 | # DM18-1351 25 | # 26 | 27 | MASTER: 28 | num_of_PLC: 4 29 | PLC 0: 30 | DATASTORE: 31 | co: 32 | start_addr: 1 33 | values: 34 | - 0 35 | behavior_1: {'type': 'none'} 36 | di: 37 | start_addr: 1 38 | values: 39 | - 1 40 | hr: 41 | start_addr: 1 42 | values: 43 | - 100 44 | behavior_1: {'type': 'fuel_tank_behavior', 'min': 0, 'max': 100, 'address': 0x00, 'time': 0, 'count': 1, 'coil_address': 0x00} 45 | ir: 46 | start_addr: 1 47 | values: 48 | - 0 49 | LOGGING: 50 | file: /usr/local/bin/scadasim_pymodbus_plc/logging/logging_0.log 51 | format: '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s 52 | %(message)s' 53 | logging_level: DEBUG 54 | SERVER: 55 | framer: TCP 56 | port: 5020 57 | type: tcp 58 | address: 0.0.0.0 59 | PLC 1: 60 | DATASTORE: 61 | co: 62 | start_addr: 1 63 | values: 64 | - 0 65 | behavior_1: {'type': 'none'} 66 | di: 67 | start_addr: 1 68 | values: 69 | - 0 70 | hr: 71 | start_addr: 1 72 | values: 73 | - 90 74 | behavior_1: {'type': 'random', 'min': 0, 'max': 5, 'address': 0x00, 'time': 5, 'count': 1} 75 | ir: 76 | start_addr: 1 77 | values: 78 | - 0 79 | LOGGING: 80 | file: /usr/local/bin/scadasim_pymodbus_plc/logging/logging_1.log 81 | format: '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s 82 | %(message)s' 83 | logging_level: INFO 84 | SERVER: 85 | framer: TCP 86 | port: 5021 87 | type: tcp 88 | address: 0.0.0.0 89 | PLC 2: 90 | DATASTORE: 91 | co: 92 | start_addr: 1 93 | values: 94 | - 0 95 | behavior_1: {'type': 'none'} 96 | di: 97 | start_addr: 1 98 | values: 99 | - 0 100 | hr: 101 | start_addr: 1 102 | values: 103 | - 100 104 | behavior_1: {'type': 'fuel_tank_behavior', 'min': 0, 'max': 100, 'address': 0x00, 'time': 0, 'count': 1, 'coil_address': 0x00} 105 | ir: 106 | start_addr: 1 107 | values: 108 | - 0 109 | LOGGING: 110 | file: /usr/local/bin/scadasim_pymodbus_plc/logging/logging_2.log 111 | format: '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s 112 | %(message)s' 113 | logging_level: INFO 114 | SERVER: 115 | framer: TCP 116 | port: 5022 117 | type: tcp 118 | address: 0.0.0.0 119 | PLC 3: 120 | DATASTORE: 121 | co: 122 | start_addr: 1 123 | values: 124 | - 0 125 | behavior_1: {'type': 'none'} 126 | di: 127 | start_addr: 1 128 | values: 129 | - 0 130 | hr: 131 | start_addr: 1 132 | values: 133 | - 90 134 | behavior_1: {'type': 'random', 'min': 0, 'max': 5, 'address': 0x00, 'time': 5, 'count': 1} 135 | ir: 136 | start_addr: 1 137 | values: 138 | - 0 139 | LOGGING: 140 | file: /usr/local/bin/scadasim_pymodbus_plc/logging/logging_3.log 141 | format: '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s 142 | %(message)s' 143 | logging_level: INFO 144 | SERVER: 145 | framer: TCP 146 | port: 5023 147 | type: tcp 148 | address: 0.0.0.0 149 | -------------------------------------------------------------------------------- /configs/water_treatment_config.yaml: -------------------------------------------------------------------------------- 1 | # SCADA Simulator 2 | # 3 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 4 | # 5 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 6 | # 7 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 8 | # 9 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 10 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 11 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 12 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 13 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 14 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 15 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 16 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 17 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 18 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 19 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 20 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 21 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 22 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 23 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 24 | # DM18-1351 25 | # 26 | 27 | MASTER: 28 | num_of_PLC: 4 29 | PLC 0: 30 | DATASTORE: 31 | co: 32 | start_addr: 1 33 | values: 34 | - 0 35 | behavior_1: {'type': 'none'} 36 | di: 37 | start_addr: 1 38 | values: 39 | - 1 40 | hr: 41 | start_addr: 1 42 | values: 43 | - 0 44 | behavior_1: {'type': 'linear_coil_dependent', 'variance': 1, 'max': 1, 'address': 0x00, 'time': 2, 'count': 1, 'coil_address': 0x00, 'default_coil_value': 1} 45 | ir: 46 | start_addr: 1 47 | values: 48 | - 0 49 | LOGGING: 50 | file: /usr/local/bin/scadasim_pymodbus_plc/logging/logging_0.log 51 | format: '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s 52 | %(message)s' 53 | logging_level: INFO 54 | SERVER: 55 | framer: TCP 56 | port: 5020 57 | type: tcp 58 | address: 0.0.0.0 59 | PLC 1: 60 | DATASTORE: 61 | co: 62 | start_addr: 1 63 | values: 64 | - 0 65 | behavior_1: {'type': 'none'} 66 | di: 67 | start_addr: 1 68 | values: 69 | - 0 70 | hr: 71 | start_addr: 1 72 | values: 73 | - 0 74 | behavior_1: {'type': 'linear_coil_dependent', 'variance': 1, 'max': 10, 'address': 0x00, 'time': 2, 'count': 1, 'coil_address': 0x00, 'default_coil_value': 1} 75 | ir: 76 | start_addr: 1 77 | values: 78 | - 0 79 | LOGGING: 80 | file: /usr/local/bin/scadasim_pymodbus_plc/logging/logging_1.log 81 | format: '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s 82 | %(message)s' 83 | logging_level: INFO 84 | SERVER: 85 | framer: TCP 86 | port: 5021 87 | type: tcp 88 | address: 0.0.0.0 89 | PLC 2: 90 | DATASTORE: 91 | co: 92 | start_addr: 1 93 | values: 94 | - 0 95 | behavior_1: {'type': 'none'} 96 | di: 97 | start_addr: 1 98 | values: 99 | - 0 100 | hr: 101 | start_addr: 1 102 | values: 103 | - 0 104 | behavior_1: {'type': 'linear_coil_dependent', 'variance': 10, 'max': 150, 'address': 0x00, 'time': 2, 'count': 1, 'coil_address': 0x00, 'default_coil_value': 1} 105 | ir: 106 | start_addr: 1 107 | values: 108 | - 0 109 | LOGGING: 110 | file: /usr/local/bin/scadasim_pymodbus_plc/logging/logging_2.log 111 | format: '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s 112 | %(message)s' 113 | logging_level: INFO 114 | SERVER: 115 | framer: TCP 116 | port: 5022 117 | type: tcp 118 | address: 0.0.0.0 119 | PLC 3: 120 | DATASTORE: 121 | co: 122 | start_addr: 1 123 | values: 124 | - 0 125 | behavior_1: {'type': 'none'} 126 | di: 127 | start_addr: 1 128 | values: 129 | - 0 130 | hr: 131 | start_addr: 1 132 | values: 133 | - 0 134 | behavior_1: {'type': 'linear_coil_dependent', 'variance': 25, 'max': 450, 'address': 0x00, 'time': 2, 'count': 1, 'coil_address': 0x00, 'default_coil_value': 1} 135 | ir: 136 | start_addr: 1 137 | values: 138 | - 0 139 | LOGGING: 140 | file: /usr/local/bin/scadasim_pymodbus_plc/logging/logging_3.log 141 | format: '%(asctime)-15s %(threadName)-15s %(levelname)-8s %(module)-15s:%(lineno)-8s 142 | %(message)s' 143 | logging_level: INFO 144 | SERVER: 145 | framer: TCP 146 | port: 5023 147 | type: tcp 148 | address: 0.0.0.0 149 | -------------------------------------------------------------------------------- /logging/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmu-sei/SCADASim/d9e4b0147567899fa25c633ea2fd23f03ec809f5/logging/.gitignore -------------------------------------------------------------------------------- /plc/async_plc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # SCADA Simulator 4 | # 5 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 6 | # 7 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 8 | # 9 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 10 | # 11 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 12 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 13 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 14 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 15 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 16 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 17 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 18 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 19 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 20 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 21 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 22 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 23 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 24 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 25 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 26 | # DM18-1351 27 | # 28 | 29 | 30 | ''' 31 | Asynchronous PyModbus Server with Client Functionality 32 | Used for SCADASim 2.0 33 | ''' 34 | 35 | # --------------------------------------------------------------------------- # 36 | # import the modbus libraries we need 37 | # --------------------------------------------------------------------------- # 38 | from pymodbus.server.asynchronous import StartSerialServer 39 | from pymodbus.server.asynchronous import StartTcpServer 40 | from pymodbus.server.asynchronous import StartUdpServer 41 | from pymodbus.device import ModbusDeviceIdentification 42 | from pymodbus.datastore import ModbusSequentialDataBlock 43 | from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext 44 | from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer, ModbusBinaryFramer 45 | 46 | # --------------------------------------------------------------------------- # 47 | # import the other libraries we need 48 | # --------------------------------------------------------------------------- # 49 | from datastore import * 50 | from helper import * 51 | from time import * 52 | from threading import Thread 53 | import logging, yaml 54 | import sys, os, argparse 55 | 56 | 57 | ''' 58 | @brief reads from backup, initializes the datastore, starts the backup thread and the register behavior threads, then starts the server 59 | ''' 60 | def run_updating_server(config_list, backup_filename, log): 61 | # ----------------------------------------------------------------------- # 62 | # initialize your data store 63 | # ----------------------------------------------------------------------- # 64 | # Run datastore_backup_on_start to use the most recent values of the datablocks, as the layout in the master config will only reflect initial values 65 | # If this is the first time this is used, the backup file will match up with what is laid out in the master config (due to master.py) 66 | datastore_config = datastore_backup_on_start(backup_filename) 67 | if datastore_config == -1: 68 | print("Issue with backup file - either not created or empty. Exiting program.") 69 | sys.exit() 70 | 71 | store = ModbusSlaveContext( 72 | di=ModbusSequentialDataBlock(datastore_config['di']['start_addr'], datastore_config['di']['values']), 73 | co=ModbusSequentialDataBlock(datastore_config['co']['start_addr'], datastore_config['co']['values']), 74 | hr=ModbusSequentialDataBlock(datastore_config['hr']['start_addr'], datastore_config['hr']['values']), 75 | ir=ModbusSequentialDataBlock(datastore_config['ir']['start_addr'], datastore_config['ir']['values'])) 76 | # Could have multiple slaves, with their own addressing. Since we have 1 PLC device handled by every async_plc.py, it is not necessary 77 | context = ModbusServerContext(slaves=store, single=True) 78 | 79 | # setup a thread with target as datastore_backup_to_yaml to start here, before other threads 80 | # this will continuously read from the context to write to a backup yaml file 81 | backup_thread = Thread(target=datastore_backup_to_yaml, args=(context, backup_filename)) 82 | backup_thread.daemon = True 83 | backup_thread.start() 84 | 85 | # start register behaviors. Updating writer is started off, which will spawn a thread for every holding register based on the config 86 | thread = Thread(target=updating_writer, args=(context, config_list, time, log, backup_filename)) 87 | thread.daemon = True 88 | thread.start() 89 | 90 | # Starting the server 91 | server_config = config_list['SERVER'] 92 | framer = configure_server_framer(server_config) 93 | if server_config['type'] == 'serial': 94 | StartSerialServer(context, port=server_config['port'], framer=framer) 95 | elif server_config['type'] == 'udp': 96 | StartUdpServer(context, identity=identity, address=(server_config['address'], int(server_config['port']))) 97 | elif server_config['type'] == 'tcp': 98 | if server_config['framer'] == 'RTU': 99 | StartTcpServer(context, identity=identity, address=(server_config['address'], int(server_config['port'])), framer=framer) 100 | else: 101 | StartTcpServer(context, address=(server_config['address'], int(server_config['port']))) 102 | 103 | ''' 104 | @brief parse args, handle master config, setup logging, then call run_updating_server 105 | ''' 106 | def main(): 107 | # --- BEGIN argparse handling --- 108 | parser = argparse.ArgumentParser(description = "Main program for PLC device based off PyModbus") 109 | parser.add_argument("--n", "--num_of_PLC", help = "The number of the PLC device") 110 | parser.add_argument("--c", "--config_filename", help = "Name of the master config file") 111 | args = parser.parse_args() 112 | if args.n is None or args.c is None: 113 | print("Need to run async_plc.py with --n and --c arguments. Run 'python async_plc.py --h' for help") 114 | return 115 | print( args ) 116 | num_of_PLC = args.n 117 | master_config_filename = args.c 118 | backup_filename = '/usr/local/bin/scadasim_pymodbus_plc/backups/backup_' + args.n + '.yaml' 119 | # --- END argparse handling --- 120 | 121 | stream = open(master_config_filename, 'r') 122 | config_list = yaml.safe_load(stream) 123 | stream.close() 124 | # Only get the current PLC's configuration dictionary 125 | config_list = config_list["PLC " + num_of_PLC] 126 | 127 | # --- BEGIN LOGGING SETUP --- 128 | FORMAT = config_list['LOGGING']['format'] 129 | # Add logic based on whether a file is used or stdout 130 | # AND whether a format string is used or not 131 | if config_list['LOGGING']['file'] == 'STDOUT': 132 | if FORMAT == 'NONE': 133 | logging.basicConfig() 134 | else: 135 | logging.basicConfig(format=FORMAT) 136 | else: 137 | if FORMAT == 'NONE': 138 | logging.basicConfig(filename=config_list['LOGGING']['file']) 139 | else: 140 | logging.basicConfig(format=FORMAT, filename=config_list['LOGGING']['file']) 141 | log = logging.getLogger() 142 | configure_logging_level(config_list['LOGGING']['logging_level'], log) 143 | # --- END LOGGING SETUP --- 144 | run_updating_server(config_list, backup_filename, log) 145 | 146 | 147 | if __name__ == "__main__": 148 | main() 149 | -------------------------------------------------------------------------------- /plc/datastore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # SCADA Simulator 4 | # 5 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 6 | # 7 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 8 | # 9 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 10 | # 11 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 12 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 13 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 14 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 15 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 16 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 17 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 18 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 19 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 20 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 21 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 22 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 23 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 24 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 25 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 26 | # DM18-1351 27 | # 28 | 29 | 30 | ''' 31 | - Helper functions to read from/write to the datastore 32 | - ***NOTE***: Read/write wrapper functions only support when ModbusServerContext is setup with single=True 33 | - Will be looking at implementing to allow for single=False 34 | - Datastore is broken down into the following: 35 | - di - discrete input - read only, boolean - 2 36 | - co - coil output - read and write, boolean - 1 37 | - hr - holding register - read and write - 3 38 | - ir - input register - read only - 4 39 | ''' 40 | 41 | def read_di_register(context, slave_id, addr, count): 42 | return context.getValues(2, addr, count) 43 | 44 | def read_co_register(context, slave_id, addr, count): 45 | return context.getValues(1, addr, count) 46 | 47 | def write_co_register(context, slave_id, addr, values): 48 | return context.setValues(1, addr, values) 49 | 50 | def read_hr_register(context, slave_id, addr, count): 51 | return context.getValues(3, addr, count) 52 | 53 | def write_hr_register(context, slave_id, addr, values): 54 | return context.setValues(3, addr, values) 55 | 56 | def read_ir_register(context, slave_id, addr, count): 57 | return context.getValues(4, addr, count) 58 | 59 | -------------------------------------------------------------------------------- /plc/helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # SCADA Simulator 4 | # 5 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 6 | # 7 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 8 | # 9 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 10 | # 11 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 12 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 13 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 14 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 15 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 16 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 17 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 18 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 19 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 20 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 21 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 22 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 23 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 24 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 25 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 26 | # DM18-1351 27 | # 28 | 29 | 30 | ''' 31 | Helper functions AND functions to be used for register behavior. 32 | 33 | (Reg behavior) Functions to be used to generate data variance for the plc devices. 34 | 35 | These functions accept: 36 | minimum value 37 | maximom value 38 | slave context 39 | 40 | These functions return: 41 | a set of values to be written to the slave context 42 | ''' 43 | import sys, logging, yaml 44 | from os import path 45 | from threading import Thread 46 | from time import * 47 | from random import * 48 | from datastore import * 49 | from pymodbus.transaction import (ModbusRtuFramer, 50 | ModbusAsciiFramer, 51 | ModbusBinaryFramer) 52 | 53 | ''' 54 | linear() will update registers/coils in a linear function 55 | - It will continue to run until it receives 'ctrl+c' KeyboardInterrupt 56 | ''' 57 | 58 | def linear(variance, time, address, slave_id, count, context, log, my_backup): 59 | try: 60 | while(True): 61 | sleep(time) 62 | values = read_hr_register(context[0], slave_id, address, count) 63 | values = [v + variance for v in values] 64 | write_hr_register(context[0], slave_id, address, values) 65 | log.debug(values) 66 | except: 67 | sys.exit() 68 | 69 | ''' 70 | linear_coil_dependent() will update registers/coils in a linear function until it reaches the max value specified 71 | - Currently will not decrement if it will fall below 0 72 | - Also will not increment if it goes past max 73 | - It will continue to run until it receives 'ctrl+c' KeyboardInterrupt 74 | - If the coil matches what the default_coil_value is, it will continue to add variance to the holding register normally 75 | - Otherwise, it will negate the variance and add it to the holding register 76 | - The holding register will be checked every 'time' seconds 77 | ''' 78 | def linear_coil_dependent(variance, max, time, address, slave_id, count, context, log, my_backup, coil_address, default_coil_value): 79 | try: 80 | while(True): 81 | sleep(time) 82 | coil_reg = read_co_register(context[0], slave_id, coil_address, 1) 83 | # the datastore helper functions return a list, even if it is just one register being read 84 | coil_reg = coil_reg[0] 85 | # check the state of the coil 86 | if coil_reg == "false" or int(coil_reg) == 0: 87 | coil_val = 0 88 | elif coil_reg == "true" or int(coil_reg) == 1: 89 | coil_val = 1 90 | # compare the current state of the coil to the default coil value 91 | if coil_val == int(default_coil_value): 92 | values = read_hr_register(context[0], slave_id, address, count) 93 | # check to see if exceeded max value 94 | if(values[0] >= max): 95 | values[0] = max 96 | else: 97 | values[0] = values[0] + variance 98 | # values = [v + variance for v in values] 99 | write_hr_register(context[0], slave_id, address, values) 100 | log.debug(values) 101 | else: # not default coil value - negate variance and add it to holding register in order to do opposite behavior 102 | values = read_hr_register(context[0], slave_id, address, count) 103 | all_greaterthan_0 = True 104 | for v in values: 105 | if v <= 0: 106 | all_greaterthan_0 = False 107 | values = [v + (variance*-1) for v in values] 108 | if all_greaterthan_0: 109 | write_hr_register(context[0], slave_id, address, values) 110 | log.debug(values) 111 | except: 112 | sys.exit() 113 | 114 | ''' 115 | random_num() will update the registers/coils randomly 116 | - Will only generate random values between 'min' and 'max' 117 | - It will continue to run until it receives 'ctr+c' KeyboardInterrupt 118 | ''' 119 | def random_num(min, max, time, address, slave_id, count, context, log, my_backup): 120 | try: 121 | while(True): 122 | sleep(time) 123 | values = read_hr_register(context[0], slave_id, address, count) 124 | variance = randint(min, max) 125 | values = [(v*0) + variance for v in values] 126 | write_hr_register(context[0], slave_id, address, values) 127 | log.debug(values) 128 | except: 129 | sys.exit() 130 | 131 | ''' 132 | random_coil_dependent() will update registers/coils in a linear function until it reaches the max value specified, then it will begin random data variance 133 | - It will continue to run until it receives 'ctr+c' KeyboardInterrupt 134 | ''' 135 | def random_coil_dependent(variance, max, rand_min, rand_max, time, address, slave_id, count, context, log, my_backup, coil_address, default_coil_value): 136 | try: 137 | # false until max is reached 138 | at_max = False 139 | while(True): 140 | sleep(time) 141 | coil_reg = read_co_register(context[0], slave_id, coil_address, 1) 142 | # the datastore helper functions return a list, even if it is just one register being read 143 | coil_reg = coil_reg[0] 144 | values = read_hr_register(context[0], slave_id, address, count) 145 | # checking to see if max value was reached to begin random data variance 146 | if(values[0] >= max): 147 | at_max = True 148 | # check the state of the coil 149 | if coil_reg == "false" or int(coil_reg) == 0: 150 | coil_val = 0 151 | elif coil_reg == "true" or int(coil_reg) == 1: 152 | coil_val = 1 153 | # compare the current state of the coil to the default coil value 154 | if coil_val == int(default_coil_value): 155 | # if at max select random int 156 | if(at_max == True): 157 | values[0] = randint(rand_min, rand_max) 158 | # check to see if exceeded max 159 | elif(values[0] >= max): 160 | values[0] = max 161 | else: 162 | values[0] = values[0] + variance 163 | # values = [v + variance for v in values] 164 | write_hr_register(context[0], slave_id, address, values) 165 | log.debug(values) 166 | else: # not default coil value - negate variance and add it to holding register in order to do opposite behavior 167 | values = read_hr_register(context[0], slave_id, address, count) 168 | all_greaterthan_0 = True 169 | for v in values: 170 | if v <= 0: 171 | all_greaterthan_0 = False 172 | if all_greaterthan_0: 173 | # no longer at max value, do not do random variance 174 | at_max = False 175 | values = [v + (variance*-1) for v in values] 176 | if(values[0] < 0): 177 | values[0] = 0 178 | write_hr_register(context[0], slave_id, address, values) 179 | log.debug(values) 180 | except: 181 | sys.exit() 182 | 183 | ''' 184 | constant_num() will update the registers/coils with a constant value 185 | - Will generate constant value to coil register 186 | - It will continue to run until it receives 'ctr+c' KeyboardInterrupt 187 | ''' 188 | def constant_num(num, time, address, slave_id, count, context, log, my_backup): 189 | try: 190 | while(True): 191 | sleep(time) 192 | values = read_co_register(context[0], slave_id, address, count) 193 | variance = num 194 | values = [(v*0) + variance for v in values] 195 | write_co_register(context[0], slave_id, address, values) 196 | log.debug(values) 197 | except: 198 | sys.exit() 199 | 200 | def fuel_tank_behavior(min, max, time, address, slave_id, count, context, log, my_backup, coil_address): 201 | print( "Thread started for fuel_tank_behavior" ) 202 | try: 203 | while True: 204 | for i in range(0, 2): 205 | # decrement behavior - decrement tank by 25% about every 15 min 206 | # open coil 207 | coil_values = read_co_register(context[0], slave_id, coil_address, 1) 208 | coil_values = [1 for v in coil_values] 209 | write_co_register(context[0], slave_id, address, coil_values) 210 | for j in range(0, 25): # take 25 seconds to decrement fuel tank level by 25% 211 | values = read_hr_register(context[0], slave_id, address, count) 212 | for v in values: 213 | if v > min: 214 | values = [(v-1) for v in values] 215 | write_hr_register(context[0], slave_id, address, values) 216 | log.debug(values) 217 | sleep(1) 218 | 219 | # close coil 220 | coil_values = read_co_register(context[0], slave_id, coil_address, 1) 221 | coil_values = [0 for v in coil_values] 222 | write_co_register(context[0], slave_id, address, coil_values) 223 | 224 | sleep_val = 875 225 | # sleep for about 15 minutes, depending on whether we decremented or also incremented 226 | sleep(sleep_val) 227 | # increment behavior - refill tank to 100% about every hour 228 | if i == 1: 229 | # open coil 230 | log.debug("Increment behavior entered\n") 231 | coil_values = read_co_register(context[0], slave_id, coil_address, 1) 232 | coil_values = [1 for v in coil_values] 233 | write_co_register(context[0], slave_id, address, coil_values) 234 | 235 | for k in range(0, 100): # take 100 seconds to refill fuel tank back to 100 236 | values = read_hr_register(context[0], slave_id, address, count) 237 | for v in values: 238 | if v < max: 239 | values = [(v+1) for v in values] 240 | write_hr_register(context[0], slave_id, address, values) 241 | log.debug(values) 242 | sleep(1) 243 | 244 | # close coil 245 | coil_values = read_co_register(context[0], slave_id, coil_address, 1) 246 | coil_values = [0 for v in coil_values] 247 | write_co_register(context[0], slave_id, address, coil_values) 248 | 249 | sleep_val = 775 250 | sleep(900) 251 | except: 252 | sys.exit() 253 | 254 | 255 | """ 256 | A worker process that runs every so often and 257 | updates live values of the context. It should be noted 258 | that there is a race condition for the update. 259 | 260 | :param arguments: The input arguments to the call 261 | """ 262 | 263 | ''' 264 | updating_writer parses the DATASTORE section of the config for the calling PLC device 265 | to start threads for each holding register based on the type of behavior and the parameters 266 | specified 267 | - Currently designed to have one thread per holding register 268 | - Currently does not handle 'di' or 'ir' register types 269 | ''' 270 | def updating_writer(context, config_list, time, log, backup_filename): 271 | # load in config list to generate thread behavior 272 | values = config_list['DATASTORE']['hr']['values'] 273 | size = len(values) 274 | i = 0 275 | # loop through each holding register 276 | while(i < size): 277 | log.debug("updating the context") 278 | name = 'behavior_' + str(i + 1) 279 | slave_id = 0x00 280 | time = config_list['DATASTORE']['hr'][name]['time'] 281 | address = config_list['DATASTORE']['hr'][name]['address'] 282 | count = config_list['DATASTORE']['hr'][name]['count'] 283 | target = '' 284 | args = () 285 | 286 | # check to see what behavior to use 287 | if (config_list['DATASTORE']['hr'][name]['type'] == 'linear'): 288 | # collect values from master config 289 | variance = config_list['DATASTORE']['hr'][name]['variance'] 290 | target = linear 291 | args = (variance, time, address, slave_id, count, context, log, backup_filename) 292 | 293 | elif (config_list['DATASTORE']['hr'][name]['type'] == 'linear_coil_dependent'): 294 | variance = config_list['DATASTORE']['hr'][name]['variance'] 295 | coil_address = config_list['DATASTORE']['hr'][name]['coil_address'] 296 | default_coil_value = config_list['DATASTORE']['hr'][name]['default_coil_value'] 297 | maximum = config_list['DATASTORE']['hr'][name]['max'] 298 | target = linear_coil_dependent 299 | args = (variance, maximum, time, address, slave_id, count, context, log, backup_filename, coil_address, default_coil_value) 300 | 301 | elif (config_list['DATASTORE']['hr'][name]['type'] == 'random'): 302 | # collect values from master config 303 | minimum = config_list['DATASTORE']['hr'][name]['min'] 304 | maximum = config_list['DATASTORE']['hr'][name]['max'] 305 | target = random_num 306 | args = (minimum, maximum, time, address, slave_id, count, context, log, backup_filename) 307 | 308 | elif (config_list['DATASTORE']['hr'][name]['type'] == 'random_coil_dependent'): 309 | variance = config_list['DATASTORE']['hr'][name]['variance'] 310 | coil_address = config_list['DATASTORE']['hr'][name]['coil_address'] 311 | default_coil_value = config_list['DATASTORE']['hr'][name]['default_coil_value'] 312 | maximum = config_list['DATASTORE']['hr'][name]['max'] 313 | rand_min = config_list['DATASTORE']['hr'][name]['rand_min'] 314 | rand_max = config_list['DATASTORE']['hr'][name]['rand_max'] 315 | target = random_coil_dependent 316 | args = (variance, maximum, rand_min, rand_max, time, address, slave_id, count, context, log, backup_filename, coil_address, default_coil_value) 317 | 318 | elif (config_list['DATASTORE']['hr'][name]['type'] == 'fuel_tank_behavior'): 319 | print( "successfully found fuel_tank_behavior" ) 320 | minimum = config_list['DATASTORE']['hr'][name]['min'] 321 | maximum = config_list['DATASTORE']['hr'][name]['max'] 322 | coil_address = config_list['DATASTORE']['hr'][name]['coil_address'] 323 | target = fuel_tank_behavior 324 | args = (minimum, maximum, time, address, slave_id, count, context, log, backup_filename, coil_address) 325 | 326 | 327 | # start thread 328 | thread = Thread(target=target, args=args) 329 | thread.daemon = True 330 | thread.start() 331 | 332 | # iterate to next thread 333 | i = i + 1 334 | 335 | # load in config list to generate thread behavior for coil registers 336 | co_values = config_list['DATASTORE']['co']['values'] 337 | co_size = len(co_values) 338 | is_behavior = False # Allow for us to add behaviors only for some coil registers if we want to 339 | j = 0 340 | while(j < co_size): 341 | log.debug("updating the context") 342 | name = 'behavior_' + str(j + 1) 343 | slave_id = 0x00 344 | # Moved time/address/count collection to if logic for constant 345 | # If we add more behaviors in the future for coils, can move it back up here and add logic 346 | # to check that behavior_N['type'] != 'none' before getting time/address/count values 347 | target = '' 348 | args = () 349 | 350 | # check to see what behavior to use. If it does not match any, don't start a thread 351 | if (config_list['DATASTORE']['co'][name]['type'] == 'constant'): 352 | # collect values from master config 353 | time = config_list['DATASTORE']['co'][name]['time'] 354 | address = config_list['DATASTORE']['co'][name]['address'] 355 | count = config_list['DATASTORE']['co'][name]['count'] 356 | num = config_list['DATASTORE']['co'][name]['num'] 357 | target = constant_num 358 | args = (num, time, address, slave_id, count, context, log, backup_filename) 359 | is_behavior = True 360 | else: 361 | # invalid type name or no behavior 362 | is_behavior = False 363 | 364 | # start thread if it is a valid behavior 365 | if is_behavior: 366 | thread = Thread(target=target, args=args) 367 | thread.daemon = True 368 | thread.start() 369 | 370 | # iterate to the next coil register to check for behavior 371 | j = j + 1 372 | 373 | ''' 374 | - @brief datastore_backup_on_start will run before the datablock/slave/server contexts are set up in order to start the server with the last known good state of the server context 375 | - It updates the local datastore_config object with the corresponding values in backup_filename 376 | - and then exits 377 | ''' 378 | def datastore_backup_on_start(my_backup): 379 | if (path.exists(my_backup) == False or path.getsize(my_backup) == 0): 380 | return -1 381 | backup = open(my_backup, 'r') 382 | backup_file = yaml.safe_load(backup) 383 | backup.close() 384 | 385 | return backup_file['DATASTORE'] 386 | 387 | ''' 388 | - @brief datastore_backup_to_yaml will run continuously to READ from the context to update the entries in the datastore backup file in YAML format 389 | - It should be run from async_plc.py as a thread to continuously run 390 | - It should start running before the other threads (for register behavior) starts running, but after the datastore context has been setup 391 | ''' 392 | def datastore_backup_to_yaml(context, my_backup): 393 | backup = open(my_backup, 'r') 394 | backup_file = yaml.safe_load(backup) 395 | backup.close() 396 | yaml_file = '' 397 | num_of_co = len(backup_file['DATASTORE']['co']['values']) 398 | num_of_di = len(backup_file['DATASTORE']['di']['values']) 399 | num_of_hr = len(backup_file['DATASTORE']['hr']['values']) 400 | num_of_ir = len(backup_file['DATASTORE']['ir']['values']) 401 | try: 402 | while(True): 403 | # Eventually change sleep value to a parameter - there are issues with the backup file if there is no sleep at all 404 | sleep(1) 405 | values = read_co_register(context[0], 0x00, 0x00, num_of_co) 406 | backup_file['DATASTORE']['co']['values'] = values 407 | values = read_di_register(context[0], 0x00, 0x00, num_of_di) 408 | backup_file['DATASTORE']['di']['values'] = values 409 | values = read_hr_register(context[0], 0x00, 0x00, num_of_hr) 410 | backup_file['DATASTORE']['hr']['values'] = values 411 | values = read_ir_register(context[0], 0x00, 0x00, num_of_ir) 412 | backup_file['DATASTORE']['ir']['values'] = values 413 | 414 | yaml_file = open(my_backup, 'w') 415 | yaml.dump(backup_file, yaml_file, default_flow_style=False) 416 | yaml_file.close() 417 | except: 418 | if(yaml_file.closed == False): 419 | yaml_file.close() 420 | sys.exit() 421 | 422 | ''' 423 | Used to configure logging and clean up the code in async_plc.py 424 | ''' 425 | def configure_logging_level(logging_level, log): 426 | # Expand on this logic 427 | if logging_level == 'CRITICAL': 428 | log.setLevel(logging.CRITICAL) 429 | elif logging_level == 'ERROR': 430 | log.setLevel(logging.ERROR) 431 | elif logging_level == 'WARNING': 432 | log.setLevel(logging.WARNING) 433 | elif logging_level == 'INFO': 434 | log.setLevel(logging.INFO) 435 | elif logging_level == 'DEBUG': 436 | log.setLevel(logging.DEBUG) 437 | else: 438 | log.setLevel(logging.NOTSET) 439 | 440 | ''' 441 | Used to configure the framer and clean up the code in async_plc.py 442 | ''' 443 | 444 | def configure_server_framer(server_config): 445 | framer = None 446 | if server_config['type'] == 'tcp': 447 | # check framer to be rtu or none 448 | if server_config['framer'] == 'RTU': 449 | framer = ModbusRtuFramer 450 | elif server_config['type'] == 'serial': 451 | # framer can be rtu, ascii, or binary 452 | if server_config['framer'] == 'RTU': 453 | framer = ModbusRtuFramer 454 | elif server_config['framer'] == 'ASCII': 455 | framer = ModbusAsciiFramer 456 | elif server_config['framer'] == 'BINARY': 457 | framer = ModbusBinaryFramer 458 | return framer 459 | -------------------------------------------------------------------------------- /startup/README_startup_service.md: -------------------------------------------------------------------------------- 1 | SCADASIM PLC 2 | 3 | SCADA Simulator 4 | 5 | Copyright 2018 Carnegie Mellon University. All Rights Reserved. 6 | 7 | NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 8 | 9 | Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 10 | 11 | [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 12 | This Software includes and/or makes use of the following Third-Party Software subject to its own license: 13 | 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 14 | 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 15 | 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 16 | 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 17 | 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 18 | 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 19 | 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 20 | 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 21 | 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 22 | 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 23 | 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 24 | 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 25 | 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 26 | DM18-1351 27 | 28 | 29 | 30 | # Add plc_startup.service as a systemd service to run on boot 31 | 32 | #### Recommended to use a symlink for plc_startup.service file 33 | - `ln -s /etc/systemd/system/plc_startup.service` 34 | - This will create a symlink to the .service file in the src repo in /etc/systemd/system 35 | - `chmod +x startup_plc.sh` 36 | - `chmod 664 /etc/systemd/system/plc_startup.service` 37 | - `systemctl daemon-reload` 38 | - `systemctl enable plc_startup.service` 39 | - `systemctl start plc_startup.service` 40 | -------------------------------------------------------------------------------- /startup/config_file_name.txt: -------------------------------------------------------------------------------- 1 | /usr/local/bin/scadasim_pymodbus_plc/configs/test_config.yaml 2 | -------------------------------------------------------------------------------- /startup/master.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # SCADA Simulator 4 | # 5 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 6 | # 7 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 8 | # 9 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 10 | # 11 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 12 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 13 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 14 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 15 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 16 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 17 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 18 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 19 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 20 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 21 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 22 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 23 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 24 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 25 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 26 | # DM18-1351 27 | # 28 | 29 | 30 | import sys 31 | import yaml 32 | from os import path 33 | 34 | # (Default) open txt file to get config.yaml file name IF path of config file was not supplied as argument to master.py 35 | # strip any trailing /n from string 36 | if len(sys.argv) == 1: 37 | f = open("/usr/local/bin/scadasim_pymodbus_plc/startup/config_file_name.txt", 'r') 38 | file_name = f.read() 39 | file_name = file_name.rstrip() 40 | f.close() 41 | else: 42 | file_name = sys.argv[1] 43 | 44 | # open yaml config file, build object 45 | config_file = open(file_name, 'r') 46 | config_yaml = yaml.safe_load(config_file) 47 | config_file.close() 48 | 49 | # get number of plc devices from MASTER section of the config file 50 | num_of_plc = config_yaml['MASTER']['num_of_PLC'] 51 | # create backup files if they do not already exist - 1 for each PLC device 52 | i = 0 53 | while(i < num_of_plc): 54 | num = str(i) 55 | plc_device_name = 'PLC ' + num 56 | # keep in the src repo, in format of "backup_N.yaml", where N is the ID of the PLC device 57 | backup_file_name = '/usr/local/bin/scadasim_pymodbus_plc/backups/backup_' + num + '.yaml' 58 | 59 | # collect num of register/coils for each plc device 60 | hr_values = config_yaml[plc_device_name]['DATASTORE']['hr']['values'] 61 | co_values = config_yaml[plc_device_name]['DATASTORE']['co']['values'] 62 | di_values = config_yaml[plc_device_name]['DATASTORE']['di']['values'] 63 | ir_values = config_yaml[plc_device_name]['DATASTORE']['ir']['values'] 64 | 65 | # check if file exists 66 | if (path.exists(backup_file_name) == False or path.getsize(backup_file_name) == 0): 67 | # create file - only storing the register starting address and values 68 | backup_dict = {} 69 | backup = open(backup_file_name, 'w+') 70 | backup_dict['DATASTORE'] = {'hr': {'start_addr': 1, 'values': hr_values}, 'ir': {'start_addr': 1, 'values': ir_values}, 'co': {'start_addr': 1, 'values': co_values}, 'di': {'start_addr': 1, 'values': di_values}} 71 | yaml.dump(backup_dict, backup) 72 | backup.close() 73 | i = i + 1 74 | 75 | # return number of backup files created and the config filepath to bash startup script 76 | print str(num_of_plc) + ' ' + file_name 77 | 78 | -------------------------------------------------------------------------------- /startup/plc_startup.service: -------------------------------------------------------------------------------- 1 | # SCADA Simulator 2 | # 3 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 4 | # 5 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 6 | # 7 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 8 | # 9 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 10 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 11 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 12 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 13 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 14 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 15 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 16 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 17 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 18 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 19 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 20 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 21 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 22 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 23 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 24 | # DM18-1351 25 | # 26 | 27 | [Unit] 28 | After = network.target 29 | 30 | [Service] 31 | ExecStart = /usr/local/bin/scadasim_pymodbus_plc/startup/startup_plc.sh 32 | 33 | [Install] 34 | WantedBy = default.target 35 | -------------------------------------------------------------------------------- /startup/startup_plc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SCADA Simulator 4 | # 5 | # Copyright 2018 Carnegie Mellon University. All Rights Reserved. 6 | # 7 | # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. 8 | # 9 | # Released under a MIT (SEI)-style license, please see license.txt or contact permission@sei.cmu.edu for full terms. 10 | # 11 | # [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use and distribution. 12 | # This Software includes and/or makes use of the following Third-Party Software subject to its own license: 13 | # 1. Packery (https://packery.metafizzy.co/license.html) Copyright 2018 metafizzy. 14 | # 2. Bootstrap (https://getbootstrap.com/docs/4.0/about/license/) Copyright 2011-2018 Twitter, Inc. and Bootstrap Authors. 15 | # 3. JIT/Spacetree (https://philogb.github.io/jit/demos.html) Copyright 2013 Sencha Labs. 16 | # 4. html5shiv (https://github.com/aFarkas/html5shiv/blob/master/MIT%20and%20GPL2%20licenses.md) Copyright 2014 Alexander Farkas. 17 | # 5. jquery (https://jquery.org/license/) Copyright 2018 jquery foundation. 18 | # 6. CanvasJS (https://canvasjs.com/license/) Copyright 2018 fenopix. 19 | # 7. Respond.js (https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT) Copyright 2012 Scott Jehl. 20 | # 8. Datatables (https://datatables.net/license/) Copyright 2007 SpryMedia. 21 | # 9. jquery-bridget (https://github.com/desandro/jquery-bridget) Copyright 2018 David DeSandro. 22 | # 10. Draggabilly (https://draggabilly.desandro.com/) Copyright 2018 David DeSandro. 23 | # 11. Business Casual Bootstrap Theme (https://startbootstrap.com/template-overviews/business-casual/) Copyright 2013 Blackrock Digital LLC. 24 | # 12. Glyphicons Fonts (https://www.glyphicons.com/license/) Copyright 2010 - 2018 GLYPHICONS. 25 | # 13. Bootstrap Toggle (http://www.bootstraptoggle.com/) Copyright 2011-2014 Min Hur, The New York Times. 26 | # DM18-1351 27 | # 28 | 29 | 30 | # Check whether the template variable is used or not 31 | # template_var would contain the full path of the config file to use 32 | # If used, run master.py with the path supplied as an argument 33 | TEMPLATE_VAR="$(vmtoolsd --cmd 'info-get guestinfo.template_variable')" 34 | if [ "$TEMPLATE_VAR" = "" ]; then 35 | echo "No template variable found on the vmx file. Using hard-coded value" 36 | # load in data from master.py using hard-coded config textfile 37 | result=$(python /usr/local/bin/scadasim_pymodbus_plc/startup/master.py) 38 | else 39 | # load in data from master.py using template_var as config textfile 40 | echo "Using template_var from vmx file" 41 | result=$(python /usr/local/bin/scadasim_pymodbus_plc/startup/master.py $TEMPLATE_VAR) 42 | fi 43 | 44 | # master.py will return the number of plc devices for this schema, and the path of the config file 45 | results=( $result ) 46 | 47 | echo "$results" 48 | 49 | # parse results 50 | START=0 51 | END=${results[0]} 52 | name_of_config=${results[1]} 53 | 54 | # loop and start plc devices with their ID and the path of the config file supplied as arguments 55 | # run in background as async_plc will start off multiple threads 56 | for (( c=$START; c<$END; c++ )) 57 | do 58 | echo "Running async_plc.py with arg $c" 59 | python /usr/local/bin/scadasim_pymodbus_plc/plc/async_plc.py --n $c --c $name_of_config & 60 | done 61 | 62 | # keep script alive so that async_plc programs continue to run 63 | while true; do 64 | echo 65 | done 66 | --------------------------------------------------------------------------------