├── .gitignore ├── 01_PythonInterpreter └── hack.py ├── 02_DataTypes └── hack.py ├── 03_Loops └── hack.py ├── 04_IfElse └── hack.py ├── 05_StringInterpolation └── hack.py ├── 06_ExceptionHandling └── hack.py ├── 07_Functions └── hack.py ├── 08_OS_And_imports └── hack.py ├── 09_File_IO ├── hack.py └── passwords │ └── passwords.txt ├── 10_Python_Scripting ├── hack.py ├── passwords │ └── passwords.txt └── simplified_example.py ├── 11_Python_CLI ├── hack.py ├── passwords │ └── passwords.txt ├── run_example.sh ├── run_hack.sh └── simplified_example.py ├── AboutMe.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .idea/ 107 | -------------------------------------------------------------------------------- /01_PythonInterpreter/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | A basic python program 3 | 4 | 5 | Python 2 or 3? 6 | Not a question, Python 2 is outdated, use Python 3 7 | """ 8 | print("Hacking Steve on mysql") 9 | 10 | -------------------------------------------------------------------------------- /02_DataTypes/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python has many data types but the most widely used are 3 | 4 | 1. Strings 5 | 2. Lists 6 | 3. Dictionaries 7 | 4. Int/Float 8 | 5. None 9 | 6. Bool 10 | 11 | EVERYTHING in Python is an object. If you don't know what this means 12 | don't worry. 13 | 14 | Brutespray examples 15 | Dictionaries 16 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L130 17 | 18 | Lists 19 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L338 20 | 21 | Booleans 22 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L300 23 | 24 | Strings 25 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L273 26 | 27 | Int/Float 28 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L288 29 | """ 30 | 31 | # Lists are ordered and values are accessed by index 32 | people = ["Steve", "Alice"] 33 | 34 | # Strings are great things to print 35 | print("Hacking " + people[0]) 36 | print("Hacking " + people[1]) 37 | 38 | # Dictionary are key value pairs useful for mapping one thing to another 39 | protocols = {"postgressql": "sql", 40 | "pop3": "email"} 41 | 42 | print("Hacking " + people[0] + "'s " + protocols["pop3"]) 43 | -------------------------------------------------------------------------------- /03_Loops/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python supports for and while loops. Python is whitespace delimited 3 | so code blocks are separated by spaces or tabs. Whatever you pick you have to stay consistent 4 | 5 | 6 | Brutespray examples 7 | 8 | For Loop 9 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L209 10 | """ 11 | 12 | people = ["Steve", "Alice"] 13 | 14 | # Iteration in python is as easy as saying for thing in iterable 15 | # Lists are one of many objects that support iterables 16 | for person in people: 17 | print("Hacking " + person) 18 | 19 | # Python supports variable unpacking in line with lists 20 | protocols = {"postgressql": "sql", 21 | "pop3": "email"} 22 | 23 | people_protocol = [["Steve", "postgressql"], ["Alice", "pop3"]] 24 | 25 | for person, protocol in people_protocol: 26 | print("Hacking " + person + " with " + protocol) 27 | 28 | 29 | # Advanced - List Comprehensions 30 | # Just another way of doing the same thing above 31 | print(["Hacking " + person + " with " + protocol for person, protocol in people_protocol]) 32 | 33 | -------------------------------------------------------------------------------- /04_IfElse/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | If else, elseif are supported, space delimited like the rest of python 3 | 4 | Brutespray examples 5 | 6 | If statement. They're basically everywhere in Brutespray 7 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L213 8 | """ 9 | 10 | protocols = {"postgressql":"sql", 11 | "pop3": "email"} 12 | 13 | people_protocol = [["Steve", "postgressql"], 14 | ["Alice", "pop3"], 15 | ["Alice", "sql"]] 16 | 17 | for person, protocol in people_protocol: 18 | # Equality is one type of conditional 19 | # Equal is denoted as == 20 | # Single = is called an assignment operator for reference 21 | if person == "Alice": 22 | print("Hacking " + person + " with " + protocol + " even harder") 23 | else: 24 | print("Hacking " + person + " with " + protocol) 25 | 26 | 27 | # And/Or statements can be used with conditionals as well 28 | for person, protocol in people_protocol: 29 | if person == "Alice" and protocol == "pop3": 30 | print("Hacking " + person + " with " + protocol + " with phishing") 31 | 32 | elif person == "Alice" and protocol == "sql": 33 | print("Hacking " + person + " with " + protocol + " with sqlinjection") 34 | 35 | else: 36 | print("Hacking " + person + " with " + protocol) 37 | -------------------------------------------------------------------------------- /05_StringInterpolation/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | If else, elseif are supported, space delimited like the rest of python 3 | 4 | Brutespray examples 5 | 6 | Not in Brutespray but really needs to be 7 | """ 8 | 9 | protocols = {"postgressql":"sql", 10 | "pop3": "email"} 11 | 12 | people_protocol = [["Steve", "postgressql"], 13 | ["Alice", "pop3"], 14 | ["Alice", "sql"]] 15 | 16 | for person, protocol in people_protocol: 17 | if person == "Alice": 18 | 19 | # .format string formatting/interpolation. Use this instead of + signs 20 | print("Hacking {0} with {1} even harder".format(person, protocol)) 21 | else: 22 | # Python 2 style, no longer fashionable 23 | print("Hacking %s with %s" % (person, protocol)) 24 | -------------------------------------------------------------------------------- /06_ExceptionHandling/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | If else, elseif are supported, space delimited like the rest of python 3 | 4 | Brutespray examples 5 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L161 6 | """ 7 | protocols = {"postgressql":"sql", 8 | "pop3": "email"} 9 | 10 | 11 | # Trying a get a protocol that does exist 12 | print(protocols["pop3"]) 13 | 14 | # Trying to get a protocol that doesn't exist 15 | # This raises what's called an exception 16 | # Comment this line out to see how the rest of the code works 17 | print(protocols["telnet"]) 18 | 19 | 20 | # This can be managed through try except 21 | try: 22 | print(protocols["telnet"]) 23 | except KeyError: 24 | print("Telnet is not an option") 25 | 26 | 27 | # In conjunction with the rest of the program 28 | 29 | people_protocol = [["Steve", "postgressql"], 30 | ["Alice", "pop3"], 31 | ["Alice", "telnet"]] 32 | 33 | for person, protocol in people_protocol: 34 | try: 35 | print("Hacking {0} with {1}".format(person, protocols[protocol])) 36 | except KeyError: 37 | print("Protocol {} is not an option. Failed hack on {}".format(protocol, person)) 38 | -------------------------------------------------------------------------------- /07_Functions/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | If else, elseif are supported, space delimited like the rest of python 3 | 4 | Brutespray examples 5 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L239 6 | """ 7 | 8 | # Variables in global scope are suggested to be capitalized by convention 9 | PROTOCOLS = {"postgressql": "sql", 10 | "pop3": "email"} 11 | 12 | 13 | PEOPLE_PROTOCOL = [["Steve", "postgressql"], 14 | ["Alice", "pop3"], 15 | ["Alice", "sql"]] 16 | 17 | 18 | def pwn(people_protocol): 19 | """Actually hack people""" 20 | for person, protocol in people_protocol: 21 | try: 22 | print("Hacking {0} with {1}".format(person, PROTOCOLS[protocol])) 23 | except KeyError: 24 | print("Protocol {} is not an option. Failed hack on {}".format(protocol, person)) 25 | 26 | # Return statement is not needed if nothing is being returned but I like putting them in 27 | return 28 | 29 | 30 | def print_targets(people_protocol): 31 | """Just print the names and protocols""" 32 | for hack_pair in people_protocol: 33 | print(hack_pair) 34 | return 35 | 36 | 37 | print_targets(PEOPLE_PROTOCOL) 38 | pwn(PEOPLE_PROTOCOL) 39 | -------------------------------------------------------------------------------- /08_OS_And_imports/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python can tell the underlying shell to run commands. Handy when for replacing 3 | bash scripts or calling tools in other languages 4 | 5 | Brutespray examples 6 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L271 7 | 8 | """ 9 | # Other code can be brought in using the import statement 10 | # Python has a really full featured built in library 11 | import subprocess 12 | 13 | 14 | PROTOCOLS = {"postgressql":"sql", 15 | "pop3": "email"} 16 | 17 | 18 | PEOPLE_PROTOCOL = [["Steve", "postgressql"], 19 | ["Alice", "pop3"]] 20 | 21 | 22 | def pwn(people_protocol): 23 | """Actually hack people""" 24 | for person, protocol in people_protocol: 25 | try: 26 | command_to_run = "Hacking {0} with {1}".format(person, PROTOCOLS[protocol]) 27 | # This command is being run in the shell directly, not in Python! 28 | subprocess.run(['echo', "{}".format(command_to_run)]) 29 | 30 | except KeyError: 31 | print("Protocol {} is not an option. Failed hack on {}".format(protocol, person)) 32 | 33 | return 34 | 35 | 36 | def print_targets(people_protocol): 37 | """Just print the names and protocols""" 38 | for hack_pair in people_protocol: 39 | print(hack_pair) 40 | return 41 | 42 | 43 | pwn(PEOPLE_PROTOCOL) 44 | -------------------------------------------------------------------------------- /09_File_IO/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | Loading things from files are very useful. In Brutespray password lists are defined 3 | per protocol 4 | 5 | Brutespray examples 6 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L155 7 | https://github.com/x90skysn3k/brutespray/tree/master/wordlist 8 | """ 9 | import subprocess 10 | 11 | # Yet another import. OS helps us abstract away the operating system which helps when you want to 12 | # run your code on either multiple operating systems without problems 13 | import os 14 | 15 | 16 | PROTOCOLS = {"postgressql": "sql", 17 | "pop3": "email"} 18 | 19 | 20 | PEOPLE_PROTOCOL = [["Steve", "postgressql"], 21 | ["Alice", "pop3"]] 22 | 23 | 24 | # An example of OS dependent things are filepaths. Windows does it differently than Linux 25 | # by using os.path.join we avoid cross platform problems 26 | filepath = os.path.join("passwords", "passwords.txt") 27 | 28 | # This opens the file in read mode 29 | with open(filepath, 'r') as password_file: 30 | # A list comprehension to make things compact 31 | # rstrip just removes the \n (newline) character from the end of the string 32 | PASSWORDS = [line.rstrip() for line in password_file] 33 | 34 | print(PASSWORDS) 35 | 36 | 37 | def pwn(people_protocol, passwords): 38 | """Actually hack people 39 | 40 | Added passwords as an argument this time 41 | """ 42 | # Extra for loop to try each password for each user, protocol pair 43 | for password in passwords: 44 | for person, protocol in people_protocol: 45 | try: 46 | command_to_run = "Hacking {0} with {1} using {2}".format(person, PROTOCOLS[protocol], password) 47 | # This command is being run in the shell directly, not in Python! 48 | subprocess.run(['echo', "{}".format(command_to_run)]) 49 | 50 | except KeyError: 51 | print("Protocol {} is not an option. Failed hack on {}".format(protocol, person)) 52 | 53 | return 54 | 55 | 56 | def print_targets(people_protocol): 57 | """Just print the names and protocols""" 58 | for hack_pair in people_protocol: 59 | print(hack_pair) 60 | return 61 | 62 | 63 | pwn(PEOPLE_PROTOCOL, PASSWORDS) 64 | -------------------------------------------------------------------------------- /09_File_IO/passwords/passwords.txt: -------------------------------------------------------------------------------- 1 | password123 2 | SuperSecurePW 3 | -------------------------------------------------------------------------------- /10_Python_Scripting/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | Loading things from files are very useful. In Brutespray password lists are defined 3 | per protocol 4 | 5 | Brutespray examples 6 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L334 7 | """ 8 | import subprocess 9 | import os 10 | 11 | 12 | PROTOCOLS = {"postgressql": "sql", 13 | "pop3": "email"} 14 | 15 | 16 | PEOPLE_PROTOCOL = [["Steve", "postgressql"], 17 | ["Alice", "pop3"]] 18 | 19 | 20 | # An example of OS dependent things are filepaths. Windows does it differently than Linux 21 | # by using os.path.join we avoid cross platform problems 22 | filepath = os.path.join("passwords", "passwords.txt") 23 | 24 | # This opens the file in read mode 25 | with open(filepath, 'r') as password_file: 26 | # A list comprehension to make things compact 27 | # rstrip just removes the \n (newline) character from the end of the string 28 | PASSWORDS = [line.rstrip() for line in password_file] 29 | 30 | print(PASSWORDS) 31 | 32 | 33 | def pwn(people_protocol, passwords): 34 | """Actually hack people 35 | 36 | Added passwords as an argument this time 37 | """ 38 | # Extra for loop to try each password for each user, protocol pair 39 | for password in passwords: 40 | for person, protocol in people_protocol: 41 | try: 42 | command_to_run = "Hacking {0} with {1} using {2}".format(person, PROTOCOLS[protocol], password) 43 | # This command is being run in the shell directly, not in Python! 44 | subprocess.run(['echo', "{}".format(command_to_run)]) 45 | 46 | except KeyError: 47 | print("Protocol {} is not an option. Failed hack on {}".format(protocol, person)) 48 | 49 | return 50 | 51 | 52 | def print_targets(people_protocol): 53 | """Just print the names and protocols""" 54 | for hack_pair in people_protocol: 55 | print(hack_pair) 56 | return 57 | 58 | 59 | """ 60 | "Scripts" are usually things that are run directly e.g. python hack.py 61 | Programs or libraries rae things that are usually imported 62 | The problem is that when you import things in python it actually runs the codeo 63 | So typically you should separate the "definitions" from the "actions" 64 | In our example we want to separate the actual pwning from the instructions on how to pwn 65 | 66 | Refer to the simplified example in this same directory 67 | """ 68 | 69 | if __name__ == "__main__": 70 | pwn(PEOPLE_PROTOCOL, PASSWORDS) 71 | -------------------------------------------------------------------------------- /10_Python_Scripting/passwords/passwords.txt: -------------------------------------------------------------------------------- 1 | password123 2 | SuperSecurePW 3 | -------------------------------------------------------------------------------- /10_Python_Scripting/simplified_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simplified version showing the difference 3 | 4 | Try running the python file directly from the terminal 5 | run "python simplified_example.py" from bash 6 | 7 | 8 | Then start Python in its interactive REPL 9 | run "import simplified_example" in python 10 | 11 | And see what happens 12 | """ 13 | print("This is a set of instructions") 14 | 15 | if __name__ == "__main__": 16 | print("This is running the instructions") 17 | -------------------------------------------------------------------------------- /11_Python_CLI/hack.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrap up all our logic in a command line interface for easy use 3 | 4 | Brutespray example 5 | https://github.com/x90skysn3k/brutespray/blob/master/brutespray.py#L306 6 | """ 7 | import subprocess 8 | import os 9 | import argparse 10 | 11 | 12 | PROTOCOLS = {"postgressql": "sql", 13 | "pop3": "email"} 14 | 15 | 16 | # An example of OS dependent things are filepaths. Windows does it differently than Linux 17 | # by using os.path.join we avoid cross platform problems 18 | filepath = os.path.join("passwords", "passwords.txt") 19 | 20 | # This opens the file in read mode 21 | with open(filepath, 'r') as password_file: 22 | # A list comprehension to make things compact 23 | # rstrip just removes the \n (newline) character from the end of the string 24 | PASSWORDS = [line.rstrip() for line in password_file] 25 | 26 | def pwn(people, passwords, protocols): 27 | """Actually hack people 28 | 29 | Added passwords as an argument this time 30 | """ 31 | # Extra for loop to try each password for each user, protocol pair 32 | for password in passwords: 33 | for person in people: 34 | for protocol in protocols: 35 | try: 36 | command_to_run = "Hacking {0} with {1} using {2}".format(person, PROTOCOLS[protocol], password) 37 | # This command is being run in the shell directly, not in Python! 38 | subprocess.run(['echo', "{}".format(command_to_run)]) 39 | 40 | except KeyError: 41 | print("Protocol {} is not an option. Failed hack on {}".format(protocol, person)) 42 | 43 | return 44 | 45 | 46 | def print_targets(people_protocol): 47 | """Just print the names and protocols""" 48 | for hack_pair in people_protocol: 49 | print(hack_pair) 50 | return 51 | 52 | 53 | parser = argparse.ArgumentParser(description="A sample Command Line Interface") 54 | parser.add_argument('-u', '--username', nargs='+', help="Usernames of people to hack", required=True, default="Steve") 55 | parser.add_argument('-p', '--password', nargs='+', help="Passwords to try", required=False) 56 | parser.add_argument('-pr', '--protocols', nargs='+', help="Protocols to try", required=False) 57 | 58 | args = parser.parse_args() 59 | 60 | """ 61 | "Scripts" are usually things that are run directly e.g. python hack.py 62 | Programs or libraries rae things that are usually imported 63 | The problem is that when you import things in python it actually runs the codeo 64 | So typically you should separate the "definitions" from the "actions" 65 | In our example we want to separate the actual pwning from the instructions on how to pwn 66 | 67 | Refer to the simplified example in this same directory 68 | """ 69 | 70 | if __name__ == "__main__": 71 | users_to_hack = args.username 72 | 73 | if args.password: 74 | passwords = args.password 75 | else: 76 | print("No password provided using default password list") 77 | passwords = PASSWORDS 78 | 79 | protocols = args.protocols 80 | pwn(users_to_hack, passwords, protocols) 81 | -------------------------------------------------------------------------------- /11_Python_CLI/passwords/passwords.txt: -------------------------------------------------------------------------------- 1 | password123 2 | SuperSecurePW 3 | -------------------------------------------------------------------------------- /11_Python_CLI/run_example.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | python simplified_example.py -u Steve -p Password123 --protocols mysql pop3 -------------------------------------------------------------------------------- /11_Python_CLI/run_hack.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | python hack.py -u Steve Alice -p Password123 MyPass --protocols mysql pop3 -------------------------------------------------------------------------------- /11_Python_CLI/simplified_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | A simplified version showing the difference 4 | 5 | Try running the python file directly from the terminal 6 | run "python simplified_example.py" from bash 7 | 8 | 9 | Then start Python in its interactive REPL 10 | run "import simplified_example" in python 11 | 12 | And see what happens 13 | """ 14 | import argparse 15 | 16 | parser = argparse.ArgumentParser(description="A sample Command Line Interface") 17 | parser.add_argument('-u', '--username', help="Username of person to hack", required=True, default="Steve") 18 | parser.add_argument('-p', '--password', help="Passwords to try", required=False) 19 | 20 | # Set nargs to take a list of protocols 21 | parser.add_argument('-pr', '--protocols', nargs='+', help="Protocols to try", required=False) 22 | 23 | args = parser.parse_args() 24 | 25 | 26 | if __name__ == "__main__": 27 | print(args.username) 28 | 29 | if args.password: 30 | print(args.password) 31 | else: 32 | print("No password provided") 33 | 34 | print(args.protocols) 35 | -------------------------------------------------------------------------------- /AboutMe.md: -------------------------------------------------------------------------------- 1 | # About Me 2 | 3 | ## Things you can call me 4 | * Ravin 5 | * Canyon289 6 | 7 | ## Relevant things 8 | * Keyholder at the best hackerspace ever (23b of course) 9 | * Write a lot of Python for a place I work at and open source. Also fun 10 | 11 | ## Why this talk? 12 | 13 | > Many pentesting tools are written in Python and I thought it would be great to teach others how to write their own. 14 | > Brutespray was a good example, and it included in Kali by default! 15 | 16 | ## Most importantly!! 17 | * Follow along on your own computer by cloning this repo! 18 | * Questions get rewarded (probably) 19 | 20 | ## Legal boilerplate 21 | Everything I say is my own opinion 22 | * I'm not representing anyone or anything in this talk but myself* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PythonforHackers 2 | Want to build your own tools like [BruteSpray](https://github.com/x90skysn3k/brutespray)? 3 | Here's the recipe. 4 | 5 | # The ingredients 6 | 1. Python intepreter 7 | 2. Data Types 8 | 3. For Loops 9 | 4. If Else Nominal Control Flow 10 | 5. String Interpolation 11 | 6. Exception Control Flow 12 | 7. Functions 13 | 8. OS Commands and imports 14 | 9. File IO 15 | 10. Python Scripting 16 | 11. Command Line Interface 17 | 18 | # How this repo works 19 | Each folder contains a Python script called hack.py. The functionality is built up one piece at a time 20 | * Links to example in Brutespray are provided at the top of each file 21 | 22 | ## 2020 Update 23 | Link to slide deck [here](https://docs.google.com/presentation/d/1tGv_tX3L6eJzmjoEpX7TpEb2Cv6IBxr8SqceAFRz0_c/edit?usp=sharing) 24 | --------------------------------------------------------------------------------