├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── cert.pem └── key.pem ├── data └── attacks.json ├── ptn ├── __init__.py ├── attacks.py ├── database.py ├── errors.py ├── importscan.py ├── static │ ├── ptnotes.css │ └── ptnotes.js ├── templates │ ├── 404.html │ ├── 500.html │ ├── about.html │ ├── attack.html │ ├── base.html │ ├── host.html │ ├── hosts.html │ ├── import.html │ ├── index.html │ ├── item.html │ ├── notes.html │ ├── project.html │ └── projects.html ├── validate.py └── webserver.py └── server /.gitignore: -------------------------------------------------------------------------------- 1 | .tar.gz 2 | .DS_Store 3 | *.sqlite 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | MAINTAINER Jayson Grace 3 | 4 | # Install flask and various dependencies 5 | RUN apk add --no-cache py2-pip \ 6 | && pip2 install --upgrade pip \ 7 | && pip2 install flask 8 | 9 | # Setup the ptnotes user, project, and folder permissions 10 | RUN adduser -h /ptnotes -g ptnotes -D ptnotes 11 | COPY . /ptnotes 12 | RUN chown -R ptnotes:ptnotes /ptnotes 13 | RUN chmod +x /ptnotes/server 14 | USER ptnotes 15 | 16 | WORKDIR /ptnotes 17 | 18 | EXPOSE 5000 19 | 20 | CMD ["./server", "-l", "0.0.0.0"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, LCI Technology Group, LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | Neither the name of LCI Technology Group, LLC nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PTNotes 2 | Simple tool for taking notes in a pentest. PTNotes uses data from imported Nessus and Nmap files along with the built-in attack data to build a list of hosts, open ports, and potential attack vectors. It then allows you to add notes to each host and each attack vector. You can then view all attack notes or all host notes at one time. PTNotes allows you to create a separate project for each penetration test. 3 | 4 | ## Prerequisites 5 | You will need to install the flask framework: `pip install flask` 6 | 7 | ## Installation 8 | `git clone https://github.com/averagesecurityguy/ptnotes` 9 | 10 | or 11 | 12 | ``` 13 | wget https://github.com/averagesecurityguy/ptnotes/archive/.zip 14 | gunzip .zip 15 | ``` 16 | 17 | ## Supported Versions 18 | The only supported versions of PTNotes is the latest release and the dev branch. All other releases are obsolete and will be routinely removed from Github. 19 | 20 | 21 | ## Usage 22 | From the ptnotes folder run `./server` then connect to the server on https://127.0.0.1:5000. PTNotes ships with a default TLS certificate. For security purposes, this certificate should be replaced when running the server in production. To install your certificate, replace the `config/cert.pem` and `config/key.pem` files with the appropriate files. PTNotes also supports the following command line options. 23 | 24 | ``` 25 | usage: server [-h] [-l LISTEN_ADDRESS] [-p LISTEN_PORT] [-d] 26 | 27 | optional arguments: 28 | -h, --help show this help message and exit 29 | -l LISTEN_ADDRESS Address to listen on. Default is 127.0.0.1 30 | -p LISTEN_PORT Port to listen on. Default is 5000. 31 | -d Enable Flask debugging. Should not be used in production. 32 | ``` 33 | 34 | ## Creating New Attacks 35 | To add new attacks to PTNotes edit the `data/attacks.json` file. Each attack uses the following structure: 36 | 37 | ``` 38 | { 39 | "name": "SMB Brute-force.", 40 | "description": "Attempt to brute-force the local administrator account on these SMB servers.", 41 | "keywords": ["--smb-os-discovery--", "--11011--"] 42 | } 43 | ``` 44 | 45 | An attack needs a name and description along with a list of keywords that signify a machine may vulnerable to the attack. When data is imported to PTNotes the Nessus plugin id or the Nmap script name are extracted along with the plugin/script output. You can search for vulnerabilities using the plugin id or script name surrounded by -- as seen in the example above. You can also use any text from the plugin or script output. Multiple keywords are joined with OR to create the final query. 46 | 47 | ## To use the Docker container 48 | Start by building it: 49 | ``` 50 | docker build . -t /ptnotes 51 | ``` 52 | Next, run it: 53 | ``` 54 | docker run -d -p 5000:5000 --name=ptnotes -v /data:/ptnotes/data /ptnotes 55 | ``` 56 | Destroy it when you're done (your data will persist since you used the volume mount parameter): 57 | ``` 58 | docker stop ptnotes && docker rm ptnotes 59 | ``` 60 | -------------------------------------------------------------------------------- /config/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF+jCCA+KgAwIBAgIJALyKJpu5KdAVMA0GCSqGSIb3DQEBBQUAMFsxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIEwJUTjEWMBQGA1UEChMNUGVudGVzdCBOb3RlczEnMCUG 4 | A1UEAxMeUGVudGVzdCBOb3RlcyBTZWxmLVNpZ25lZCBDZXJ0MB4XDTE3MTAwOTA0 5 | MDk1NFoXDTI3MTAwNzA0MDk1NFowWzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlRO 6 | MRYwFAYDVQQKEw1QZW50ZXN0IE5vdGVzMScwJQYDVQQDEx5QZW50ZXN0IE5vdGVz 7 | IFNlbGYtU2lnbmVkIENlcnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC 8 | AQC9eBr5uJHuPGyTeaaWF2GT7/zsPs03hPM0L7sJdPClCffKgwy4r7hzaIxqFtRu 9 | vIa04+kdnNSy05zWddQ0rj0Y4d6VVr/zV3u2MYQv4awXJ6yDTK8Z41I7Qne7H9i4 10 | 8evknb8lS0XZdYfriJEgq9xBrfolSa4nibJ50LW3tCI3yVJXLv+Mr39RznUjbJYp 11 | c71QWMFuMVoU08pnHvuGnitFqsjWJx9Viw3Hm6DNUiq13R2WD0jRCshNGRUsUxu/ 12 | pRkyG05V2bJHSgi+IGsNAZZEvxep3Z7W+nQblHAWv2M6u/UJtJChRJjkNOqq3r/G 13 | Z8hyoebPnLzJP75rdcjiwECMCgKWHKtFU0UTnDU5FPg3oso9Y9bEvgN7ScNQ/Rzj 14 | 0DcCrw5tNksNUb2m/nb2F9FItlh7Me6pYGOFdMmY4gFFrY7ro3cH7GDADw+P9SUZ 15 | gEbxxYsftw5I8ss2AmxBNLkH4Rq+6LmDNgy2fMCIIgpRnkdoSbeOiwsRuOWp5M9U 16 | NVvD/94FvtteQlooOtUSbAeW3HKEFLUOvCarGYNiEq+uCaaI5odyyYj4Oj2QVubW 17 | brzZpPOyrG7OeyHwluh4nhikTmtKE9R5oSXUsRFpsWPZhSLU2cLRE/EnNBDlPKvX 18 | E6APH1HoFy1RmN1pS6dHE2lDnHJxjvHF/bSCoja4z8HapQIDAQABo4HAMIG9MB0G 19 | A1UdDgQWBBSm7R/Ot8+c92igN9mI+u40H6TpxDCBjQYDVR0jBIGFMIGCgBSm7R/O 20 | t8+c92igN9mI+u40H6TpxKFfpF0wWzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlRO 21 | MRYwFAYDVQQKEw1QZW50ZXN0IE5vdGVzMScwJQYDVQQDEx5QZW50ZXN0IE5vdGVz 22 | IFNlbGYtU2lnbmVkIENlcnSCCQC8iiabuSnQFTAMBgNVHRMEBTADAQH/MA0GCSqG 23 | SIb3DQEBBQUAA4ICAQCad2AqAFkDjyi5Rg6ivx6sTnrCGGjgJFHXFmrAoQ37o/An 24 | /2CswhsMFUjkBUEeNBiZc+84eA12rXKxhZsgdDVrYJBjsLkr1fQHuAuelik4e60I 25 | QljihUwEmEdYPnlFAliN/kGPYD2BkcHHQ1ujMoYF76I4gyUwKyLnhxhGICPokNM1 26 | Rjyg4LcGCzqQc5EJysyShsHxpRxzUsQ0YHTp4gGZuBefpL1pEqUiQVAvzTEXk/on 27 | J01TdIzXVGLlDQ2IOkc+3yuik/nWKSR5Y1kGDzph0g7ReKdlwgtsN43jYBBEP8c+ 28 | IG1U1BmK9Lu3yPj0DM09RRX4GJJ66Jrx1j9wofBetisvIqqY5WpjFnbwufO8Zue6 29 | CZAAShSdmB9ZNk6puJzDu3RodKDkrb/mCzCk6tQA425JVB3w7rtRZx/AUQGFYGfi 30 | HNBIcMEjINPjEvWSb+ZjH6MK8yzHZG2ikCCcszpsk76HLLwZ214Pq1Nxz+FhY9Ab 31 | z94tRUBc2f1U10KvYxjf9aFQoBj4bPktezQ/UUUcZN778Y93orlztpvpHXIYFtCQ 32 | uq9gHGk16n6bJ4PwttmDMh4sQt/jJ8EcrzOmXyTpnxXdd+miyKqUka2NROSlKkuE 33 | 9Bk0O2xmTJmznJUS3bIfy4fi6YMVLcw7IHVL+r8HhWIIURcA5jW5Zv40bXhtFA== 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /config/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /data/attacks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "SSH Brute-force", 4 | "description": "Many devices have common SSH username and password combinations.", 5 | "keywords": ["--ssh-hostkey--", "--10881--", "Service: ssh"] 6 | }, 7 | { 8 | "name": "SMB Brute-force", 9 | "description": "Attempt to brute-force the local administrator account on these SMB servers.", 10 | "keywords": ["--smb-os-discovery--", "--11011--", "Service: microsoft-ds"] 11 | }, 12 | { 13 | "name": "Telnet Servers", 14 | "description": "Many devices have common telnet username and password combinations.", 15 | "keywords": ["--42263--"] 16 | }, 17 | { 18 | "name": "User Accounts", 19 | "description": "Local and Domain user accounts identified on the machine.", 20 | "keywords": ["--56211--", "--10860--", "--10399--"] 21 | }, 22 | { 23 | "name": "Open Windows Shares", 24 | "description": "Windows shared folders that allow anonymous access.", 25 | "keywords": ["--42411--"] 26 | }, 27 | { 28 | "name": "Open NFS Shares", 29 | "description": "NFS shared folders that do not require authentication.", 30 | "keywords": ["--11356--", "--42256--"] 31 | }, 32 | { 33 | "name": "Open AFP Shares", 34 | "description": "AFP shared folders that do not require authentication.", 35 | "keywords": ["--43580--"] 36 | }, 37 | { 38 | "name": "Apache Tomcat Credentials", 39 | "description": "Weak or Default Apache Tomcat Credentials", 40 | "keywords": ["--34970--"] 41 | }, 42 | { 43 | "name": "SNMP Default Community Strings", 44 | "description": "SNMP Servers that allow read and/or write access with default community strings.", 45 | "keywords": ["--10264--", "--41028--"] 46 | }, 47 | { 48 | "name": "Unauthenticated VNC Access", 49 | "description": "VNC servers that do not require authentication.", 50 | "keywords": ["--26925--"] 51 | }, 52 | { 53 | "name": "IPMI Authentication Bypass", 54 | "description": "IPMI authentication bypass using cipher suite zero.", 55 | "keywords": ["--68931--"] 56 | }, 57 | { 58 | "name": "Web Server Detection", 59 | "description": "Many web servers have easily exploited vulnerabilities.", 60 | "keywords": ["--10107--", "--http-server-header--"] 61 | }, 62 | { 63 | "name": "Default Credentials", 64 | "description": "These services allow access using default credentials.", 65 | "keywords": ["default credentials"] 66 | }, 67 | { 68 | "name": "Directory Traversal", 69 | "description": "These web servers are vulnerable to directory traversal flaws.", 70 | "keywords": ["directory traversal"] 71 | }, 72 | { 73 | "name": "Metasploit", 74 | "description": "These devices have vulnerabilities with associated Metasploit modules.", 75 | "keywords": ["Metasploit:"] 76 | }, 77 | { 78 | "name": "IKE Aggressive Mode", 79 | "description": "These servers allow IKE connections, they should be checked for aggressive mode support.", 80 | "keywords": ["--62694--"] 81 | }, 82 | { 83 | "name": "FTP Brute Force", 84 | "description": "Attempt to brute-force accounts on these FTP servers.", 85 | "keywords": ["--10092--"] 86 | }, 87 | { 88 | "name": "Anonymous FTP", 89 | "description": "These FTP servers allow anonymous access.", 90 | "keywords": ["--ftp-anon--"] 91 | } 92 | ] 93 | -------------------------------------------------------------------------------- /ptn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/averagesecurityguy/ptnotes/253f9785bf0d088b0f367605b6b8b78b2f1fbe87/ptn/__init__.py -------------------------------------------------------------------------------- /ptn/attacks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import sys 6 | import os.path 7 | import json 8 | import random 9 | 10 | import validate 11 | import database 12 | 13 | ## 14 | # Process all of the attacks in the JSON file and add new attacks to the 15 | # specified project file. 16 | # 17 | 18 | ATK_FILE = os.path.join('data', 'attacks.json') 19 | 20 | class Attack(): 21 | def __init__(self, project_file): 22 | self.db = database.ScanDatabase(project_file) 23 | self.log = logging.getLogger('ATTACK') 24 | self.attacks = self.load_attacks(ATK_FILE) 25 | 26 | def load_attacks(self, filename): 27 | """ 28 | Load attacks from JSON file. 29 | """ 30 | attacks = [] 31 | 32 | self.log.info('Loading attack file.') 33 | try: 34 | with open(filename) as f: 35 | attacks = json.loads(f.read()) 36 | 37 | except (IOError, ValueError) as e: 38 | self.log.critical('Could not load attack file: {0}'.format(e)) 39 | 40 | return attacks 41 | 42 | def find_attacks(self): 43 | """ 44 | Find all of the potential attacks in the project based on the attacks 45 | described in the attack file. 46 | """ 47 | for a in self.attacks: 48 | self.log.info('Finding attacks for {0}.'.format(a['name'])) 49 | items = self.get_items(a) 50 | 51 | if items != []: 52 | # If the attack does not exist, create it. If it does exist then 53 | # add hosts to it. 54 | attack = self.db.attackdb.get_attack_by_name(a['name']) 55 | if attack is None: 56 | self.db.attackdb.create_attack(a['name'], a['description'], items) 57 | else: 58 | self.db.attackdb.update_attack_hosts(attack['id'], items) 59 | 60 | def get_items(self, attack): 61 | """ 62 | Get all items matching the attack data. 63 | """ 64 | self.log.debug('Getting items for {0}.'.format(attack['name'])) 65 | items = [] 66 | 67 | items.extend(self.db.itemdb.get_items_by_keywords(attack.get('keywords'))) 68 | 69 | self.log.debug('Found {0} total items.'.format(len(items))) 70 | 71 | items = ["{0}:{1}:{2}".format(i[0], i[1], i[2]) for i in items] 72 | 73 | return items 74 | -------------------------------------------------------------------------------- /ptn/database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sqlite3 5 | import logging 6 | import sys 7 | import os.path 8 | import json 9 | import random 10 | 11 | import validate 12 | 13 | # 14 | # Initialize the database once when we import the module. 15 | # 16 | PRJ_FILE = os.path.join('data', 'projects.sqlite') 17 | 18 | def ip_key(ip): 19 | return tuple(int(part) for part in ip.split('.')) 20 | 21 | 22 | class DatabaseException(Exception): 23 | pass 24 | 25 | class Database(): 26 | """ 27 | Class to handle all database interactions. 28 | """ 29 | 30 | def __init__(self, filename): 31 | """ 32 | Setup the connection and initialize the database. 33 | """ 34 | self.log = logging.getLogger('DATABASE') 35 | self.valid = validate.Validate() 36 | self.filename = filename 37 | self.con = sqlite3.connect(self.filename) 38 | self.con.row_factory = sqlite3.Row 39 | self.cur = self.con.cursor() 40 | 41 | def __del__(self): 42 | """ 43 | Clean up the database connection if it exists. 44 | """ 45 | if self.con is not None: 46 | self.con.close() 47 | 48 | def get_tables(self): 49 | """ 50 | Get a list of tables in the database. 51 | """ 52 | stmt = "SELECT name FROM sqlite_master WHERE type='table'" 53 | if self.execute_sql(stmt) is True: 54 | return [n['name'] for n in self.cur.fetchall()] 55 | else: 56 | return [] 57 | 58 | def execute_sql(self, stmt, args=None, commit=True): 59 | """ 60 | Execute an SQL statement. 61 | 62 | Attempt to execute an SQL statement and log any errors. Return True if 63 | successful and false if not. 64 | """ 65 | self.log.debug('Executing {0} with args {1}.'.format(stmt, args)) 66 | 67 | try: 68 | if args is None: 69 | self.cur.execute(stmt) 70 | else: 71 | self.cur.execute(stmt, args) 72 | 73 | if commit is True: 74 | self.con.commit() 75 | 76 | return True 77 | 78 | except sqlite3.Error as e: 79 | self.log.debug(e) 80 | return False 81 | 82 | 83 | class ScanDatabase(): 84 | """ 85 | Class to handle scan data and attack notes. 86 | """ 87 | def __init__(self, filename): 88 | self.log = logging.getLogger('DATABASE') 89 | self.itemdb = ItemDatabase(filename) 90 | self.attackdb = AttackDatabase(filename) 91 | self.hostdb = HostDatabase(filename) 92 | self.importdb = ImportDatabase(filename) 93 | 94 | def get_stats(self): 95 | """ 96 | Get host and attack stats for the database. 97 | """ 98 | self.log.debug('Gathering stats.') 99 | 100 | hosts = len(self.itemdb.get_unique_hosts()) 101 | attacks = len(self.attackdb.get_attacks()) 102 | 103 | return 'Hosts: {0} Attacks {1}'.format(hosts, attacks) 104 | 105 | def get_host_details(self, ip): 106 | """ 107 | Get all information associated with an IP. 108 | """ 109 | host = {'note': '', 'items': []} 110 | 111 | host['note'] = self.hostdb.get_host_note(ip) 112 | host['items'] = self.itemdb.get_items_by_ip(ip) 113 | 114 | return host 115 | 116 | def get_summary(self): 117 | """ 118 | Get summary information for all of the hosts. 119 | """ 120 | summary = [] 121 | 122 | hosts = self.itemdb.get_unique_hosts() 123 | for host in hosts: 124 | h = dict(self.hostdb.get_host(host)) 125 | 126 | ports = self.itemdb.get_ports_by_ip(host) 127 | h['tcp'] = [str(p) for p in sorted(ports['tcp'])] 128 | h['udp'] = [str(p) for p in sorted(ports['udp'])] 129 | 130 | summary.append(h) 131 | 132 | summary = sorted(summary, key=lambda x: ip_key(x['ip'])) 133 | 134 | return summary 135 | 136 | def get_unique(self): 137 | unique = {} 138 | 139 | ips = [ip for ip in self.itemdb.get_unique_hosts()] 140 | unique['ip'] = sorted(ips, key=lambda x: ip_key(x)) 141 | 142 | ports = self.itemdb.get_unique_ports() 143 | unique['tcp'] = [str(p) for p in sorted(ports['tcp'])] 144 | unique['udp'] = [str(p) for p in sorted(ports['udp'])] 145 | 146 | return unique 147 | 148 | 149 | class ItemDatabase(Database): 150 | """ 151 | Class to handle item data. 152 | """ 153 | def __init__(self, filename): 154 | Database.__init__(self, filename) 155 | 156 | items = ''' 157 | CREATE TABLE IF NOT EXISTS items ( 158 | id integer primary key autoincrement, 159 | ip text, 160 | port integer, 161 | protocol text, 162 | note text, 163 | hash text 164 | ) 165 | ''' 166 | ires = self.execute_sql(items) 167 | 168 | if ires is False: 169 | raise DatabaseException('Could not create items table.') 170 | 171 | def create_item(self, ip, port, protocol, note, hash): 172 | """ 173 | Add new item. 174 | """ 175 | self.log.debug('Creating new item.') 176 | try: 177 | self.valid.ip(ip) 178 | self.valid.port(port) 179 | self.valid.protocol(protocol) 180 | self.valid.hash(hash) 181 | 182 | except AssertionError as e: 183 | self.log.error(e) 184 | return False 185 | 186 | stmt = "INSERT INTO items (ip, port, protocol, note, hash) VALUES(?,?,?,?,?)" 187 | return self.execute_sql(stmt, (ip, port, protocol, note, hash)) 188 | 189 | def get_item(self, item_id): 190 | """ 191 | Get all items associated with an item_id. 192 | """ 193 | self.log.debug('Getting information for item {0}.'.format(item_id)) 194 | stmt = "SELECT * FROM items WHERE id=?" 195 | 196 | if self.execute_sql(stmt, (item_id,)) is True: 197 | return self.cur.fetchone() 198 | else: 199 | return {} 200 | 201 | def get_unique_hosts(self): 202 | """ 203 | Get unique hosts listed in the item database. 204 | """ 205 | self.log.debug('Getting unique hosts.') 206 | stmt = "SELECT DISTINCT ip FROM items ORDER BY ip" 207 | 208 | if self.execute_sql(stmt) is True: 209 | return [h['ip'] for h in self.cur.fetchall()] 210 | else: 211 | return [] 212 | 213 | def get_unique_ports(self): 214 | """ 215 | Get unique ports in the database. 216 | """ 217 | ports = {'tcp': [], 'udp': []} 218 | 219 | self.log.debug('Getting unique TCP ports from the database.') 220 | stmt = """SELECT DISTINCT(port) FROM items 221 | WHERE port != 0 AND protocol == 'tcp' 222 | ORDER BY port ASC""" 223 | 224 | if self.execute_sql(stmt) is True: 225 | ports['tcp'] = [h['port'] for h in self.cur.fetchall()] 226 | 227 | self.log.debug('Getting unique UDP ports from the database.') 228 | stmt = """SELECT DISTINCT(port) FROM items 229 | WHERE port != 0 AND protocol == 'udp' 230 | ORDER BY port ASC""" 231 | 232 | if self.execute_sql(stmt) is True: 233 | ports['udp'] = [h['port'] for h in self.cur.fetchall()] 234 | 235 | return ports 236 | 237 | def get_ports_by_ip(self, ip): 238 | """ 239 | Get unique TCP and UDP ports associated with an IP. 240 | """ 241 | ports = {} 242 | 243 | self.log.debug('Getting unique TCP ports for {0}.'.format(ip)) 244 | stmt = """SELECT DISTINCT(port) FROM items 245 | WHERE port != 0 AND protocol == 'tcp' 246 | AND ip == ? 247 | ORDER BY port ASC""" 248 | 249 | if self.execute_sql(stmt, (ip,)) is True: 250 | ports['tcp'] = [h['port'] for h in self.cur.fetchall()] 251 | 252 | self.log.debug('Getting unique UDP ports for {0}.'.format(ip)) 253 | stmt = """SELECT DISTINCT(port) FROM items 254 | WHERE port != 0 AND protocol == 'udp' 255 | AND ip == ? 256 | ORDER BY port ASC""" 257 | 258 | if self.execute_sql(stmt, (ip,)) is True: 259 | ports['udp'] = [h['port'] for h in self.cur.fetchall()] 260 | 261 | return ports 262 | 263 | def get_items_by_ip(self, ip): 264 | """ 265 | Get all items associated with a host. 266 | """ 267 | self.log.debug('Getting items for host {0}.'.format(ip)) 268 | stmt = "SELECT * FROM items WHERE ip=?" 269 | 270 | if self.execute_sql(stmt, (ip,)) is True: 271 | return self.cur.fetchall() 272 | else: 273 | return [] 274 | 275 | def get_items_by_hash(self, hash): 276 | """ 277 | Return a list of hosts with the specified hash. 278 | """ 279 | self.log.debug('Getting items associated with hash {0}.'.format(hash)) 280 | 281 | stmt = "SELECT ip FROM items WHERE hash=?" 282 | if self.execute_sql(stmt, (hash,)) is True: 283 | return [i['ip'] for i in self.cur.fetchall()] 284 | else: 285 | return [] 286 | 287 | def get_items_by_keywords(self, keywords): 288 | """ 289 | Return a list of items with the specified keywords. 290 | """ 291 | if keywords is None: 292 | return [] 293 | else: 294 | self.log.debug('Getting items associated with keywords {0}.'.format(','.join(keywords))) 295 | 296 | stmt = "SELECT id, ip, port FROM items WHERE " 297 | stmt += ' OR '.join(["note LIKE ?" for i in xrange(len(keywords))]) 298 | kw_strs = tuple(['%{0}%'.format(kw) for kw in keywords]) 299 | 300 | if self.execute_sql(stmt, kw_strs) is True: 301 | return [(i['id'], i['ip'], i['port']) for i in self.cur.fetchall()] 302 | else: 303 | return [] 304 | 305 | 306 | class AttackDatabase(Database): 307 | """ 308 | Class to handle attack data. 309 | """ 310 | def __init__(self, filename): 311 | Database.__init__(self, filename) 312 | 313 | attacks = ''' 314 | CREATE TABLE IF NOT EXISTS attacks ( 315 | id integer primary key autoincrement, 316 | name text, 317 | description text, 318 | items text, 319 | note text 320 | ) 321 | ''' 322 | ares = self.execute_sql(attacks) 323 | 324 | if ares is False: 325 | raise DatabaseException('Could not create attack table.') 326 | 327 | def create_attack(self, name, description, items): 328 | """ 329 | Create a new attack in the database. 330 | """ 331 | self.log.debug('Creating new attack for {0}.'.format(name)) 332 | 333 | stmt = "INSERT INTO attacks (name, description, items, note) VALUES(?,?,?,?)" 334 | return self.execute_sql(stmt, (name, description, ','.join(items), '')) 335 | 336 | def get_attack_by_name(self, name): 337 | """ 338 | Get an attack id by name. 339 | """ 340 | self.log.debug('Getting attack id for {0}.'.format(name)) 341 | 342 | stmt = "SELECT id, note FROM attacks WHERE name=?" 343 | if self.execute_sql(stmt, (name, ), commit=False) is True: 344 | return self.cur.fetchone() 345 | else: 346 | return None 347 | 348 | def get_attack(self, aid): 349 | """ 350 | Get an attack and a list of potential targets. 351 | """ 352 | self.log.debug('Getting attack {0}.'.format(aid)) 353 | 354 | stmt = "SELECT * FROM attacks WHERE id=?" 355 | if self.execute_sql(stmt, (aid,), commit=False) is True: 356 | return self.cur.fetchone() 357 | else: 358 | return None 359 | 360 | def get_attacks(self): 361 | """ 362 | Get all potential attacks. 363 | """ 364 | self.log.debug('Getting all potential attacks.') 365 | 366 | stmt = "SELECT id, name, description FROM attacks" 367 | if self.execute_sql(stmt, commit=False) is True: 368 | return self.cur.fetchall() 369 | else: 370 | return [] 371 | 372 | def get_attack_notes(self): 373 | """ 374 | Get all attack notes. 375 | """ 376 | self.log.debug('Getting notes for all attacks.') 377 | 378 | stmt = "SELECT name, note FROM attacks" 379 | if self.execute_sql(stmt, commit=False) is True: 380 | return [(a['name'], a['note']) for a in self.cur.fetchall()] 381 | else: 382 | return [] 383 | 384 | def update_attack_hosts(self, aid, items): 385 | """ 386 | Update the attack items. 387 | """ 388 | self.log.debug('Updating items for attack {0}.'.format(aid)) 389 | 390 | stmt = "UPDATE attacks SET items=? WHERE id=?" 391 | return self.execute_sql(stmt, (','.join(items), aid)) 392 | 393 | def update_attack_note(self, aid, note): 394 | """ 395 | Update the attack note. 396 | """ 397 | self.log.debug('Updating note for attack {0}.'.format(aid)) 398 | 399 | stmt = "UPDATE attacks SET note=? WHERE id=?" 400 | return self.execute_sql(stmt, (note, aid)) 401 | 402 | 403 | class HostDatabase(Database): 404 | """ 405 | Class to handle host data. 406 | """ 407 | def __init__(self, filename): 408 | Database.__init__(self, filename) 409 | 410 | hosts = ''' 411 | CREATE TABLE IF NOT EXISTS hosts ( 412 | id integer primary key autoincrement, 413 | ip text, 414 | os text, 415 | fqdn text, 416 | note text 417 | ) 418 | ''' 419 | hres = self.execute_sql(hosts) 420 | 421 | if hres is False: 422 | raise DatabaseException('Could not create hosts table.') 423 | 424 | def create_host(self, ip, os, fqdn): 425 | """ 426 | Create a new host identified by the IP address. 427 | """ 428 | self.log.debug('Creating new host for {0}.'.format(ip)) 429 | 430 | stmt = "INSERT INTO hosts (ip, os, fqdn) VALUES(?,?,?)" 431 | return self.execute_sql(stmt, (ip, os, fqdn)) 432 | 433 | def get_host(self, ip): 434 | """ 435 | Get host data associated with ip. 436 | """ 437 | self.log.debug('Getting host data for {0}.'.format(ip)) 438 | stmt = "SELECT ip, os, fqdn FROM hosts WHERE ip=?" 439 | 440 | if self.execute_sql(stmt, (ip,)) is True: 441 | return self.cur.fetchone() 442 | else: 443 | return {} 444 | 445 | def get_host_ip(self, ip): 446 | """ 447 | Return the host if it exists in the database. 448 | """ 449 | self.log.debug('Getting host record associated with IP {0}.'.format(ip)) 450 | 451 | stmt = "SELECT ip FROM hosts WHERE ip=? LIMIT=1" 452 | if self.execute_sql(stmt, (ip,)) is True: 453 | return [i['ip'] for i in self.cur.fetchall()] 454 | else: 455 | return [] 456 | 457 | def get_host_notes(self): 458 | """ 459 | Get all notes for hosts. 460 | """ 461 | self.log.debug('Getting all host notes.') 462 | stmt = "SELECT ip, note from hosts ORDER BY ip" 463 | 464 | if self.execute_sql(stmt) is True: 465 | return self.cur.fetchall() 466 | else: 467 | return [] 468 | 469 | def get_host_note(self, ip): 470 | """ 471 | Get notes for the specified host. 472 | """ 473 | self.log.debug('Getting notes for {0}.'.format(ip)) 474 | stmt = "SELECT note from hosts WHERE ip=?" 475 | 476 | if self.execute_sql(stmt, (ip,)) is True: 477 | return self.cur.fetchone()['note'] 478 | else: 479 | return "" 480 | 481 | def update_host_note(self, ip, note): 482 | """ 483 | Update the host note. 484 | """ 485 | self.log.debug('Updating note for host {0}.'.format(ip)) 486 | 487 | stmt = "UPDATE hosts SET note=? WHERE ip=?" 488 | return self.execute_sql(stmt, (note, ip)) 489 | 490 | 491 | class ImportDatabase(Database): 492 | """ 493 | Class to handle import data. 494 | """ 495 | def __init__(self, filename): 496 | Database.__init__(self, filename) 497 | 498 | imports = ''' 499 | CREATE TABLE IF NOT EXISTS imports ( 500 | id integer primary key autoincrement, 501 | filename text 502 | ) 503 | ''' 504 | ires = self.execute_sql(imports) 505 | 506 | if ires is False: 507 | raise DatabaseException('Could not create imports table.') 508 | 509 | def get_imported_files(self): 510 | """ 511 | Get all imported files for the specified project id. 512 | """ 513 | self.log.debug('Getting all imported files.') 514 | stmt = "SELECT filename FROM imports ORDER BY filename" 515 | 516 | if self.execute_sql(stmt) is True: 517 | return [p['filename'] for p in self.cur.fetchall()] 518 | else: 519 | return [] 520 | 521 | def add_import_file(self, filename): 522 | """ 523 | Add a filename to the table of imported files for a project. 524 | """ 525 | self.log.debug('Adding imported file {0}.'.format(filename)) 526 | 527 | stmt = "INSERT INTO imports (filename) VALUES (?)" 528 | return self.execute_sql(stmt, (filename,)) 529 | 530 | 531 | class ProjectDatabase(Database): 532 | """ 533 | Keep track of projects and the database names associated with them. 534 | """ 535 | def __init__(self): 536 | Database.__init__(self, PRJ_FILE) 537 | tables = self.get_tables() 538 | self.log.debug('TABLES: {0}'.format(tables)) 539 | if not ('projects' in tables): 540 | self.initialize_project_database() 541 | 542 | def initialize_project_database(self): 543 | """ 544 | Create a new project database. 545 | """ 546 | projects = ''' 547 | CREATE TABLE IF NOT EXISTS projects ( 548 | id integer primary key autoincrement, 549 | name text, 550 | note text, 551 | dbfile text 552 | ) 553 | ''' 554 | res = self.execute_sql(projects) 555 | 556 | if res is False: 557 | raise DatabaseException('Could not initialize project database.') 558 | 559 | def create_project(self, name): 560 | """ 561 | Add new project. 562 | """ 563 | self.log.debug('Creating new project.') 564 | db_name = ''.join([random.choice('0123456789abcdef') for _ in range(12)]) 565 | db_name = os.path.join('data', db_name + '.sqlite') 566 | 567 | try: 568 | scan_db = ScanDatabase(db_name) 569 | stmt = "INSERT INTO projects (name, dbfile) VALUES(?,?)" 570 | return self.execute_sql(stmt, (name, db_name)) 571 | except DatabaseException: 572 | return False 573 | 574 | def get_project(self, pid): 575 | """ 576 | Get the project name and database file associated with the pid. 577 | """ 578 | self.log.debug('Getting project for {0}.'.format(pid)) 579 | stmt = "SELECT name, dbfile, note FROM projects WHERE id=?" 580 | 581 | if self.execute_sql(stmt, (pid,)) is True: 582 | return self.cur.fetchone() 583 | else: 584 | return None, None 585 | 586 | def get_projects(self): 587 | """ 588 | Get all projects. 589 | """ 590 | self.log.debug('Getting all projects.') 591 | stmt = "SELECT * FROM projects ORDER BY name" 592 | 593 | if self.execute_sql(stmt) is True: 594 | return self.cur.fetchall() 595 | else: 596 | return [] 597 | 598 | def update_project_note(self, pid, note): 599 | """ 600 | Update the project notes. 601 | """ 602 | self.log.debug('Updating project {0}.'.format(pid)) 603 | stmt = "UPDATE projects SET note=? WHERE id=?" 604 | return self.execute_sql(stmt, (note, pid)) 605 | 606 | def delete_project(self, pid): 607 | """ 608 | Delete the project and the associated database file. 609 | """ 610 | self.log.debug('Deleting project {0}.'.format(pid)) 611 | name, db_file, _ = self.get_project(pid) 612 | if name is None: 613 | self.log.error('Could not find project {0}.'.format(pid)) 614 | 615 | stmt = "DELETE FROM projects WHERE id=?" 616 | if self.execute_sql(stmt, (pid,)) is True: 617 | self.delete_file(db_file) 618 | else: 619 | self.error('Could not delete project {0} from database.'.format(pid)) 620 | 621 | def delete_file(self, filename): 622 | """ 623 | Delete the specified file. 624 | """ 625 | try: 626 | os.remove(filename) 627 | except Exception as e: 628 | self.log.error('Could not delete file {0}: {1}'.format(filename, e)) 629 | -------------------------------------------------------------------------------- /ptn/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class ScanImportError(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /ptn/importscan.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree 2 | import logging 3 | import hashlib 4 | 5 | import database 6 | import errors 7 | 8 | 9 | class Import(): 10 | 11 | def __init__(self, db_file): 12 | self.log = logging.getLogger('IMPORT') 13 | self.db = database.ScanDatabase(db_file) 14 | 15 | def import_scan(self, scan_data): 16 | file_type = self.get_file_type(scan_data[:300]) 17 | 18 | try: 19 | if file_type == 'Nessus': 20 | self.import_nessus(scan_data) 21 | elif file_type == 'Nmap': 22 | self.import_nmap(scan_data) 23 | else: 24 | self.log.error('Unknown file type, skipping import.') 25 | return 'Failed' 26 | 27 | except (errors.ScanImportError): 28 | self.log.error('Failed to parse scan file.') 29 | return False 30 | 31 | return True 32 | 33 | def get_file_type(self, header): 34 | """ 35 | Determine the source of the scan data. 36 | """ 37 | self.log.debug('Checking file type with header {0}.'.format(header)) 38 | if '' in header: 39 | return 'Nessus' 40 | elif '' in header: 41 | return 'Nmap' 42 | elif 'The item you were looking for could not be found. 4 | 5 |

Return to the Projects page.

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /ptn/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 |

The server encountered an internal error.

4 | 5 |

Return to the Projects page.

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /ptn/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 |

About Pentest Notes

4 | 5 |

Contact

6 |

Pentest Notes was originally developed by Stephen Haywood (AverageSecurityGuy). If you have questions or comments about the software feel free to get in touch with him via Twitter or email. If you have issues with the software please go to the project page on Github and open a new issue.

7 | 8 |

License

9 |
10 | Copyright (c) 2015, LCI Technology Group, LLC
11 | All rights reserved.
12 | 
13 | Redistribution and use in source and binary forms, with or without
14 | modification, are permitted provided that the following conditions are met:
15 | 
16 |  Redistributions of source code must retain the above copyright notice, this
17 |  list of conditions and the following disclaimer.
18 | 
19 |  Redistributions in binary form must reproduce the above copyright notice,
20 |  this list of conditions and the following disclaimer in the documentation
21 |  and/or other materials provided with the distribution.
22 | 
23 |  Neither the name of LCI Technology Group, LLC nor the names of its
24 |  contributors may be used to endorse or promote products derived from this
25 |  software without specific prior written permission.
26 | 
27 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
37 | POSSIBILITY OF SUCH DAMAGE.
38 | 
39 | 40 | {% endblock %} -------------------------------------------------------------------------------- /ptn/templates/attack.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 | 4 |

{{ name }}

5 |

6 | Project Summary | 7 | Imported Hosts | 8 | Attack Notes | 9 | Host Notes | 10 | Import Data 11 |

12 | 13 |
14 |

Notes

15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |

{{ attack['name'] }}

23 |

{{ attack['description'] }}

24 | 25 |
26 |

Affected Hosts

27 | 49 | 50 |

Raw Host List

51 | 59 |
60 |
61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /ptn/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pentest Notes 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

Pentest Notes

12 | 17 |
18 | 19 | 20 |
21 | {% block data %} 22 | {% endblock %} 23 |
24 | 25 | 26 |
27 |

© 2015 LCI Technology Group, LLC

28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /ptn/templates/host.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 | 4 |

{{ name }}

5 |

6 | Project Summary | 7 | Imported Hosts | 8 | Attack Notes | 9 | Host Notes | 10 | Import Data 11 |

12 | 13 |
14 |

Notes

15 |
16 |
17 | 18 |
19 |
20 | 21 | {% if details != {} %} 22 |
23 | 24 |

{{ host }}

25 |

Click on each port heading to see the associated data.

26 | 27 |
28 | {% for k in keys %} 29 |

{{ k }}

30 | 35 | {% endfor %} 36 |
37 |
38 | {% endif %} 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /ptn/templates/hosts.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 | 4 |

{{ name }}

5 |

6 | Project Summary | 7 | Imported Hosts | 8 | Attack Notes | 9 | Host Notes | 10 | Import Data 11 |

12 | 13 |

Imported Hosts

14 | 15 | {% if not hosts%} 16 |

No hosts have been imported yet, please import a Nessus or Nmap XML file.

17 | {% else %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for host in hosts %} 27 | 28 | 29 | 30 | 32 | 33 | 34 | {% endfor %} 35 |
IPHostnameOSTCP PortsUDP Ports
{{ host['ip'] }}{{ host['fqdn'] }}{{ host['os'] }} 31 | {{ ', '.join(host['tcp']) }}{{ ', '.join(host['udp']) }}
36 | {% endif %} 37 | 38 |

Unique Hosts

39 | {{ ', '.join(unique['ip']) }} 40 | 41 |

Unique TCP Ports

42 | {{ ','.join(unique['tcp']) }} 43 | 44 |

Unique UDP Ports

45 | {{ ','.join(unique['udp']) }} 46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /ptn/templates/import.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 | 4 |

{{ name }}

5 |

6 | Project Summary | 7 | Imported Hosts | 8 | Attack Notes | 9 | Host Notes | 10 | Import Data 11 |

12 | 13 |

Import Scan Data

14 |
15 |
16 | 17 |
18 | 19 |

Imported Files

20 |
    21 | {% for file in files %} 22 |
  • {{ file }}
  • 23 | {% endfor %} 24 |
25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /ptn/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 | 4 |

Overview

5 |

Pentest Notes allows testers to keep track of what can be done, has been done, and still needs to be done during a network penetration test. To use Pentest Notes, create a new project using the Projects tab. Next, select the newly created project and import any Nessus or Nmap XML files into the project. The imported data will be organized by hosts and attacks. Select a host to get detailed information about it. Select an attack to get more details about the attack including the hosts that may be vulnerable to the attack. If you would like to make suggestions to improve the tool, submit code, or report issues please head over to Github and contribute.

6 | {% endblock %} -------------------------------------------------------------------------------- /ptn/templates/item.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 | 4 |

{{ name }}

5 |

6 | Project Summary | 7 | Imported Hosts | 8 | Attack Notes | 9 | Host Notes | 10 | Import Data 11 |

12 | 13 | {% if item != {} %} 14 |

{{ item['ip'] }} ({{ item['port'] }}/{{ item['protocol'] }})

15 | 16 |
{{ item['note'] }}
17 | {% endif %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /ptn/templates/notes.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 | 4 |

{{ name }}

5 |

6 | Project Summary | 7 | Imported Hosts | 8 | Attack Notes | 9 | Host Notes | 10 | Import Data 11 |

12 | 13 | {% for note in notes %} 14 | {% if note[1] != "" and note[1] != None %} 15 |

{{ note[0] }}

16 |
{{ note[1] }}
17 | {% endif %} 18 | {% endfor %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /ptn/templates/project.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 | 4 |

{{ name }}

5 |

6 | Project Summary | 7 | Imported Hosts | 8 | Attack Notes | 9 | Host Notes | 10 | Import Data 11 |

12 | 13 |
14 |

Notes

15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |

Potential Attack Vectors

23 |

The following potential attack vectors were identified. Click on the name of the attack vector for additional details

24 | 25 |
26 | {% if not attacks %} 27 |

No attacks are available yet.

28 | {% else %} 29 | {% for a in attacks %} 30 | 33 | {% endfor %} 34 | 35 | 36 | {% endif %} 37 |
38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /ptn/templates/projects.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block data %} 3 | 4 |

Projects

5 |

Create New Project

6 |
7 | 8 | 9 |
10 | 11 |

Current Projects

12 | {% if not projects %} 13 |

No projects have been created yet.

14 | {% else %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for p in projects %} 22 | 23 | 24 | 25 | 28 | 29 | {% endfor %} 30 |
ProjectStatsActions
{{ p['name'] }}{{ stats[p['id']] }} 26 | Delete Project 27 |
31 | {% endif %} 32 | 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /ptn/validate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import re 6 | 7 | re_hash = re.compile('^[0-9a-f]{64}$') 8 | 9 | class Validate(): 10 | 11 | def __init__(self): 12 | self.log = logging.getLogger('VALID') 13 | self.protocols = ['tcp', 'udp', 'icmp'] 14 | self.min_port = 0 15 | self.max_port = 65535 16 | 17 | def ip(self, ip): 18 | """ 19 | Validate the IP address. 20 | """ 21 | self.log.debug('Validating IP address {0}.'.format(ip)) 22 | try: 23 | octets = ip.split('.') 24 | for octet in octets: 25 | o = int(octet) 26 | assert (o >= 0) and (o <= 255) 27 | 28 | return True 29 | 30 | except (TypeError, ValueError, AssertionError): 31 | self.log.error('The IP address must be in dotted quad notation.') 32 | raise AssertionError('Invalid IP address.') 33 | 34 | def port(self, port): 35 | self.log.debug("Validating port {0}.".format(port)) 36 | try: 37 | assert (port >= self.min_port) and (port <= self.max_port) 38 | 39 | except (TypeError, AssertionError): 40 | self.log.error('Port must be an integer from 0 to 65535') 41 | raise AssertionError('Invalid port.') 42 | 43 | def protocol(self, protocol): 44 | self.log.debug('Validating protocol {0}.'.format(protocol)) 45 | try: 46 | assert protocol in self.protocols 47 | 48 | except AssertionError: 49 | self.log.error('Protocol must be in {0}.'.format(', '.join(protocols))) 50 | raise AssertionError('Invalid protocol.') 51 | 52 | def hash(self, hash): 53 | self.log.debug('Validating hash {0}.'.format(hash)) 54 | try: 55 | m = re_hash.search(hash.lower()) 56 | assert m is not None 57 | 58 | except AssertionError: 59 | self.log.error('Hash must be a valid SHA256 hex value.') 60 | raise AssertionError('Invalid hash.') 61 | -------------------------------------------------------------------------------- /ptn/webserver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import flask 4 | from functools import wraps 5 | import logging 6 | 7 | import database 8 | import importscan 9 | import attacks 10 | 11 | 12 | #----------------------------------------------------------------------------- 13 | # WEB SERVER 14 | #----------------------------------------------------------------------------- 15 | app = flask.Flask(__name__) 16 | 17 | def get_project_db(pid): 18 | """ 19 | Get our project database. 20 | """ 21 | pdb = database.ProjectDatabase() 22 | project = pdb.get_project(pid) 23 | 24 | if project is None: 25 | flask.abort(404) 26 | 27 | return project 28 | 29 | 30 | @app.route("/") 31 | def index(): 32 | return flask.render_template('index.html') 33 | 34 | 35 | @app.route("/about") 36 | def about(): 37 | return flask.render_template('about.html') 38 | 39 | 40 | @app.route("/project//hosts") 41 | def hosts(pid): 42 | """ 43 | Get summary inforation about all imported hosts. 44 | """ 45 | project = get_project_db(pid) 46 | db = database.ScanDatabase(project['dbfile']) 47 | 48 | hosts = db.get_summary() 49 | unique = db.get_unique() 50 | 51 | return flask.render_template('hosts.html', pid=pid, name=project['name'], 52 | hosts=hosts, unique=unique) 53 | 54 | 55 | @app.route('/project//host/', methods=['GET', 'POST']) 56 | def host(pid, ip): 57 | """ 58 | Get all the information about a host. 59 | """ 60 | project = get_project_db(pid) 61 | db = database.ScanDatabase(project['dbfile']) 62 | 63 | if flask.request.method == 'POST': 64 | note = flask.request.form['note'] 65 | db.hostdb.update_host_note(ip, note) 66 | 67 | data = db.get_host_details(ip) 68 | 69 | if data is None: 70 | flask.abort(404) 71 | 72 | details = {} 73 | for item in data['items']: 74 | key = "{0}/{1}".format(item['port'], item['protocol']) 75 | if details.get(key) is None: 76 | details[key] = [] 77 | details[key].append(item['note']) 78 | else: 79 | details[key].append(item['note']) 80 | 81 | keys = sorted(details.keys(), key=lambda x: int(x.split('/')[0])) 82 | note = data['note'] 83 | 84 | return flask.render_template('host.html', pid=pid, host=ip, 85 | details=details, keys=keys, note=note, 86 | name=project['name']) 87 | 88 | 89 | @app.route('/project//host/notes') 90 | def host_notes(pid): 91 | """ 92 | Display all host notes. 93 | """ 94 | project = get_project_db(pid) 95 | db = database.ScanDatabase(project['dbfile']) 96 | notes = db.hostdb.get_host_notes() 97 | 98 | return flask.render_template('notes.html', pid=pid, notes=notes, 99 | name=project['name']) 100 | 101 | 102 | @app.route('/project//item/') 103 | def item(pid, item_id): 104 | """ 105 | Get all the information about an item. 106 | """ 107 | project = get_project_db(pid) 108 | db = database.ScanDatabase(project['dbfile']) 109 | item = db.itemdb.get_item(item_id) 110 | 111 | if item is None: 112 | flask.abort(404) 113 | 114 | return flask.render_template('item.html', pid=pid, item=item, 115 | name=project['name']) 116 | 117 | 118 | @app.route('/project//attack/', methods=['GET', 'POST']) 119 | def get_attack(pid, aid): 120 | """ 121 | Get list of all the hosts possibly vulnerable to the attack. 122 | """ 123 | project = get_project_db(pid) 124 | db = database.ScanDatabase(project['dbfile']) 125 | 126 | if flask.request.method == 'POST': 127 | note = flask.request.form['note'] 128 | db.attackdb.update_attack_note(aid, note) 129 | 130 | attack = db.attackdb.get_attack(aid) 131 | 132 | if attack is None: 133 | flask.abort(404) 134 | 135 | items = [i.split(':') for i in attack['items'].split(',')] 136 | 137 | return flask.render_template('attack.html', pid=pid, attack=attack, 138 | items=items, name=project['name']) 139 | 140 | 141 | @app.route('/project//import', methods=['GET', 'POST']) 142 | def import_scan(pid): 143 | """ 144 | Import scan data into the database associated with the pid. 145 | """ 146 | project = get_project_db(pid) 147 | db = database.ScanDatabase(project['dbfile']) 148 | 149 | if flask.request.method == 'GET': 150 | files = db.importdb.get_imported_files() 151 | 152 | return flask.render_template('import.html', pid=pid, files=files, 153 | name=project['name']) 154 | 155 | else: 156 | i = importscan.Import(project['dbfile']) 157 | scans = flask.request.files.getlist("scans[]") 158 | 159 | for scan in scans: 160 | res = i.import_scan(scan.read()) 161 | if res is True: 162 | db.importdb.add_import_file(scan.filename) 163 | 164 | a = attacks.Attack(project['dbfile']) 165 | a.find_attacks() 166 | 167 | return flask.redirect(flask.url_for('get_project', pid=pid)) 168 | 169 | 170 | @app.route('/project//attack/notes') 171 | def attack_notes(pid): 172 | """ 173 | Display all attack notes. 174 | """ 175 | project = get_project_db(pid) 176 | db = database.ScanDatabase(project['dbfile']) 177 | notes = db.attackdb.get_attack_notes() 178 | 179 | return flask.render_template('notes.html', pid=pid, notes=notes, 180 | name=project['name']) 181 | 182 | 183 | @app.route('/projects', methods=['GET', 'POST']) 184 | def projects(): 185 | """ 186 | Get a list of all projects. 187 | """ 188 | pdb = database.ProjectDatabase() 189 | stats = {} 190 | 191 | if flask.request.method == 'POST': 192 | name = flask.request.form['project_name'] 193 | pdb.create_project(name) 194 | 195 | project_list = pdb.get_projects() 196 | for project in project_list: 197 | db = database.ScanDatabase(project['dbfile']) 198 | stats[project['id']] = db.get_stats() 199 | 200 | return flask.render_template('projects.html', projects=project_list, stats=stats) 201 | 202 | 203 | @app.route('/project/') 204 | def get_project(pid): 205 | """ 206 | Get a project, including the list of hosts attacks. 207 | """ 208 | project = get_project_db(pid) 209 | 210 | db = database.ScanDatabase(project['dbfile']) 211 | attacks = db.attackdb.get_attacks() 212 | 213 | return flask.render_template('project.html', pid=pid, note=project['note'], 214 | name=project['name'], attacks=attacks) 215 | 216 | 217 | @app.route('/project//notes', methods=['GET', 'POST']) 218 | def project_notes(pid): 219 | """ 220 | Display all project notes. 221 | """ 222 | pdb = database.ProjectDatabase() 223 | project = get_project_db(pid) 224 | 225 | if flask.request.method == 'POST': 226 | note = flask.request.form['note'] 227 | pdb.update_project_note(pid, note) 228 | 229 | return flask.redirect(flask.url_for('get_project', pid=pid)) 230 | else: 231 | return flask.render_template('project_notes.html', pid=pid, 232 | name=project['name'], note=project['note']) 233 | 234 | @app.route('/project//delete') 235 | def delete_project(pid): 236 | """ 237 | Delete the specified project. 238 | """ 239 | pdb = database.ProjectDatabase() 240 | project = pdb.delete_project(pid) 241 | 242 | return flask.redirect(flask.url_for('projects')) 243 | 244 | 245 | @app.errorhandler(404) 246 | def page_not_found(e): 247 | return flask.render_template('404.html'), 404 248 | 249 | 250 | @app.errorhandler(500) 251 | def inernal_error(e): 252 | return flask.render_template('500.html'), 500 253 | -------------------------------------------------------------------------------- /server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import os 6 | import argparse 7 | 8 | #Parse command line arguments using argparse 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('-l', action='store', default='127.0.0.1', metavar='LISTEN_ADDRESS', 11 | help='Address to listen on. Default is 127.0.0.1') 12 | parser.add_argument('-p', action='store', type=int, default=5000, metavar="LISTEN_PORT", 13 | help='Port to listen on. Default is 5000.') 14 | parser.add_argument('-d', action='store_true', default=False, 15 | help='Enable Flask debugging. Should not be used in production.') 16 | 17 | args = parser.parse_args() 18 | 19 | SERVER = args.l 20 | PORT = args.p 21 | DEBUG = args.d 22 | LOG_LEVEL = logging.INFO 23 | 24 | #----------------------------------------------------------------------------- 25 | # Do not edit anything below this line. 26 | #----------------------------------------------------------------------------- 27 | 28 | try: 29 | os.mkdir('log') 30 | except OSError: 31 | # Log directory already exists 32 | pass 33 | 34 | log_file = os.path.join('log', 'ptnotes.log') 35 | 36 | logging.basicConfig( 37 | level=LOG_LEVEL, 38 | filename=log_file) 39 | 40 | console = logging.StreamHandler() 41 | console.setLevel(logging.ERROR) 42 | formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') 43 | console.setFormatter(formatter) 44 | logging.getLogger('').addHandler(console) 45 | 46 | logging.info('Starting PTNotes server.') 47 | print('Starting PTNotes server on {0}:{1}'.format(SERVER, PORT)) 48 | 49 | cert_file = os.path.join('config', 'cert.pem') 50 | key_file = os.path.join('config', 'key.pem') 51 | 52 | import ptn.webserver as server 53 | server.app.run(host=SERVER, port=PORT, debug=DEBUG, ssl_context=(cert_file, key_file)) 54 | --------------------------------------------------------------------------------