├── ChangeLog ├── Guinevere.py ├── README.md └── static └── G-Checklist ├── G-Checklist.css └── G-Checklist_Template.html /ChangeLog: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | Added --debug command line argument 3 | Changed colors for printing vulnerability criticalities to the terminal 4 | Added gather_assessment_details function to gather additional information about the assessment from Gauntlet 5 | Added new menu item "Generate Assessment JSON File" and associated function generate_assessment_json 6 | 7 | 1.3.0 8 | Added --ports command line argument. Specify the flag to EXCLUDE port information from generated report 9 | Generated output will now include host followed by port (i.e. 192.168.1.10:445/tcp) 10 | -if port is 0/tcp or 0/udp, port information will be excluded 11 | Modified Vulnerability headings to a level 4 header 12 | Modified Vulnerability narrative to be aligned justified 13 | 14 | 1.4.0 15 | Addedd --cvss command line argument. Specify the flag to EXCLUDE CVSS scores from the vulnerability titles 16 | Imported a new library: cvss 17 | Added new get_cvss function 18 | Updated write_multi_vul & write_single_vul to print CVSS score with the header in this format: "CVSS: 10.0 - High" 19 | Added new CVSS column to multi vuln tables -------------------------------------------------------------------------------- /Guinevere.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #!/usr/bin/env python 3 | 4 | """Guinevere is a tool used to automate security assessment reporting""" 5 | 6 | import MySQLdb, os, docx, argparse, math, netaddr, logging, readline, sys, json 7 | from warnings import filterwarnings, resetwarnings 8 | from docx.enum.text import WD_ALIGN_PARAGRAPH 9 | from cvss import CVSS2 10 | 11 | #Requires MySQL driver, python-mysqldb for Linux. Seems to be installed in Kali 12 | #Requires python-docx library, apt-get update; apt-get install -y python-pip mysql-client;pip install python-docx 13 | #Requires cvsss library; pip install cvss 14 | ### OSX Install Notes: 15 | # sudo su - 16 | # export CFLAGS=-Qunused-arguments 17 | # export CPPFLAGS=-Qunused-arguments 18 | # pip install mysql-python 19 | # pip install python-docx 20 | # pip install netaddr 21 | 22 | ################################################# 23 | # Guinevere Variables # 24 | ################################################# 25 | __author__ = "Russel Van Tuyl" 26 | __license__ = "GPL" 27 | __version__ = "1.4.1" 28 | __maintainer__ = "Russel Van Tuyl" 29 | __email__ = "Russel.VanTuyl@gmail.com" 30 | __status__ = "Development" 31 | G_root = os.path.dirname(os.path.realpath(__file__)) 32 | readline.parse_and_bind('tab: complete') 33 | readline.set_completer_delims('\t') 34 | # logging.basicConfig(stream=sys.stdout, format='%(asctime)s\t%(levelname)s\t%(message)s', 35 | # datefmt='%Y-%m-%d %I:%M:%S %p', level=logging.DEBUG) # Log to STDOUT 36 | logging.basicConfig(filename=os.path.join(G_root, 'Guinevere.log'), format='%(asctime)s\t%(levelname)s\t%(message)s', 37 | datefmt='%Y-%m-%d %I:%M:%S %p', level=logging.DEBUG) # Log to File 38 | ################################################# 39 | #CHANGE TO MATCH YOUR DATABASE 40 | g_ip = "127.0.0.1" # Database IP address 41 | g_p = 3306 # Database Port 42 | g_user = "gauntlet" # Database Username 43 | g_pass = "password" # Database Password 44 | ################################################# 45 | # COLORS # 46 | ################################################# 47 | note = "\033[0;0;33m-\033[0m" 48 | warn = "\033[0;0;31m!\033[0m" 49 | info = "\033[0;0;36mi\033[0m" 50 | question = "\033[0;0;37m?\033[0m" 51 | debug = "\033[0;0;31m[DEBUG]\033[0m" 52 | 53 | #Parse command line arguments 54 | parser = argparse.ArgumentParser() 55 | parser.add_argument('-H', '--db-host', type=str, default=g_ip, help="MySQL Database Host. Default set in script") 56 | parser.add_argument('-U', '--db-user', type=str, default=g_user, help="MySQL Database Username. Default set in script") 57 | parser.add_argument('-P', '--db-pass', type=str, default=g_pass, help="MySQL Database Password. Default set in script") 58 | parser.add_argument('-p', '--db-port', type=str, default=g_p, help="MySQL Database Port. Default set in script") 59 | parser.add_argument('-l', '--lines', type=int, default=10, help="Number of lines to display when selecting an " 60 | "engagement. Default is 10") 61 | parser.add_argument('-A', '--all-vulns', action='store_true', default=False, help="Include all vulnerability headings " 62 | "when there are no associated report " 63 | "narratives") 64 | parser.add_argument('-V', '--all-verb', action='store_true', default=False, help="Include all vureto vulnerability " 65 | "verbiage when there are no " 66 | "associated report narratives") 67 | parser.add_argument('--ports', action='store_false', default=True, help="Exclude port information vulnerability " 68 | "write-up portion of the report") 69 | parser.add_argument('--cvss', action='store_false', default=True, help="Exclude CVSS scores from vulnerability titles") 70 | parser.add_argument('-sC', action='store_false', default=True, help="Exclude Critical-Severity Vulnerabilities") 71 | parser.add_argument('-sH', action='store_false', default=True, help="Exclude High-Severity Vulnerabilities") 72 | parser.add_argument('-sM', action='store_false', default=True, help="Exclude Medium-Severity Vulnerabilities") 73 | parser.add_argument('-sL', action='store_true', default=False, help="Include Low-Severity Vulnerabilities") 74 | parser.add_argument('-sI', action='store_true', default=False, help="Include Informational-Severity Vulnerabilities") 75 | parser.add_argument('-aD', '--assessment-date', action='store_true', default=False, help='Include the date when ' 76 | 'selecting an assessment ' 77 | 'to report on') 78 | parser.add_argument('-T', '--tool-output', action='store_false', default=True, help="Exclude Tool Output When Printing " 79 | "G-Checklist") 80 | parser.add_argument('--debug', action='store_true', default=False, help="Enable debug output to console") 81 | args = parser.parse_args() 82 | 83 | 84 | def get_assessment(info_string_for_user): 85 | """Connects to the assessment database and prompts user to select an engagement""" 86 | 87 | logging.info('Entering the get_assessment function') 88 | db = MySQLdb.connect(host=args.db_host, user=args.db_user, passwd=args.db_pass, port=args.db_port) 89 | gauntlet=db.cursor() 90 | gauntlet.execute("""show databases""") 91 | all_available_databases = gauntlet.fetchall() 92 | gauntlet.close() 93 | discovered_gauntlet_databases = [] 94 | os.system('clear') 95 | banner() 96 | 97 | # number of databases to print at a time 98 | database_print_limit_counter = args.lines 99 | 100 | # add all databases that start with 'gauntlet_' to a list 101 | for discovered_db in all_available_databases: 102 | if discovered_db[0].startswith('gauntlet'): 103 | discovered_gauntlet_databases.append(discovered_db[0].replace("gauntlet_", '')) 104 | 105 | #Print engagements to screen and have user choose one 106 | # Counter for number of engagements printed to the screen 107 | printed_engagement_counter = 0 108 | z = False 109 | # Counter for the number of engagements to display at a time 110 | eng_display_limit_counter = database_print_limit_counter 111 | while z != True: 112 | if (printed_engagement_counter <= eng_display_limit_counter) and (printed_engagement_counter < len(discovered_gauntlet_databases)): 113 | if args.assessment_date: 114 | SDate = db_query('select value FROM engagement_details WHERE `key`="Start Date"', discovered_gauntlet_databases[printed_engagement_counter]) 115 | if SDate is not None: 116 | print "[" + str(printed_engagement_counter) + "]" + discovered_gauntlet_databases[printed_engagement_counter] + "\t" + SDate[0][0] 117 | else: 118 | print "[" + str(printed_engagement_counter) + "] " + discovered_gauntlet_databases[printed_engagement_counter] 119 | printed_engagement_counter += 1 120 | else: 121 | print "[99] More..." 122 | user_selection_string = raw_input("\n[" + question +"]Please select " + info_string_for_user + ": ") 123 | try: 124 | if ((user_selection_string == "99") or (user_selection_string == "")): 125 | if printed_engagement_counter == len(discovered_gauntlet_databases): 126 | printed_engagement_counter = 0 127 | eng_display_limit_counter = database_print_limit_counter 128 | else: 129 | eng_display_limit_counter = eng_display_limit_counter + database_print_limit_counter 130 | os.system('clear') 131 | banner() 132 | elif user_selection_string == "Q" or user_selection_string == "q": 133 | exit() 134 | elif discovered_gauntlet_databases[int(user_selection_string)]: 135 | z = True 136 | else: 137 | pass 138 | except: 139 | os.system('clear') 140 | banner() 141 | print "["+warn+"]ERROR: " + user_selection_string + " is not a valid option. Try again" 142 | printed_engagement_counter = 0 143 | eng_display_limit_counter = database_print_limit_counter 144 | os.system('clear') 145 | logging.info(discovered_gauntlet_databases[int(user_selection_string)] + " assessment selected by user") 146 | 147 | return discovered_gauntlet_databases[int(user_selection_string)] 148 | 149 | 150 | def get_crosstable(assessment): 151 | """Select the which assessment crosstable to use""" 152 | 153 | logging.info('Entering the get_crosstable function') 154 | chosen_crosstable = "" 155 | list_of_crosstables = () 156 | tries = 0 157 | #Find all the crosstables for the assessment 158 | while not list_of_crosstables: 159 | try: 160 | if tries < 100: 161 | q = """SELECT DISTINCT table_id from cross_data_nva""" 162 | list_of_crosstables = db_query(q, assessment) 163 | else: 164 | print "Its broken try again" 165 | break 166 | except: 167 | tries = tries+1 168 | 169 | while not chosen_crosstable: 170 | 171 | # If there is more than 1 crosstable, have the user choose 172 | if len(list_of_crosstables) > 1: 173 | list_number = 0 174 | for crosstable in list_of_crosstables: 175 | print "[" + str(list_number) + "]",crosstable[0] 176 | list_number += 1 177 | try: 178 | user_choice = raw_input("\nWhich Crosstable would you like to use: ") 179 | chosen_crosstable = list_of_crosstables[int(user_choice)] 180 | except: 181 | os.system('clear') 182 | banner() 183 | print "["+warn+"]Error: please try again" 184 | 185 | # Otherwise choose for the user 186 | else: 187 | chosen_crosstable = list_of_crosstables[0] 188 | 189 | os.system('clear') 190 | banner() 191 | logging.info(str(chosen_crosstable[0]) + " crosstable selected") 192 | return chosen_crosstable[0] 193 | 194 | 195 | def assessment_vulns(assessment, crosstable): 196 | """Builds a list of the assessment vulnerabilities""" 197 | 198 | vulns = [] 199 | plugins = "" 200 | #Import data from gauntlet db for the selected crosstable 201 | hosts = db_query("select * from cross_data_nva WHERE table_id = '%s'" % (crosstable), assessment) 202 | 203 | # Vureto Vuln ID is in spot 2 of the tuple returned by i in the following loop 204 | Vureto_Vuln_ID_Location = 2 205 | 206 | for i in hosts: 207 | Vureto_Vuln_ID = i[Vureto_Vuln_ID_Location] 208 | if ((Vureto_Vuln_ID.startswith('Nessus') or Vureto_Vuln_ID.startswith('Netsparker') or Vureto_Vuln_ID.startswith('Acunetix') 209 | or Vureto_Vuln_ID.startswith('BurpSuite')) and Vureto_Vuln_ID not in plugins): 210 | plugins += Vureto_Vuln_ID 211 | else: 212 | vulns.append(Vureto_Vuln_ID) 213 | if plugins != "": 214 | pass 215 | return vulns 216 | 217 | 218 | def assessment_report(vulnerability_id_info_mapping_list): 219 | """Builds a unique list of Report IDs for the selected assessment and crosstable""" 220 | 221 | logging.info('Entered assessment_report function') 222 | temp = [] 223 | for vulnerability_id in vulnerability_id_info_mapping_list: 224 | if args.debug: 225 | logging.info('Vulnerability: ' + vulnerability_id + " - " + vulnerability_id_info_mapping_list[vulnerability_id]['vuln_title']) 226 | if vulnerability_id_info_mapping_list[vulnerability_id]['vuln_report_id'] is not None: 227 | temp.append(vulnerability_id_info_mapping_list[vulnerability_id]['vuln_report_id']) 228 | else: 229 | pass 230 | logging.info('Leaving assessment_report_function') 231 | return set(temp) 232 | 233 | 234 | def get_vulns(vuln_IDs, assessment, crosstable): 235 | """Build dictionary containing the assessment vulnerabilities and their associated information""" 236 | 237 | logging.info('Entered get_vulns function...') 238 | vulns = {} 239 | plugins = "" 240 | tools = ['Nessus', 'Netsparker', 'Acunetix', 'BurpSuite', 'Nmap', 'Nikto', 'dirb'] # names of tools to ignore 241 | db = MySQLdb.connect(host=args.db_host, user=args.db_user, passwd=args.db_pass, port=args.db_port, db='GauntletData') 242 | filterwarnings('ignore', category = MySQLdb.Warning) # Disable MySQL Warnings 243 | 244 | # Variable for progress bar 245 | countvulnVar = 0 246 | 247 | # vuln_IDs is an array of vuln id's created in assessment_vulns function 248 | for vuln_id in vuln_IDs: 249 | 250 | # Create a progress bar 251 | countvulnVar = countvulnVar + 1.0 252 | progress = str(countvulnVar / len(vuln_IDs) * 100) 253 | print "\r\t[" + note + "]Querying Database For Vulnerability Information:", progress[:5] + "%", 254 | 255 | #TODO to remove "Nessus 1111" entries 256 | 257 | if vuln_id.split()[0] in tools: 258 | if vuln_id not in plugins and vuln_id is "": 259 | plugins += "\t["+warn+"]" + vuln_id + " plugin needs to be added to your Gauntlet database" 260 | elif vuln_id not in plugins: 261 | plugins += "\n\t["+warn+"]" + vuln_id + " plugin needs to be added to your Gauntlet database" 262 | 263 | # Get vulnerability verbage 264 | else: 265 | gauntlet = db.cursor() 266 | gauntlet.execute("""select title, description, solution, report_id from vulns WHERE gnaat_id=%s""", (vuln_id,)) 267 | temp = gauntlet.fetchone() 268 | gauntlet.close() 269 | if temp[3] is not None: 270 | 271 | vulns[vuln_id] = {'vuln_id': vuln_id, 'vuln_title': temp[0], 'vuln_desc': temp[1], 'vuln_sol': temp[2], 'vuln_report_id': int(temp[3])} 272 | else: 273 | vulns[vuln_id] = {'vuln_id': vuln_id, 'vuln_title': temp[0], 'vuln_desc': temp[1], 'vuln_sol': temp[2], 'vuln_report_id': temp[3]} 274 | 275 | db2 = MySQLdb.connect(host=args.db_host, user=args.db_user, passwd=args.db_pass, port=args.db_port, db='gauntlet_'+ assessment) 276 | #Add all hosts with the associated vulnerability to the rpt dictionary 277 | print "" 278 | countvulnVar = 0 279 | 280 | #Determine if cross_data_nva has a 'port' column 281 | portColumn = False 282 | gauntlet=db2.cursor() 283 | gauntlet.execute("""SHOW COLUMNS FROM cross_data_nva FROM gauntlet_%s;""" % assessment) 284 | columns = gauntlet.fetchall() 285 | gauntlet.close 286 | for c in columns: 287 | if c[0] == 'port': 288 | portColumn = True 289 | 290 | for vuln_id in vuln_IDs: 291 | 292 | # Create progress bar 293 | countvulnVar = countvulnVar + 1.0 294 | progress = str(countvulnVar / len(vuln_IDs) * 100) 295 | print "\r\t[" + note + "]Querying Database For Affected Hosts:", progress[:5] + "%", 296 | 297 | # Ignore id's in tool list 298 | if vuln_id.split()[0] in tools: 299 | pass 300 | else: 301 | gauntlet=db2.cursor() 302 | if args.ports and portColumn: 303 | gauntlet.execute( 304 | """SELECT host, port, protocol FROM cross_data_nva WHERE cross_data_nva.table_id =%s 305 | AND vuln_id=%s AND (s1='Y' or s2='Y' or s3='Y' or s4='Y' or s5='Y')""", 306 | (crosstable,vuln_id)) 307 | else: 308 | gauntlet.execute( 309 | """SELECT host FROM cross_data_nva WHERE cross_data_nva.table_id =%s AND vuln_id=%s 310 | AND (s1='Y' or s2='Y' or s3='Y' or s4='Y' or s5='Y')""", 311 | (crosstable, vuln_id)) 312 | temp2 = gauntlet.fetchall() 313 | gauntlet.close() 314 | #print vuln[j]['vuln_title'], temp2 #DEBUG 315 | vulns[vuln_id].update({'vuln_hosts': temp2}) 316 | 317 | #Determine the rank of the vulnerability 318 | print "" 319 | countvulnVar = 0 320 | for vuln_id in vuln_IDs: 321 | 322 | # Create progress bar 323 | countvulnVar = countvulnVar + 1.0 324 | progress = str(countvulnVar / len(vuln_IDs) * 100) 325 | print "\r\t[" + note + "]Querying Database For Vulnerability Rank:", progress[:5] + "%", 326 | 327 | # Craft assessment string for SQL query 328 | if assessment == "GauntletData": 329 | assessment_name = assessment 330 | else: 331 | assessment_name = "gauntlet_" + assessment 332 | 333 | # Ignore ids in tool list 334 | if vuln_id.split()[0] in tools: 335 | pass 336 | # Select severity values from cross_tables 337 | else: 338 | temp4 = db_query("""SELECT s1, s2, s3, s4, s5 FROM cross_data_nva WHERE table_id ='%s' AND vuln_id = %s;""" % (crosstable, vuln_id), assessment) 339 | severities = [] 340 | for value in temp4: 341 | if value[0] is 'Y' and ('Critical' not in severities): 342 | severities.append('Critical') 343 | elif value[1] is 'Y' and ('High' not in severities): 344 | severities.append('High') 345 | elif value[2] is 'Y' and ('Medium' not in severities): 346 | severities.append('Medium') 347 | elif value[3] is 'Y' and ('Low' not in severities): 348 | severities.append('Low') 349 | elif value[4] is 'Y' and ('Informational' not in severities): 350 | severities.append('Informational') 351 | else: 352 | if None in severities: 353 | pass 354 | else: 355 | severities.append(None) 356 | 357 | # Update vuln_rating to the highest discovered severity 358 | if 'Critical' in severities: 359 | vulns[vuln_id].update({'vuln_rating': 'Critical'}) 360 | elif 'High' in severities: 361 | vulns[vuln_id].update({'vuln_rating': 'High'}) 362 | elif 'Medium' in severities: 363 | vulns[vuln_id].update({'vuln_rating': 'Medium'}) 364 | elif 'Low' in severities: 365 | vulns[vuln_id].update({'vuln_rating': 'Low'}) 366 | elif 'Informational' in severities: 367 | vulns[vuln_id].update({'vuln_rating': 'Informational'}) 368 | else: 369 | vulns[vuln_id].update({'vuln_rating': None}) 370 | 371 | if plugins != "": 372 | print "%s" %(plugins,), 373 | print "" 374 | resetwarnings() #Re-enable MySQL Warnings 375 | return vulns 376 | 377 | 378 | def get_report(report_record_IDs, vuln_ID_info_mapping): 379 | """Build a dictionary containing all of the reporting information""" 380 | logging.info("Entering get_report function") 381 | 382 | rpt = {} 383 | 384 | # change to GauntletData after dev/or vureto for dev 385 | db = MySQLdb.connect(host=args.db_host, user=args.db_user, passwd=args.db_pass, port=args.db_port, db='GauntletData') 386 | 387 | for report_record_ID in report_record_IDs: 388 | gauntlet = db.cursor() 389 | gauntlet.execute("""select title, identification, explanation, impact, recommendation from report 390 | WHERE report_id=%s""" , (report_record_ID,)) 391 | temp = gauntlet.fetchone() 392 | gauntlet.close() 393 | rpt[report_record_ID] = {'report_id': report_record_ID, 'report_title': temp[0], 'report_identification': temp[1], 394 | 'report_explanation': temp[2], 'report_impact': temp[3], 'report_recommendation': temp[4]} 395 | 396 | # Add all vulnerabilities with this report ID to the dictionary 397 | for vuln_ID in vuln_ID_info_mapping: 398 | if vuln_ID_info_mapping[vuln_ID]['vuln_report_id'] is not None: 399 | if 'vulns' in rpt[vuln_ID_info_mapping[vuln_ID]['vuln_report_id']]: 400 | rpt[vuln_ID_info_mapping[vuln_ID]['vuln_report_id']]['vulns'][vuln_ID_info_mapping[vuln_ID]['vuln_id']] = vuln_ID_info_mapping[vuln_ID] 401 | else: 402 | rpt[vuln_ID_info_mapping[vuln_ID]['vuln_report_id']]['vulns'] = {vuln_ID_info_mapping[vuln_ID]['vuln_id']: vuln_ID_info_mapping[vuln_ID]} 403 | else: 404 | pass 405 | 406 | # Determine the highest severity level and set it for the reporting record 407 | for record in rpt: 408 | r = [] 409 | for vulnerability in rpt[record]['vulns']: 410 | r.append(rpt[record]['vulns'][vulnerability]['vuln_rating']) 411 | if 'Critical' in r: 412 | rpt[record]['report_rating'] = 'Critical' 413 | continue 414 | elif 'High' in r: 415 | rpt[record]['report_rating'] = 'High' 416 | continue 417 | elif 'Medium' in r: 418 | rpt[record]['report_rating'] = 'Medium' 419 | continue 420 | elif 'Low' in r: 421 | rpt[record]['report_rating'] = 'Low' 422 | continue 423 | elif 'Informational' in r: 424 | rpt[record]['report_rating'] = 'Informational' 425 | continue 426 | else: 427 | rpt[record]['report_rating'] = None 428 | logging.info('Leaving get_report Function') 429 | return rpt 430 | 431 | 432 | def banner(): 433 | """Guinevere's banner""" 434 | #Art retrieved from http://www.oocities.org/spunk1111/women.htm 435 | 436 | print """ ,,,_""" 437 | print """ .' `'. ################################################""" 438 | print " / ____ \\# Guinevere v"+__version__+" #" 439 | print " | .`_ _\/# #" 440 | print " / ) a a| # Automated Security Assessment Reporting #" 441 | print " / ( > | ################################################" 442 | print """ ( ) ._ / """ 443 | print " ) _/-.__.'`\\" 444 | print """( .-'`-. \__ )""" 445 | print """ `/ `-./ `. """ 446 | print " | \ \ \\" 447 | print " | \ \ \ \\" 448 | print " |\ `. / / \\" 449 | print "_________________________________________________________________" 450 | 451 | 452 | def int_to_string(i): 453 | """Converts an integer to its spelled out version; Used in reporting narratives""" 454 | 455 | s = { 0: "", #Dictionary of integers to spelled out words 456 | 1: "one", 457 | 2: "two", 458 | 3: "three", 459 | 4: "four", 460 | 5: "five", 461 | 6: "six", 462 | 7: "seven", 463 | 8: "eight", 464 | 9: "nine", 465 | 10: "ten", 466 | 11: "eleven", 467 | 12: "twelve", 468 | 13: "thirteen", 469 | 14: "fourteen", 470 | 15: "fifteen", 471 | 16: "sixteen", 472 | 17: "seventeen", 473 | 18: "eighteen", 474 | 19: "nineteen", 475 | 20: "twenty", 476 | 30: "thirty", 477 | 40: "forty", 478 | 50: "fifty", 479 | 60: "sixty", 480 | 70: "seventy", 481 | 80: "eighty", 482 | 90: "ninety" 483 | } 484 | 485 | #break i into ones, tens, hundreds, thousands and then build string 486 | #ONES 487 | if len(str(i)) is 1 and str(i) is "1": #Spelling for host as opposed to hosts 488 | return s[i] +" (" + str(i) + ") host" 489 | elif len(str(i)) is 1 and str(i) is not "1": #Spelling for single digit hosts 490 | return s[i] +" (" + str(i) + ") hosts" 491 | #TENS 492 | elif len(str(i)) is 2 and str(i).startswith("1") : #To grab spelling for 11 through 19 493 | return s[i]+" (" + str(i) + ") hosts" 494 | elif len(str(i)) is 2 and not str(i).startswith("1") and not str(i).endswith("0"): #To grab spelling for 20 through 99 where the number doesn't end in 0 495 | return s[(i/10)*10]+"-"+s[i -((i/10)*10)] +" (" + str(i) + ") hosts" 496 | elif len(str(i)) is 2 and not str(i).startswith("1") and str(i).endswith("0"): #To grab spelling for 20 through 99 where the number doesn't end in 0 497 | return s[(i/10)*10]+s[i -((i/10)*10)] +" (" + str(i) + ") hosts" 498 | #HUNDREDS 499 | elif len(str(i)) is 3 and "0" not in str(i): #to grab spelling for 100's where the number doesn't have a zero 500 | return s[(i/100)]+"-hundred "+s[((i -((i/100)*100))/10)*10]+"-"+s[i-(((i/100)*100)+((i -((i/100)*100))/10)*10)] +" (" + str(i) + ") hosts" 501 | elif len(str(i)) is 3 and not str(i).endswith("0") and "0" in str(i): #to grab spelling for 100's where the 10s place is 0 502 | return s[(i/100)]+"-hundred"+s[((i -((i/100)*100))/10)*10]+" "+s[i-(((i/100)*100)+((i -((i/100)*100))/10)*10)] +" (" + str(i) + ") hosts" 503 | elif len(str(i)) is 3 and str(i).endswith("0"): #to grab spelling for 100's where the number ends in 0 504 | return s[(i/100)]+"-hundred"+s[((i -((i/100)*100))/10)*10]+s[i-(((i/100)*100)+((i -((i/100)*100))/10)*10)] +" (" + str(i) + ") hosts" 505 | else: 506 | return "ERROR, was not able to return the number of host(s)" 507 | 508 | 509 | def ip_sort(hosts): 510 | """Put the provided list of tuples IP addresses into order""" 511 | 512 | ips = [] 513 | hostnames = [] 514 | for ip in hosts: 515 | if unicode(ip[0].split('.')[0]).isnumeric(): 516 | ips.append(netaddr.IPAddress(ip[0])) # isnumeric only works with Unicode; checking for IP 517 | # elseif: ip[0].isalpha(): # Checking for when a hostname is used instead of an IP 518 | else: 519 | hostnames.append(ip[0]) 520 | 521 | ips = sorted(ips) 522 | sorted_hosts = [] 523 | for i in ips: # Add IPs 524 | sorted_hosts.append(str(i)) 525 | for i in hostnames: # Add Hostnames 526 | sorted_hosts.append(str(i)) 527 | return sorted_hosts 528 | 529 | 530 | def ip_sort_list(hosts): 531 | """Put the provided list IP addresses into order""" 532 | # TODO normalize both ip_sort functions to only use one data type (list or tuple) 533 | ips = [] 534 | hostnames = [] 535 | for ip in hosts: 536 | if unicode(ip.split('.')[0]).isnumeric(): 537 | ips.append(netaddr.IPAddress(ip)) # isnumeric only works with Unicode; checking for IP 538 | else: 539 | hostnames.append(ip) 540 | 541 | ips = sorted(ips) 542 | sorted_hosts = [] 543 | for i in ips: # Add IPs 544 | sorted_hosts.append(str(i)) 545 | for i in hostnames: # Add Hostnames 546 | sorted_hosts.append(str(i)) 547 | return sorted_hosts 548 | 549 | 550 | def generate_hosts_table(file, ass): 551 | """Build a list of assessment interesting hosts; hosts with atleast one TCP or UDP port open.""" 552 | 553 | hosts = {} 554 | file.add_page_break() 555 | logging.info('Entering the generate_hosts_table function') 556 | print "["+note+"]Generating Interesting Hosts Table" 557 | # Build dictionary of host IDs and IPs from gauntlet's 'hosts' table 558 | engagement = db_query("""SELECT value FROM gauntlet_%s.engagement_details WHERE engagement_details.key = 'Engagement Task 1'""" % (ass), ass) 559 | if engagement: 560 | if 'Internal' in engagement[0][0]: 561 | temp = db_query("""SELECT host_id, ip_address, machine_name from hosts""", ass) 562 | else: 563 | temp = db_query("""SELECT host_id, ip_address, fqdn from hosts""", ass) 564 | else: 565 | temp = db_query("""SELECT host_id, ip_address, machine_name from hosts""", ass) 566 | temp2 = db_query("""SELECT host_id, port, protocol from ports""", ass) 567 | for i in temp: 568 | tcp = [] 569 | udp = [] 570 | for j in temp2: 571 | if (j[0] == i[0]) and (j[1] != "0"): 572 | if j[2] == 'tcp': 573 | tcp.append(j[1]) 574 | elif j[2] == 'udp': 575 | udp.append(j[1]) 576 | else: 577 | pass 578 | else: 579 | pass 580 | hosts[i[0]] = {'IP': i[1], 'Name': i[2], 'TCP': tcp, 'UDP': udp} 581 | x = 0 # Number of interesting hosts counter 582 | for host in hosts: 583 | if (len(hosts[host]['TCP']) > 0) or (len(hosts[host]['UDP']) > 0): 584 | x += 1 585 | else: 586 | pass 587 | logging.info("["+info+"]"+str(x) + " Interesting Hosts") 588 | print "\t["+info+"]"+str(x) + " Interesting Hosts" 589 | file.add_heading(str(x) + ' Interesting Host(s) List') 590 | table = file.add_table(rows=1, cols=4) 591 | hdr_cells = table.rows[0].cells 592 | hdr_cells[0].text = 'IP Address' 593 | hdr_cells[1].text = 'Hostname' 594 | hdr_cells[2].text = 'Open TCP Port(s)' 595 | hdr_cells[3].text = 'Open UDP Port(s)' 596 | table.style = 'Medium Grid 1 Accent 1' 597 | 598 | # Build a list of sorted IPs 599 | sorted_hosts = [] 600 | for host in hosts: 601 | sorted_hosts.append(hosts[host]['IP']) 602 | sorted_hosts = ip_sort_list(sorted_hosts) 603 | 604 | for ip in sorted_hosts: 605 | for k in hosts: 606 | if hosts[k]['IP'] == ip: 607 | if (len(hosts[k]['TCP']) > 0) or (len(hosts[k]['UDP']) > 0): 608 | x += 1 609 | row_cells = table.add_row().cells 610 | row_cells[0].text = hosts[k]['IP'] 611 | if len(hosts[k]['Name']) > 0: 612 | row_cells[1].text = hosts[k]['Name'] 613 | else: 614 | row_cells[1].text = "---" 615 | if len(hosts[k]['TCP']) > 0: 616 | row_cells[2].text = str(hosts[k]['TCP']).lstrip('[').rstrip(']').replace("'", "") 617 | else: 618 | row_cells[2].text = "---" 619 | if len(hosts[k]['UDP']) > 0: 620 | row_cells[3].text = str(hosts[k]['UDP']).lstrip('[').rstrip(']').replace("'", "") 621 | else: 622 | row_cells[3].text = "---" 623 | else: 624 | pass 625 | 626 | return file 627 | 628 | 629 | def generate_vuln_list(report_docx, assessment, vuln_dictionary_data): 630 | """Build the bullet list of vulnerabilities used in the executive summary""" 631 | 632 | logging.info("Entering the generate_vuln_list function") 633 | engagement = db_query("""SELECT value FROM gauntlet_%s.engagement_details WHERE engagement_details.key = 'Engagement Task 1'""" % (assessment), assessment) 634 | 635 | if engagement: 636 | report_docx.add_heading(str(engagement[0][0]) + ' NVA/PT') 637 | else: 638 | report_docx.add_heading('NVA/PT') 639 | 640 | def writeBullet(s, h): 641 | n = s.find('[n]') + 3 # Find '[n]' and add three to account for the length of '[n]' 642 | s = s.replace(s[0:n], int_to_string(h)) 643 | s = s.rstrip('\n') 644 | s = s.rstrip('\t') 645 | s = s.rstrip() 646 | s = s.rstrip('.') 647 | s = s[0:1].upper() + s[1:] 648 | report_docx.add_paragraph(s, style='List Bullet') 649 | 650 | logging.info("Writing bullets based on criticality in the generate_vuln_list function") 651 | for i in vuln_dictionary_data: 652 | if args.debug: 653 | print "[" + info + "]%s" % vuln_dictionary_data[i] 654 | 655 | # Check to see if is a multi vuln report item 656 | if len(vuln_dictionary_data[i]['vulns']) > 1: 657 | h = 0 658 | for j in vuln_dictionary_data[i]['vulns']: 659 | h += len(vuln_dictionary_data[i]['vulns'][j]['vuln_hosts']) 660 | if args.sC and vuln_dictionary_data[i]['report_rating'] == 'Critical': 661 | writeBullet(vuln_dictionary_data[i]['report_identification'], h) 662 | elif args.sH and vuln_dictionary_data[i]['report_rating'] == 'High': 663 | writeBullet(vuln_dictionary_data[i]['report_identification'], h) 664 | elif args.sM and vuln_dictionary_data[i]['report_rating'] == 'Medium': 665 | writeBullet(vuln_dictionary_data[i]['report_identification'], h) 666 | elif args.sL and vuln_dictionary_data[i]['report_rating'] == 'Low': 667 | writeBullet(vuln_dictionary_data[i]['report_identification'], h) 668 | elif args.sI and vuln_dictionary_data[i]['report_rating'] == 'Informational': 669 | writeBullet(vuln_dictionary_data[i]['report_identification'], h) 670 | elif vuln_dictionary_data[i]['report_rating'] is None: 671 | print "\t[" + note + "]" + vuln_dictionary_data[i]['report_title'] + " has no affected hosts" 672 | else: 673 | pass 674 | 675 | else: 676 | for j in vuln_dictionary_data[i]['vulns']: 677 | if args.sC and vuln_dictionary_data[i]['report_rating'] == 'Critical': 678 | writeBullet(vuln_dictionary_data[i]['report_identification'], len(vuln_dictionary_data[i]['vulns'][j]['vuln_hosts'])) 679 | elif args.sH and vuln_dictionary_data[i]['report_rating'] == 'High': 680 | writeBullet(vuln_dictionary_data[i]['report_identification'], len(vuln_dictionary_data[i]['vulns'][j]['vuln_hosts'])) 681 | elif args.sM and vuln_dictionary_data[i]['report_rating'] == 'Medium': 682 | writeBullet(vuln_dictionary_data[i]['report_identification'], len(vuln_dictionary_data[i]['vulns'][j]['vuln_hosts'])) 683 | elif args.sL and vuln_dictionary_data[i]['report_rating'] == 'Low': 684 | writeBullet(vuln_dictionary_data[i]['report_identification'], len(vuln_dictionary_data[i]['vulns'][j]['vuln_hosts'])) 685 | elif args.sI and vuln_dictionary_data[i]['report_rating'] == 'Informational': 686 | writeBullet(vuln_dictionary_data[i]['report_identification'], len(vuln_dictionary_data[i]['vulns'][j]['vuln_hosts'])) 687 | elif vuln_dictionary_data[i]['report_rating'] is None: 688 | print "\t[" + note + "]" + vuln_dictionary_data[i]['report_title'] + " has no affected hosts" 689 | else: 690 | pass 691 | 692 | return report_docx 693 | 694 | 695 | def db_query(q, assessment): 696 | """General use function used for querying the assessment database""" 697 | 698 | if assessment == "GauntletData": 699 | assessment2 = assessment 700 | else: 701 | assessment2 = "gauntlet_" + assessment 702 | 703 | db = MySQLdb.connect(host=args.db_host, user=args.db_user, passwd=args.db_pass, port=args.db_port, db=assessment2) 704 | try: 705 | gauntlet=db.cursor() 706 | gauntlet.execute(q) 707 | recordset = gauntlet.fetchall() 708 | gauntlet.close() 709 | return recordset 710 | except: 711 | print "There was an error performing the following query: " 712 | print q 713 | 714 | 715 | def save_report(file, ass): 716 | """Save the generated assessment report""" 717 | out_dir = get_path() 718 | guinevere_file = os.path.join(out_dir, "Guinevere_"+ass+".docx") 719 | file.save(guinevere_file) 720 | print "["+warn+"]Report saved to: " + guinevere_file 721 | raw_input("["+question+"]Press enter to continue...") 722 | main_menu() 723 | 724 | 725 | def retest(): 726 | """Create a report for a retest of an assessment""" 727 | 728 | os.system('clear') 729 | banner() 730 | print "Retrieving available assessments..." 731 | 732 | # Collect data from the original assessment 733 | original_assessment = get_assessment("the original assessment") 734 | banner() 735 | original_crosstable = get_crosstable(original_assessment) 736 | print "["+note+"]Gathering original assessment vulnerability IDs..." 737 | original_vID = assessment_vulns(original_assessment, original_crosstable) 738 | print "["+note+"]Gathering original assessment vulnerability dataset..." 739 | original_vuln = get_vulns(original_vID, original_assessment, original_crosstable) 740 | 741 | # Collect data from the retest 742 | retest_assessment = get_assessment("the retest assessment") 743 | banner() 744 | retest_crosstable = get_crosstable(retest_assessment) 745 | print "["+note+"]Gathering retest vulnerability IDs..." 746 | retest_vID = assessment_vulns(retest_assessment, retest_crosstable) 747 | print "["+note+"]Gathering retest vulnerability dataset..." 748 | retest_vuln = get_vulns(retest_vID, retest_assessment, retest_crosstable) 749 | 750 | # Create the report stub 751 | retest_report = docx.Document() 752 | retest_report.add_heading(original_assessment+' Retest Results') 753 | 754 | retest = {} # Dictionary to hold retest data 755 | 756 | for i in original_vuln: 757 | if original_vuln[i]['vuln_rating'] is not None and original_vuln[i]['vuln_rating'] is not "Informational": 758 | retest[i] = {'vuln_id': i, 'vuln_title': original_vuln[i]['vuln_title'], 'vuln_rating': original_vuln[i]['vuln_rating'], 'total_orig': len(set(original_vuln[i]['vuln_hosts']))} 759 | if i in retest_vuln: 760 | o = set(original_vuln[i]['vuln_hosts']) #Original 761 | r = set(retest_vuln[i]['vuln_hosts']) #Retest 762 | l = o - r #Leftover, fixed hosts 763 | b = [] # List of hosts from the original retest that are found in the retest 764 | 765 | for x in o: # For each host in the original assessment, check to see if it is in the retest assessment 766 | if x in r: 767 | b.append(x) 768 | if len(b) == 0: 769 | print "\t["+note+"]" + original_vuln[i]['vuln_title'] + " - Remediated" 770 | retest[i].update({'status': 'Remediated'}) 771 | elif len(b) == len(o): 772 | print "\t["+warn+"]" + original_vuln[i]['vuln_title'] + " - Not Remediated" 773 | retest[i].update({'status': 'Not Remediated'}) 774 | retest[i].update({'v_hosts': o}) #Hosts Still Vulnerable, contributed by Zach 775 | else: 776 | print "\t["+info+"]" + original_vuln[i]['vuln_title'] + \ 777 | " - Partially Remediated (Still vulnerable: " + str(len(b)) + ")" 778 | retest[i].update({'status': 'Partially Remediated'}) 779 | retest[i].update({'v_hosts': b})#Hosts still vulnerable 780 | retest[i].update({'f_hosts': l}) #Fixed hosts 781 | else: 782 | print "\t["+note+"]" + original_vuln[i]['vuln_title'] + " - Remediated" 783 | retest[i].update({'status': 'Remediated'}) 784 | 785 | # Build Status Table 786 | retest_report.add_heading('Vulnerability Status') 787 | status_table = retest_report.add_table(rows=1, cols=3) 788 | status_table.style = 'Medium Grid 1 Accent 1' 789 | hdr_cells = status_table.rows[0].cells 790 | hdr_cells[0].text = 'Severity' 791 | hdr_cells[1].text = 'Vulnerability' 792 | hdr_cells[2].text = 'Status' 793 | 794 | # Add Critical first 795 | for i in retest: 796 | if retest[i]['vuln_rating'] is 'Critical': 797 | row_cells = status_table.add_row().cells 798 | row_cells[0].text = retest[i]['vuln_rating'] 799 | row_cells[1].text = retest[i]['vuln_title'] 800 | row_cells[2].text = retest[i]['status'] 801 | 802 | # Add High second 803 | for i in retest: 804 | if retest[i]['vuln_rating'] is 'High': 805 | row_cells = status_table.add_row().cells 806 | row_cells[0].text = retest[i]['vuln_rating'] 807 | row_cells[1].text = retest[i]['vuln_title'] 808 | row_cells[2].text = retest[i]['status'] 809 | 810 | # Add Medium third 811 | for i in retest: 812 | if retest[i]['vuln_rating'] is 'Medium': 813 | row_cells = status_table.add_row().cells 814 | row_cells[0].text = retest[i]['vuln_rating'] 815 | row_cells[1].text = retest[i]['vuln_title'] 816 | row_cells[2].text = retest[i]['status'] 817 | 818 | # Add Low last 819 | for i in retest: 820 | if retest[i]['vuln_rating'] is 'Low': 821 | row_cells = status_table.add_row().cells 822 | row_cells[0].text = retest[i]['vuln_rating'] 823 | row_cells[1].text = retest[i]['vuln_title'] 824 | row_cells[2].text = retest[i]['status'] 825 | 826 | # Build Still Vulnerable Hosts Table 827 | retest_report.add_heading('Hosts Still Vulnerable') 828 | vulnerable_table = retest_report.add_table(rows=1, cols=3) 829 | vulnerable_table.style = 'Medium Grid 1 Accent 1' 830 | hdr_cells = vulnerable_table.rows[0].cells 831 | hdr_cells[0].text = 'Severity' 832 | hdr_cells[1].text = 'Vulnerability' 833 | hdr_cells[2].text = 'Hosts' 834 | 835 | #Criticals 836 | for i in retest: 837 | # "and retest[i]['vuln_rating'] is not 'Informational' and len(retest[i]['v_hosts']) > 0" Contriubted by Zach 838 | if 'v_hosts' in retest[i] and retest[i]['vuln_rating'] is not 'Informational' and len(retest[i]['v_hosts']) > 0: 839 | if retest[i]['vuln_rating'] is 'Critical': 840 | row_cells = vulnerable_table.add_row().cells 841 | row_cells[0].text = retest[i]['vuln_rating'] 842 | row_cells[1].text = retest[i]['vuln_title'] 843 | hosts = [] 844 | for h in retest[i]['v_hosts']: 845 | hosts.append(h[0]) 846 | row_cells[2].text = ((str(ip_sort_list(hosts)).replace("'", "")).lstrip("[")).rstrip("]") 847 | 848 | #Highs 849 | for i in retest: 850 | # "and retest[i]['vuln_rating'] is not 'Informational' and len(retest[i]['v_hosts']) > 0" Contriubted by Zach 851 | if 'v_hosts' in retest[i] and retest[i]['vuln_rating'] is not 'Informational' and len(retest[i]['v_hosts']) > 0: 852 | if retest[i]['vuln_rating'] is 'High': 853 | row_cells = vulnerable_table.add_row().cells 854 | row_cells[0].text = retest[i]['vuln_rating'] 855 | row_cells[1].text = retest[i]['vuln_title'] 856 | hosts = [] 857 | for h in retest[i]['v_hosts']: 858 | hosts.append(h[0]) 859 | row_cells[2].text = ((str(ip_sort_list(hosts)).replace("'", "")).lstrip("[")).rstrip("]") 860 | 861 | #Mediums 862 | for i in retest: 863 | # "and retest[i]['vuln_rating'] is not 'Informational' and len(retest[i]['v_hosts']) > 0" Contriubted by Zach 864 | if 'v_hosts' in retest[i] and retest[i]['vuln_rating'] is not 'Informational' and len(retest[i]['v_hosts']) > 0: 865 | if retest[i]['vuln_rating'] is 'Medium': 866 | row_cells = vulnerable_table.add_row().cells 867 | row_cells[0].text = retest[i]['vuln_rating'] 868 | row_cells[1].text = retest[i]['vuln_title'] 869 | hosts = [] 870 | for h in retest[i]['v_hosts']: 871 | hosts.append(h[0]) 872 | row_cells[2].text = ((str(ip_sort_list(hosts)).replace("'", "")).lstrip("[")).rstrip("]") 873 | 874 | #Lows 875 | for i in retest: 876 | # "and retest[i]['vuln_rating'] is not 'Informational' and len(retest[i]['v_hosts']) > 0" Contriubted by Zach 877 | if 'v_hosts' in retest[i] and retest[i]['vuln_rating'] is not 'Informational' and len(retest[i]['v_hosts']) > 0: 878 | if retest[i]['vuln_rating'] is 'Low': 879 | row_cells = vulnerable_table.add_row().cells 880 | row_cells[0].text = retest[i]['vuln_rating'] 881 | row_cells[1].text = retest[i]['vuln_title'] 882 | hosts = [] 883 | for h in retest[i]['v_hosts']: 884 | hosts.append(h[0]) 885 | row_cells[2].text = ((str(ip_sort_list(hosts)).replace("'", "")).lstrip("[")).rstrip("]") 886 | 887 | 888 | # Build stats table 889 | o_total_c = 0 # Original Total Critical 890 | r_total_c = 0 # Retest Total Critical 891 | o_total_h = 0 892 | r_total_h = 0 893 | o_total_m = 0 894 | r_total_m = 0 895 | o_total_l = 0 896 | r_total_l = 0 897 | for i in retest: 898 | # Critical Vulnerabilities 899 | if retest[i]['vuln_rating'] is 'Critical': 900 | o_total_c += retest[i]['total_orig'] 901 | if 'v_hosts' in retest[i]: 902 | r_total_c += len(retest[i]['v_hosts']) 903 | # High Vulnerabilities 904 | if retest[i]['vuln_rating'] is 'High': 905 | o_total_h += retest[i]['total_orig'] 906 | if 'v_hosts' in retest[i]: 907 | r_total_h += len(retest[i]['v_hosts']) 908 | # Medium Vulnerabilities 909 | if retest[i]['vuln_rating'] is 'Medium': 910 | o_total_m += retest[i]['total_orig'] 911 | if 'v_hosts' in retest[i]: 912 | r_total_m += len(retest[i]['v_hosts']) 913 | # Low Vulnerabilities 914 | if retest[i]['vuln_rating'] is 'Low': 915 | o_total_l += retest[i]['total_orig'] 916 | if 'v_hosts' in retest[i]: 917 | r_total_l += len(retest[i]['v_hosts']) 918 | 919 | s = "The original security assessment identified (" + str(o_total_c) + ") critical-severity, (" + str(o_total_h) + ") high-severity, (" + str(o_total_m) + ") medium-severity, and (" + str(o_total_l) + ") low-severity vulnerabilities." 920 | 921 | # Setup Table 922 | retest_report.add_heading('Retest Statistics') 923 | retest_report.add_paragraph(s) 924 | stats_table = retest_report.add_table(rows=1, cols=5) 925 | stats_table.style = 'Medium Grid 1 Accent 1' 926 | hdr_cells = stats_table.rows[0].cells 927 | hdr_cells[0].text = '' 928 | hdr_cells[1].text = 'Critical' 929 | hdr_cells[2].text = 'High' 930 | hdr_cells[3].text = 'Medium' 931 | hdr_cells[4].text = 'Low' 932 | # Original Assessment Numbers 933 | row_cells = stats_table.add_row().cells 934 | row_cells[0].text = 'Original' 935 | row_cells[1].text = str(o_total_c) 936 | row_cells[2].text = str(o_total_h) 937 | row_cells[3].text = str(o_total_m) 938 | row_cells[4].text = str(o_total_l) 939 | 940 | # Retest Assessment Numbers 941 | row_cells = stats_table.add_row().cells 942 | row_cells[0].text = 'Retest' 943 | row_cells[1].text = str(r_total_c) 944 | row_cells[2].text = str(r_total_h) 945 | row_cells[3].text = str(r_total_m) 946 | row_cells[4].text = str(r_total_l) 947 | 948 | save_report(retest_report, retest_assessment) 949 | 950 | 951 | def main_menu(): 952 | """Display the main menu""" 953 | 954 | logging.info('Entered into main_menu function') 955 | i = None 956 | valid_options = {1: generate_assessment_report, 957 | 2: sql_dump, 958 | 3: retest, 959 | 4: patch_gauntlet, 960 | 5: pentest_checklist, 961 | 6: generate_assessment_json, 962 | 7: exit, 963 | } 964 | os.system('clear') 965 | banner() 966 | try: 967 | while i is None: 968 | print "\t\t\t\033[0;0;37mGUINEVERE MAIN MENU\033[0m\n" 969 | print "[1]Generate Assessment Report" 970 | print "[2]Export Assessment" 971 | print "[3]Generate Retest Report" 972 | print "[4]Patch Gauntled Database" 973 | print "[5]Generate Pentest Checklist" 974 | print "[6]Generate Assessment JSON File" 975 | print "[7]Exit" 976 | i = raw_input("\nWhat would you like to do: ") 977 | if int(i) in valid_options: 978 | valid_options[int(i)]() 979 | else: 980 | os.system('clear') 981 | banner() 982 | print "["+warn+"]" + str(i) + " is not a valid option, please try again: " 983 | i = None 984 | except ValueError: 985 | main_menu() 986 | 987 | 988 | def sql_dump(): 989 | """Use mysqldump to export an assessment to a .sql file""" 990 | 991 | import subprocess, time, platform 992 | 993 | #Find the script location and set the mysqldump executable 994 | #Will implement functionality in the future 995 | script_path = os.path.dirname(os.path.realpath(__file__)) 996 | bin_path = os.path.join(script_path, 'bin') 997 | #Determine the operating system 998 | operating_system = platform.platform() 999 | if 'Windows' in operating_system: 1000 | ext = '.exe' 1001 | elif 'Linux' in operating_system: 1002 | ext = '.bin' 1003 | else: 1004 | ext = '.nothing' 1005 | mysqldump = "mysqldump" 1006 | if os.path.isdir(bin_path) and os.path.exists(bin_path+'mysqldump.'+ext): 1007 | mysqldump = bin_path+'mysqldump.'+ext 1008 | 1009 | assessment = get_assessment("the assessment to backup") 1010 | os.system('clear') 1011 | banner() 1012 | output_path = get_path() 1013 | date_time = time.strftime('%m%d%Y-%H%M%S') 1014 | try: 1015 | sql_file = open(os.path.join(output_path, assessment+"_"+date_time+".sql"), "w") 1016 | subprocess.call([mysqldump, "--host="+args.db_host, "-u", args.db_user, "-p"+args.db_pass, "gauntlet_"+assessment], stdout=sql_file) 1017 | #os.system(mysqldump+" --host="+args.db_host+" -u "+args.db_user+" -p"+args.db_pass+" gauntlet_"+assessment+" > "+sql_file) 1018 | print "["+warn+"]SQL file saved to: " + os.path.join(output_path, assessment+"_"+date_time+".sql") 1019 | except OSError: 1020 | print "["+warn+"]mysqldump is likely not in your path, please add it and try again" 1021 | raise 1022 | except: 1023 | raise #Just use for debug 1024 | raw_input("["+question+"]Press enter to continue...") 1025 | main_menu() 1026 | 1027 | 1028 | def get_path(): 1029 | """Prompt the user to enter a directory path""" 1030 | 1031 | output_path = None 1032 | while output_path is None: 1033 | print "["+question+"]Please enter the directory where you would like the file saved?" 1034 | output_path = raw_input() 1035 | if os.path.isdir(os.path.expanduser(output_path)): 1036 | pass 1037 | else: 1038 | os.system('clear') 1039 | banner() 1040 | print "["+warn+"]" + str(output_path) + " is not valid, please try again: " 1041 | output_path = None 1042 | return os.path.expanduser(output_path) 1043 | 1044 | 1045 | def write_single_vul(rpt, report): 1046 | """Write the single vulnerability paragraph""" 1047 | 1048 | cvss = None 1049 | if len(rpt['vulns']) is 1: 1050 | for r in rpt['vulns']: 1051 | cvss = get_cvss(r) 1052 | 1053 | if args.cvss: 1054 | report.add_heading("%s (CVSS: %0.1f - %s)" % (rpt['report_title'], cvss.base_score, rpt['report_rating']), 1055 | level=4) 1056 | else: 1057 | report.add_heading("%s (%s)" % (rpt['report_title'], rpt['report_rating']), level=4) 1058 | 1059 | for i in rpt['vulns']: 1060 | vulnerableHosts = [] 1061 | for j in set(rpt['vulns'][i]['vuln_hosts']): # Build Single Dimensional List 1062 | vulnerableHosts.append(j) 1063 | sortedHosts = ip_sort(vulnerableHosts) # Create a unique & sorted list of hosts (avoids duplicate hosts) 1064 | hosts = [] 1065 | for h in sortedHosts: 1066 | for h2 in vulnerableHosts: 1067 | if h2[0] == h: 1068 | if args.ports: 1069 | if len(h2) == 3: 1070 | if h2[1] != '0' and h2[2] != 'icmp': 1071 | host_info = h2[0] + ":" + h2[1] + "/" + h2[2] 1072 | if host_info not in hosts: 1073 | hosts.append(host_info) 1074 | else: 1075 | if h2[0] not in hosts: 1076 | hosts.append(h2[0]) 1077 | else: 1078 | if h2[0] not in hosts: 1079 | hosts.append(h2[0]) 1080 | else: 1081 | if h2[0] not in hosts: 1082 | hosts.append(h2[0]) 1083 | 1084 | if len(hosts) == 1: # If there is just one host 1085 | p = rpt['report_identification'].replace("[n]", int_to_string(len(hosts))+ ' ('+hosts[0]+')') 1086 | elif len(hosts) == 2: # If there are two hosts 1087 | p = rpt['report_identification'].replace("[n]", int_to_string(len(hosts))+ ' ('+hosts[0]+' and '+hosts[1]+')') 1088 | elif len(hosts) >= 2 and len(hosts) <=5: # If there are more than two but lest than five hosts 1089 | host_list = "" 1090 | for h in hosts: 1091 | if h is hosts[len(hosts)-1]: # Check to see if this is the last item in the list 1092 | host_list += "and " + h + ") " 1093 | else: 1094 | host_list += h + ", " 1095 | p = rpt['report_identification'].replace("[n]", int_to_string(len(hosts)) + ' ('+host_list) 1096 | elif len(hosts) >= 6: # If there are six or more hosts 1097 | p = rpt['report_identification'].replace("[n]", int_to_string(len(hosts)) + '(refer to TABLE X)') 1098 | 1099 | if p.endswith(" ") or rpt['report_explanation'].startswith(" "): 1100 | p += rpt['report_explanation'] 1101 | else: 1102 | p += (" " + rpt['report_explanation']) 1103 | if p.endswith(" ") or rpt['report_impact'].startswith(" "): 1104 | p += rpt['report_impact'] 1105 | else: 1106 | p += (" " + rpt['report_impact']) 1107 | p = report.add_paragraph(p, style='Normal') 1108 | p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY 1109 | p = report.add_paragraph(rpt['report_recommendation'], style='Normal') 1110 | p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY 1111 | 1112 | if args.debug: 1113 | print debug + "Vulnerable Hosts: %s" % hosts 1114 | raw_input(debug + "Press any key to continue") 1115 | if len(hosts) >= 6: # Draw the table 1116 | c = 4 # number of desired columns 1117 | r = int(math.ceil((len(hosts) / float(4)))) # Determine number of rows for table using a max of 4 columns 1118 | hosts_table = report.add_table(rows=r, cols=c) 1119 | hosts_table.style = 'Medium Grid 1 Accent 1' 1120 | z = 0 # number of hosts 1121 | x = 0 # row indices 1122 | y = 0 # column indices 1123 | while z < len(hosts): 1124 | if (y / float(c)) == 1: # Determine if we need to start putting data on a new row 1125 | y = 0 # reset column indices since max number of columns reached 1126 | x += 1 1127 | hosts_table.cell(x, y).text = hosts[z] 1128 | z += 1 1129 | y += 1 # Add one to up the column data is put in 1130 | if len(hosts)/float(c) != 1.000: # Add "---" for empty spots in table 1131 | d = c * (x+1) 1132 | while d > len(hosts): 1133 | hosts_table.cell(x, y).text = "---" 1134 | d -= 1 1135 | y += 1 1136 | 1137 | return report 1138 | 1139 | 1140 | def write_multi_vul(rpt, report): 1141 | """Write report data for grouped or multi vulnerabilities""" 1142 | 1143 | total_hosts = 0 1144 | for i in rpt['vulns']: 1145 | if args.sC and rpt['vulns'][i]['vuln_rating'] == 'Critical': 1146 | total_hosts += len(rpt['vulns'][i]['vuln_hosts']) 1147 | elif args.sH and rpt['vulns'][i]['vuln_rating'] == 'High': 1148 | total_hosts += len(rpt['vulns'][i]['vuln_hosts']) 1149 | elif args.sM and rpt['vulns'][i]['vuln_rating'] == 'Medium': 1150 | total_hosts += len(rpt['vulns'][i]['vuln_hosts']) 1151 | elif args.sL and rpt['vulns'][i]['vuln_rating'] == 'Low': 1152 | total_hosts += len(rpt['vulns'][i]['vuln_hosts']) 1153 | elif args.sI and rpt['vulns'][i]['vuln_rating'] == 'Informational': 1154 | total_hosts += len(rpt['vulns'][i]['vuln_hosts']) 1155 | 1156 | report.add_heading(rpt['report_title'] + " (" + rpt['report_rating']+")", level=4) 1157 | p = rpt['report_identification'].replace("[n]", int_to_string(total_hosts)) 1158 | p = p.rstrip('\n') 1159 | p = p.rstrip('\t') 1160 | p = p.rstrip() 1161 | 1162 | if p.endswith(" ") or rpt['report_explanation'].startswith(" "): 1163 | p += rpt['report_explanation'] 1164 | else: 1165 | p += (" " + rpt['report_explanation']) 1166 | if p.endswith(" ") or rpt['report_impact'].startswith(" "): 1167 | p += rpt['report_impact'] 1168 | else: 1169 | p += (" " + rpt['report_impact']) 1170 | 1171 | p = report.add_paragraph(p, style='Normal') 1172 | p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY 1173 | p = report.add_paragraph(rpt['report_recommendation'], style='Normal') 1174 | p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY 1175 | 1176 | table = None 1177 | hdr_cells = None 1178 | 1179 | if args.cvss: 1180 | table = report.add_table(rows=1, cols=4) 1181 | hdr_cells = table.rows[0].cells 1182 | hdr_cells[0].text = 'Severity' 1183 | hdr_cells[1].text = 'CVSS' 1184 | hdr_cells[2].text = 'Vulnerability' 1185 | hdr_cells[3].text = 'Affected Host(s)' 1186 | else: 1187 | table = report.add_table(rows=1, cols=3) 1188 | hdr_cells = table.rows[0].cells 1189 | hdr_cells[0].text = 'Severity' 1190 | hdr_cells[1].text = 'Vulnerability' 1191 | hdr_cells[2].text = 'Affected Host(s)' 1192 | 1193 | table.style = 'Medium Grid 1 Accent 1' 1194 | 1195 | def writeRow(r, t, h, c=None): 1196 | row_cells = table.add_row().cells 1197 | if c is not None: 1198 | row_cells[0].text = r 1199 | row_cells[1].text = c 1200 | row_cells[2].text = t 1201 | row_cells[3].text = h 1202 | else: 1203 | row_cells[0].text = r 1204 | row_cells[1].text = t 1205 | row_cells[2].text = h 1206 | 1207 | for i in rpt['vulns']: 1208 | if args.sC and rpt['vulns'][i]['vuln_rating'] == 'Critical': 1209 | cvss = get_cvss(i) 1210 | if args.cvss: 1211 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1212 | str(len(rpt['vulns'][i]['vuln_hosts'])), "%0.1f" % cvss.base_score) 1213 | else: 1214 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1215 | str(len(rpt['vulns'][i]['vuln_hosts']))) 1216 | for i in rpt['vulns']: 1217 | if args.sH and rpt['vulns'][i]['vuln_rating'] == 'High': 1218 | cvss = get_cvss(i) 1219 | if args.cvss: 1220 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1221 | str(len(rpt['vulns'][i]['vuln_hosts'])), "%0.1f" % cvss.base_score) 1222 | else: 1223 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1224 | str(len(rpt['vulns'][i]['vuln_hosts']))) 1225 | for i in rpt['vulns']: 1226 | if args.sM and rpt['vulns'][i]['vuln_rating'] == 'Medium': 1227 | cvss = get_cvss(i) 1228 | if args.cvss: 1229 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1230 | str(len(rpt['vulns'][i]['vuln_hosts'])), "%0.1f" % cvss.base_score) 1231 | else: 1232 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1233 | str(len(rpt['vulns'][i]['vuln_hosts']))) 1234 | for i in rpt['vulns']: 1235 | if args.sL and rpt['vulns'][i]['vuln_rating'] == 'Low': 1236 | cvss = get_cvss(i) 1237 | if args.cvss: 1238 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1239 | str(len(rpt['vulns'][i]['vuln_hosts'])), "%0.1f" % cvss.base_score) 1240 | else: 1241 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1242 | str(len(rpt['vulns'][i]['vuln_hosts']))) 1243 | for i in rpt['vulns']: 1244 | if args.sI and rpt['vulns'][i]['vuln_rating'] == 'Informational': 1245 | cvss = get_cvss(i) 1246 | if args.cvss: 1247 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1248 | str(len(rpt['vulns'][i]['vuln_hosts'])), "%0.1f" % cvss.base_score) 1249 | else: 1250 | writeRow(rpt['vulns'][i]['vuln_rating'], rpt['vulns'][i]['vuln_title'], 1251 | str(len(rpt['vulns'][i]['vuln_hosts']))) 1252 | 1253 | return report 1254 | 1255 | 1256 | def write_all_vuln(vuln, the_Report): 1257 | 1258 | print "["+note+"]Writing list of all vulnerabilities to the report: " 1259 | the_Report.add_page_break() 1260 | the_Report.add_heading("List of Assessment Vulnerabilities", 1) 1261 | for i in vuln: 1262 | if args.sC and vuln[i]['vuln_rating'] is 'Critical': 1263 | print "\t["+info+"]"+ vuln[i]['vuln_title'], "(" + vuln[i]['vuln_rating'] + ")" 1264 | the_Report.add_heading(vuln[i]['vuln_title'] + " (" + vuln[i]['vuln_rating'] + ")", 3) 1265 | if args.all_verb: 1266 | the_Report.add_paragraph(vuln[i]['vuln_desc'], style='Body Text') 1267 | the_Report.add_paragraph(vuln[i]['vuln_sol'], style='Body Text') 1268 | for i in vuln: 1269 | if args.sH and vuln[i]['vuln_rating'] is 'High': 1270 | print "\t["+info+"]"+ vuln[i]['vuln_title'], "(" + vuln[i]['vuln_rating'] + ")" 1271 | the_Report.add_heading(vuln[i]['vuln_title'] + " (" + vuln[i]['vuln_rating'] + ")", 3) 1272 | if args.all_verb: 1273 | the_Report.add_paragraph(vuln[i]['vuln_desc'], style='Body Text') 1274 | the_Report.add_paragraph(vuln[i]['vuln_sol'], style='Body Text') 1275 | for i in vuln: 1276 | if args.sM and vuln[i]['vuln_rating'] is 'Medium': 1277 | print "\t["+info+"]"+ vuln[i]['vuln_title'], "(" + vuln[i]['vuln_rating'] + ")" 1278 | the_Report.add_heading(vuln[i]['vuln_title'] + " (" + vuln[i]['vuln_rating'] + ")", 3) 1279 | if args.all_verb: 1280 | the_Report.add_paragraph(vuln[i]['vuln_desc'], style='Body Text') 1281 | the_Report.add_paragraph(vuln[i]['vuln_sol'], style='Body Text') 1282 | for i in vuln: 1283 | if args.sL and vuln[i]['vuln_rating'] is 'Low': 1284 | print "\t["+info+"]"+ vuln[i]['vuln_title'], "(" + vuln[i]['vuln_rating'] + ")" 1285 | the_Report.add_heading(vuln[i]['vuln_title'] + " (" + vuln[i]['vuln_rating'] + ")", 3) 1286 | if args.all_verb: 1287 | the_Report.add_paragraph(vuln[i]['vuln_desc'], style='Body Text') 1288 | the_Report.add_paragraph(vuln[i]['vuln_sol'], style='Body Text') 1289 | for i in vuln: 1290 | if args.sI and vuln[i]['vuln_rating'] is 'Informational': 1291 | print "\t["+info+"]"+ vuln[i]['vuln_title'], "(" + vuln[i]['vuln_rating'] + ")" 1292 | the_Report.add_heading(vuln[i]['vuln_title'] + " (" + vuln[i]['vuln_rating'] + ")", 3) 1293 | if args.all_verb: 1294 | the_Report.add_paragraph(vuln[i]['vuln_desc'], style='Body Text') 1295 | the_Report.add_paragraph(vuln[i]['vuln_sol'], style='Body Text') 1296 | #if vuln[i]['vuln_report_id'] is None and (((vuln[i]['vuln_rating'] is "Critical") and args.sC) or ((vuln[i]['vuln_rating'] is "High") and args.sH) or ((vuln[i]['vuln_rating'] is "Medium") and args.sM) or ((vuln[i]['vuln_rating'] is "Low") and args.sL) or ((vuln[i]['vuln_rating'] is "Informational") and args.sI)): 1297 | #print "\t["+info+"]"+ vuln[i]['vuln_title'], "(" + vuln[i]['vuln_rating'] + ")" 1298 | #the_Report.add_heading(vuln[i]['vuln_title'] + " (" + vuln[i]['vuln_rating'] + ")", 3) 1299 | #if args.all_verb: 1300 | #the_Report.add_paragraph(vuln[i]['vuln_desc'], style='BodyText') 1301 | #the_Report.add_paragraph(vuln[i]['vuln_sol'], style='BodyText') 1302 | 1303 | return the_Report 1304 | 1305 | 1306 | def generate_assessment_report(): 1307 | """The main function for automatically generating an assessment report""" 1308 | 1309 | logging.info('Entering the generate_assessment_report function') 1310 | os.system('clear') 1311 | banner() 1312 | print "Retrieving available assessments..." 1313 | 1314 | # Database containing the options of crosstables 1315 | assessment = get_assessment("the assessment to create a report for") 1316 | banner() 1317 | 1318 | # Choose crosstable from the options listed from get_assessment() 1319 | crosstable = get_crosstable(assessment) 1320 | 1321 | vulnerability_ID_list = assessment_vulns(assessment, crosstable) 1322 | os.system('clear') 1323 | banner() 1324 | print "["+note+"]Building list of found vulnerabilities for " + assessment + " Crosstable " + crosstable + "..." 1325 | vuln_ID_to_info_mapping = get_vulns(vulnerability_ID_list, assessment, crosstable) 1326 | 1327 | ## FOLLOW vuln_id_to_info_mapping - Contains the severity rating, needs to be reset after each crosstable 1328 | 1329 | print "["+note+"]Generating report for the following vulnerabilities:" 1330 | 1331 | # Unique identifier for reporting record, allows access to verbage 1332 | report_record_ID_set = assessment_report(vuln_ID_to_info_mapping) 1333 | 1334 | # Returns huge dictionary of vulnerabilities and associated data 1335 | assessment_db = get_report(report_record_ID_set, vuln_ID_to_info_mapping) 1336 | 1337 | # Make the word document 1338 | the_Report = docx.Document() 1339 | the_Report.add_heading(assessment, 1) 1340 | the_Report = generate_vuln_list(the_Report, assessment, assessment_db) 1341 | 1342 | if ((len(assessment_db) is 0) and args.all_vulns is False): 1343 | exit("["+warn+"]Nothing to report on, quitting...") 1344 | 1345 | #################################### 1346 | # Write the report in severity order 1347 | #################################### 1348 | 1349 | logging.info("Writing the critical vulnerability narratives to the report") 1350 | 1351 | for i in assessment_db: 1352 | if assessment_db[i]['report_rating'] == 'Critical' and args.sC: 1353 | 1354 | # Grouped Vulnerability Write-up 1355 | if len(assessment_db[i]['vulns']) > 1: 1356 | print '\t['+info+']Multi finding: ', assessment_db[i]['report_title'] 1357 | logging.info('['+info+']Multi finding: ' + assessment_db[i]['report_title']) 1358 | the_report = write_multi_vul(assessment_db[i], the_Report) 1359 | 1360 | # Single Vulnerability Write-up 1361 | elif assessment_db[i]['report_rating'] is not None: 1362 | logging.info("["+info+"]" + assessment_db[i]['report_title'] + 1363 | "(" + assessment_db[i]['report_rating'] + ")") 1364 | print "\t["+info+"]" + assessment_db[i]['report_title'] + " \033[0;0;31m(" + \ 1365 | assessment_db[i]['report_rating'] + ")\033[0m" 1366 | the_Report = write_single_vul(assessment_db[i], the_Report) 1367 | 1368 | logging.info("Writing the high vulnerability narratives to the report") 1369 | 1370 | for i in assessment_db: 1371 | if assessment_db[i]['report_rating'] == 'High' and args.sH: 1372 | if len(assessment_db[i]['vulns']) > 1: 1373 | logging.info('['+info+']Multi finding: ' + assessment_db[i]['report_title']) 1374 | print '\t['+info+']Multi finding: ', assessment_db[i]['report_title'] 1375 | the_report = write_multi_vul(assessment_db[i], the_Report) 1376 | elif assessment_db[i]['report_rating'] is not None: 1377 | logging.info("["+info+"]" + assessment_db[i]['report_title'] + 1378 | "(" + assessment_db[i]['report_rating'] + ")") 1379 | print "\t["+info+"]" + assessment_db[i]['report_title'] + " \033[0;0;35m(" + \ 1380 | assessment_db[i]['report_rating'] + ")\033[0m" 1381 | the_Report = write_single_vul(assessment_db[i], the_Report) 1382 | 1383 | logging.info("Writing the medium vulnerability narratives to the report") 1384 | 1385 | for i in assessment_db: 1386 | if assessment_db[i]['report_rating'] == 'Medium' and args.sM: 1387 | if len(assessment_db[i]['vulns']) > 1: 1388 | logging.info('['+info+']Multi finding: ' + assessment_db[i]['report_title']) 1389 | print '\t['+info+']Multi finding: ', assessment_db[i]['report_title'] 1390 | the_report = write_multi_vul(assessment_db[i], the_Report) 1391 | elif assessment_db[i]['report_rating'] is not None: 1392 | logging.info("["+info+"]" + assessment_db[i]['report_title'] + 1393 | "(" + assessment_db[i]['report_rating'] + ")") 1394 | print "\t["+info+"]" + assessment_db[i]['report_title'] + " \033[0;0;33m(" + \ 1395 | assessment_db[i]['report_rating'] + ")\033[0m" 1396 | the_Report = write_single_vul(assessment_db[i], the_Report) 1397 | 1398 | logging.info("Writing the low vulnerability narratives to the report") 1399 | 1400 | for i in assessment_db: 1401 | if assessment_db[i]['report_rating'] == 'Low' and args.sL: 1402 | if len(assessment_db[i]['vulns']) > 1: 1403 | logging.info('['+info+']Multi finding: ' + assessment_db[i]['report_title']) 1404 | print '\t['+info+']Multi finding: ', assessment_db[i]['report_title'] 1405 | the_report = write_multi_vul(assessment_db[i], the_Report) 1406 | elif assessment_db[i]['report_rating'] is not None: 1407 | logging.info("["+info+"]" + assessment_db[i]['report_title'] + 1408 | "(" + assessment_db[i]['report_rating'] + ")") 1409 | print "\t["+info+"]" + assessment_db[i]['report_title'] + " \033[0;0;34m(" + \ 1410 | assessment_db[i]['report_rating'] + ")\033[0m" 1411 | the_Report = write_single_vul(assessment_db[i], the_Report) 1412 | 1413 | logging.info("Writing the informational vulnerability narratives to the report") 1414 | 1415 | for i in assessment_db: 1416 | if assessment_db[i]['report_rating'] == 'Informational' and args.sI: 1417 | if len(assessment_db[i]['vulns']) > 1: 1418 | logging.info('['+info+']Multi finding: ' + assessment_db[i]['report_title']) 1419 | print '\t['+info+']Multi finding: ', assessment_db[i]['report_title'] 1420 | the_report = write_multi_vul(assessment_db[i], the_Report) 1421 | elif assessment_db[i]['report_rating'] is not None: 1422 | logging.info("["+info+"]" + assessment_db[i]['report_title'] + 1423 | "(" + assessment_db[i]['report_rating'] + ")") 1424 | print "\t["+info+"]" + assessment_db[i]['report_title'] + " \033[0;0;37m(" + \ 1425 | assessment_db[i]['report_rating'] + ")\033[0m" 1426 | the_Report = write_single_vul(assessment_db[i], the_Report) 1427 | 1428 | if args.all_vulns: 1429 | the_Report = write_all_vuln(vuln_ID_to_info_mapping, the_Report) 1430 | the_Report = generate_hosts_table(the_Report, assessment) 1431 | save_report(the_Report, assessment) 1432 | 1433 | 1434 | def generate_assessment_json(): 1435 | """Generate a JSON object of the aggregated assessment information""" 1436 | 1437 | logging.info('Entering the generate_assessment_json function') 1438 | os.system('clear') 1439 | banner() 1440 | print "Retrieving available assessments..." 1441 | assessment = get_assessment("the assessment to create a JSON object for") 1442 | banner() 1443 | crosstable = get_crosstable(assessment) 1444 | vID = assessment_vulns(assessment, crosstable) 1445 | os.system('clear') 1446 | banner() 1447 | print "["+note+"]Building JSON object for " + assessment + " Crosstable " + crosstable + "..." 1448 | vuln = get_vulns(vID, assessment, crosstable) 1449 | rID = assessment_report(vuln) 1450 | assessment_db = get_report(rID, vuln) 1451 | engagment_details = gather_assessment_details(assessment) 1452 | json_dict = {'engagment_details': engagment_details, 'report': assessment_db} 1453 | json_object = json.dumps(json_dict) 1454 | out_dir = get_path() 1455 | json_file = os.path.join(out_dir, "Guinevere_" + assessment + "_" + crosstable + ".json") 1456 | with open(json_file, "w") as j: 1457 | j.write(json_object) 1458 | print "["+warn+"]Assessment JSON object saved to: " + json_file 1459 | raw_input("["+question+"]Press enter to continue...") 1460 | main_menu() 1461 | 1462 | 1463 | def gather_assessment_details(assessment): 1464 | """Gather assessment details from Gauntlet and create a dictionary""" 1465 | 1466 | logging.info('Entering the gather_assessment_details function') 1467 | engagement = db_query("""SELECT value FROM gauntlet_%s.engagement_details WHERE 1468 | engagement_details.key = 'Engagement Task 1'""" % (assessment), assessment)[0][0] 1469 | start_date = db_query("""SELECT value FROM gauntlet_%s.engagement_details WHERE 1470 | engagement_details.key = 'Start Date'""" % (assessment), assessment)[0][0] 1471 | end_date = db_query("""SELECT value FROM gauntlet_%s.engagement_details WHERE 1472 | engagement_details.key = 'End Date'""" % (assessment), assessment)[0][0] 1473 | analyst = db_query("""SELECT value FROM gauntlet_%s.engagement_details WHERE 1474 | engagement_details.key = 'Analyst 1'""" % (assessment), assessment)[0][0] 1475 | 1476 | i = {'engagment_type': engagement, 'start_date': start_date, 'stop_date': end_date, 'analyst': analyst} 1477 | return i 1478 | 1479 | 1480 | def patch_gauntlet(): 1481 | print "Nothing to test right now" 1482 | db = MySQLdb.connect(host=args.db_host, user=args.db_user, passwd=args.db_pass, port=args.db_port, db='GauntletData') 1483 | 1484 | create_table = """ 1485 | CREATE TABLE report ( 1486 | report_id integer NOT NULL AUTO_INCREMENT, 1487 | title character varying(255) NOT NULL DEFAULT '', 1488 | identification blob, 1489 | explanation blob, 1490 | impact blob, 1491 | recommendation blob, 1492 | status ENUM('NEW','MODIFIED','ACCEPTED','MARKED','DELETED') NOT NULL, 1493 | owner character varying(255) NOT NULL DEFAULT '', 1494 | PRIMARY KEY (report_id) 1495 | );""" 1496 | mod_report = """ALTER TABLE report AUTO_INCREMENT = 50000;""" 1497 | mod_vuln_1 = """ALTER TABLE vulns ADD report_id int;""" 1498 | mod_vuln_2 = """ALTER TABLE vulns ADD FOREIGN KEY (report_id) REFERENCES report(report_id);""" 1499 | 1500 | os.system('clear') 1501 | banner() 1502 | print """["""+warn+"""]Please make sure you have previously selected "(Re-)Initialize Server" in Gauntlet.""" 1503 | raw_input("["+question+"]Press enter to continue...") 1504 | try: 1505 | gauntlet = db.cursor() 1506 | gauntlet.execute(create_table) 1507 | gauntlet.execute(mod_report) 1508 | gauntlet.execute(mod_vuln_1) 1509 | gauntlet.execute(mod_vuln_2) 1510 | gauntlet.close() 1511 | except: 1512 | print "\n["+warn+"]Please report this error to " + __maintainer__ + " by email at: " + __email__ 1513 | raise 1514 | main_menu() 1515 | 1516 | print "["+note+"]You can now upload a new master dataset to Gauntlet" 1517 | main_menu() 1518 | 1519 | 1520 | def pentest_checklist(): 1521 | """Generate a pentest checklist to be used for an assessment""" 1522 | 1523 | logging.info('Entered into pentest_checklist function') 1524 | 1525 | def build_html(): 1526 | """Generate HTML pentest checklist""" 1527 | 1528 | html_part = '' 1529 | 1530 | host_part = '\n\n\t\n\t\t\n\t\t\n\t' 1532 | port_part = '\n\t\n\t\t\n\t\t\n\t' 1534 | tool_part = '\n\t\n\t\t\n\t' 1536 | tool_output_part = '\n\t\n\t\t\n\t' 1538 | note_part = '\n\t\n\t\t\n\t' 1539 | 1540 | for host in hosts2: 1541 | # Build table Header for each host 1542 | html_part += host_part 1543 | if hosts2[host]['fqdn'][0] is not "": 1544 | html_part = html_part.replace('$host-name', hosts2[host]['ipv4'] + " - " + hosts2[host]['fqdn'][0]) 1545 | else: 1546 | html_part = html_part.replace('$host-name', hosts2[host]['ipv4']) 1547 | 1548 | # Build Port Rows 1549 | for port_id in hosts2[host]['ports']: 1550 | html_part += port_part 1551 | 1552 | # Build Vulnerability Rows 1553 | if 'vulns' in hosts2[host]['ports'][port_id].keys() and args.tool_output: 1554 | for vuln_id in hosts2[host]['ports'][port_id]['vulns']: 1555 | html_part += tool_part 1556 | html_part += tool_output_part 1557 | tool_name = hosts2[host]['ports'][port_id]['vulns'][vuln_id]['tool'] 1558 | html_part = html_part.replace('$tool-id', tool_name + "-" + vuln_id) 1559 | html_part = html_part.replace('$tool-name', tool_name) 1560 | html_part = html_part.replace('$vuln-title', 1561 | str(hosts2[host]['ports'][port_id]['vulns'][vuln_id]['title'])) 1562 | html_part = html_part.replace('$tool-output', 1563 | (hosts2[host]['ports'][port_id]['vulns'][vuln_id]['output']) 1564 | .replace('&', '&') 1565 | .replace('<', '<') 1566 | .replace('>', '>')) 1567 | 1568 | html_part = html_part.replace('$port-id', hosts2[host]['ports'][port_id]['port'] + "-" + 1569 | hosts2[host]['ports'][port_id]['type']) 1570 | html_part = html_part.replace('$port-text', hosts2[host]['ports'][port_id]['port'] + "/" + 1571 | hosts2[host]['ports'][port_id]['type'] + "/" + 1572 | str(hosts2[host]['ports'][port_id]['service'])) 1573 | 1574 | html_part = html_part.replace('$host-id', hosts2[host]['ipv4']) 1575 | html_part += (note_part * 3) 1576 | html_part += '\n
$host-name
$port-text
$vuln-title
' \ 1537 | '
$tool-output
' 1577 | 1578 | 1579 | 1580 | out_dir = get_path() 1581 | checklist = os.path.join(out_dir, "Guinevere_"+assessment+"_checklist.html") 1582 | html_file = open(checklist, 'w') 1583 | # Build HTML File 1584 | css = open(os.path.join(G_root, 'static', 'G-Checklist', 'G-Checklist.css'), 'r').read() 1585 | html = open(os.path.join(G_root, 'static', 'G-Checklist', 'G-Checklist_Template.html'), 'r').read() 1586 | html = html.replace('$ASSESSMENT', assessment) 1587 | html = html.replace('$CSS', css) 1588 | html = html.replace('$DATA', html_part) 1589 | html_file.write(html) 1590 | html_file.close() 1591 | print "["+warn+"]Report saved to: " + checklist 1592 | 1593 | logging.info('Entered into build_html function within the pentest_checklist function') 1594 | assessment = get_assessment("the assessment to create a pentest checklist for") 1595 | banner() 1596 | print "["+note+"]Building Pentest Checklist for " + assessment + "..." 1597 | # hosts1 holds the record set returned from the SQL query 1598 | hosts1 = db_query("""SELECT hosts.host_id, ip_address, fqdn, port_id, port, protocol, name """ 1599 | """FROM hosts INNER JOIN ports ON hosts.host_id=ports.host_id WHERE port IS NOT NULL""", 1600 | assessment) 1601 | host_ids = [] # A list to store retrieved host IDs 1602 | hosts2 = {} # A dictionary that holds the data used to print the checklist 1603 | 1604 | for row in hosts1: 1605 | if row[0] not in host_ids: 1606 | host_ids.append(row[0]) 1607 | 1608 | for host_id in host_ids: 1609 | hosts2[host_id] = {'ports': {}} # Create a key in the hosts2 dictionary from the host id 1610 | for row in hosts1: 1611 | if host_id == row[0]: 1612 | hosts2[host_id].update({'ipv4': row[1]}) 1613 | if 'fqdn' in hosts2[host_id].keys(): 1614 | if row[2] not in hosts2[host_id]['fqdn']: 1615 | hosts2[host_id]['fqdn'].append(row[2]) 1616 | else: 1617 | hosts2[host_id].update({'fqdn': [row[2]]}) 1618 | if row[4] > 0: 1619 | hosts2[host_id]['ports'][row[3]] = {} 1620 | hosts2[host_id]['ports'][row[3]].update({'port': row[4]}) 1621 | hosts2[host_id]['ports'][row[3]].update({'type': row[5]}) 1622 | hosts2[host_id]['ports'][row[3]].update({'service': row[6]}) 1623 | 1624 | #This check for the "tool_title" is here for backward compatability, remove when version are higher 1625 | columns = db_query("""SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 1626 | 'gauntlet_%s' AND TABLE_NAME = 'vulnerabilities'""" % (assessment), assessment) 1627 | if any('tool_title' in column for column in columns): 1628 | tool_data = db_query("""SELECT vuln_id, gnaat_id, tool, txt, tool_title FROM vulnerabilities 1629 | WHERE host_id=%s and port_id=%s""" % (str(host_id), str(row[3])), assessment) 1630 | hosts2[host_id]['ports'][row[3]]['vulns'] = {} 1631 | for tool in tool_data: 1632 | hosts2[host_id]['ports'][row[3]]['vulns'].update({tool[0]: {'gnaat_id': tool[1], 1633 | 'tool': tool[2], 1634 | 'title': tool[4], 1635 | 'output': tool[3], 1636 | 'vuln_id': tool[0]}}) 1637 | else: 1638 | tool_data = db_query("""SELECT vuln_id, gnaat_id, tool, txt FROM vulnerabilities 1639 | WHERE host_id= %s and port_id= %s""" % (str(host_id), str(row[3])), assessment) 1640 | if tool_data: 1641 | hosts2[host_id]['ports'][row[3]]['vulns'] = {} 1642 | for tool in tool_data: 1643 | if tool[1] != "": 1644 | title = db_query("SELECT title from vulns where gnaat_id=%s" % (tool[1]), 'GauntletData') 1645 | else: 1646 | title = None 1647 | if title is not None and len(title) > 0: 1648 | hosts2[host_id]['ports'][row[3]]['vulns'].update({tool[0]: {'gnaat_id': tool[1], 1649 | 'tool': tool[2], 1650 | 'title': title[0][0], 1651 | 'output': tool[3], 1652 | 'vuln_id': tool[0]}}) 1653 | else: 1654 | hosts2[host_id]['ports'][row[3]]['vulns'].update({tool[0]: {'gnaat_id': tool[1], 1655 | 'tool': tool[2], 1656 | 'title': None, 1657 | 'output': tool[3], 1658 | 'vuln_id': tool[0]}}) 1659 | 1660 | build_html() 1661 | raw_input("["+note+"]Press enter to return to the main menu") 1662 | 1663 | main_menu() 1664 | 1665 | 1666 | def get_cvss(gnaat_id, version=2): 1667 | """Get a CVSS object using the gnaat_id from the vulns table in the GauntletData database""" 1668 | 1669 | logging.info('Entered into get_cvss function') 1670 | 1671 | c = db_query("SELECT DISTINCT `cvss_access_vector`, `cvss_access_complexity`, `cvss_authentication`, " 1672 | "`cvss_confidentiality_impact`, `cvss_integrity_impact`, `cvss_availability_impact`, " 1673 | "`cvss_exploitability`, `cvss_remediation_level`, `cvss_report_confidence` FROM " 1674 | "vulns WHERE gnaat_id='%s';" % gnaat_id, "GauntletData") 1675 | 1676 | AV = None # Access Vector 1677 | AC = None # Access Complexity 1678 | Au = None # Authentication 1679 | C = None # Confidentiality 1680 | I = None # Integrity 1681 | A = None # Availability 1682 | E = None # Exploitability 1683 | RL = None # Remediation Level 1684 | RC = None # Report Confidence 1685 | 1686 | # Access Vector 1687 | if c[0][0] == 'remote': 1688 | AV = 'A' 1689 | elif c[0][0] == 'local': 1690 | AV = 'L' 1691 | elif c[0][0] == 'network': 1692 | AV = 'N' 1693 | 1694 | # Access Complexity 1695 | if c[0][1] == 'low': 1696 | AC = 'L' 1697 | elif c[0][1] == 'medium': 1698 | AC = 'M' 1699 | elif c[0][1] == 'high': 1700 | AC = 'H' 1701 | 1702 | # Authentication 1703 | if c[0][2] == 'not_required': 1704 | Au = 'N' 1705 | elif c[0][2] == 'required': 1706 | Au = 'S' 1707 | elif c[0][2] == 'multiple': 1708 | Au = 'M' 1709 | 1710 | # Confidentiality 1711 | if c[0][3] == 'none': 1712 | C = 'N' 1713 | elif c[0][3] == 'complete': 1714 | C = 'C' 1715 | elif c[0][3] == 'partial': 1716 | C = 'P' 1717 | 1718 | # Integrity 1719 | if c[0][4] == 'none': 1720 | I = 'N' 1721 | elif c[0][4] == 'complete': 1722 | I = 'C' 1723 | elif c[0][4] == 'partial': 1724 | I = 'P' 1725 | 1726 | # Availability 1727 | if c[0][5] == 'none': 1728 | A = 'N' 1729 | elif c[0][5] == 'complete': 1730 | A = 'C' 1731 | elif c[0][5] == 'partial': 1732 | A = 'P' 1733 | 1734 | # Exploitability 1735 | if c[0][6] == 'not_defined': 1736 | E = 'ND' 1737 | elif c[0][6] == 'unproven': 1738 | E = 'U' 1739 | elif c[0][6] == 'proof_of_concept': 1740 | E = 'POC' 1741 | elif c[0][6] == 'functional': 1742 | E = 'F' 1743 | elif c[0][6] == 'high': 1744 | E = 'H' 1745 | 1746 | # Remediation Level 1747 | if c[0][7] == 'not_defined': 1748 | RL = 'ND' 1749 | elif c[0][7] == 'official': 1750 | RL = 'OF' 1751 | elif c[0][7] == 'workaround': 1752 | RL = 'W' 1753 | elif c[0][7] == 'unavailable': 1754 | RL = 'U' 1755 | elif c[0][7] == 'temporary': 1756 | RL = 'TF' 1757 | 1758 | # Report Confidence 1759 | if c[0][8] == 'not_defined': 1760 | RC = 'ND' 1761 | elif c[0][8] == 'confirmed': 1762 | RC = 'C' 1763 | elif c[0][8] == 'unconfirmed': 1764 | RC = 'UC' 1765 | elif c[0][8] == 'uncorroborated': 1766 | RC = 'UR' 1767 | 1768 | vector = 'AV:%s/AC:%s/Au:%s/C:%s/I:%s/A:%s/E:%s/RL:%s/RC:%s' % (AV, AC, Au, C, I, A, E, RL, RC) 1769 | 1770 | cvss = CVSS2(vector) 1771 | 1772 | return cvss 1773 | 1774 | 1775 | if args.debug: 1776 | print "\n["+warn+"]Debug output enabled" 1777 | logging.basicConfig(stream=sys.stdout, format='%(asctime)s\t%(levelname)s\t%(message)s', 1778 | datefmt='%Y-%m-%d %I:%M:%S %p', level=logging.DEBUG) # Log to STDOUT 1779 | raw_input("Press enter to continue...") 1780 | 1781 | 1782 | if __name__ == '__main__': 1783 | try: 1784 | main_menu() 1785 | except KeyboardInterrupt: 1786 | logging.info('User Interrupt! Quitting') 1787 | print "\n["+warn+"]User Interrupt! Quitting...." 1788 | except SystemExit: 1789 | pass 1790 | except: 1791 | print "\n["+warn+"]Please report this error to " + __maintainer__ + " by email at: "+ __email__ 1792 | raise 1793 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guinevere - Automated Security Assessment Reporting Tool 2 | 3 | This tool works with Gauntlet (a private tool) to automate assessment reporting. 4 | 5 | Main features include: 6 | * Generate Assessment Report 7 | * Export Assessment 8 | * Generate Retest Report 9 | * Generate Pentest Checklist 10 | 11 | 12 | ### Generate Assessment Report 13 | This option will generate you .docx report based on the vulnerabilities identified during an assessment. The report will contain a bullet list of findings, the vulnerability report write-up, and a table of interesting hosts to include host names and ports. Each report write up automatically calculates the number of affected hosts and updates the report verbiage accordingly. 14 | 15 | ### Export Assessment 16 | An SQL dump of the assessment data from gauntlet will be export to a .sql file. This file can later be imported into by other analysts. 17 | 18 | ### Generate Retest Report 19 | A .docx retest report will be generated. The tool will evaluate the original assessment findings against the retest findings. The retest findings don't need to be ranked as only the severity level of a vulnerability found in the orginial assessment will be used. New vulnerabilities and new hosts found during the retest will also be ignored. The report will contain a list of vulnerabilities along with their status (Remediated, Partially Remediated, or Not Remediated). A table will also be provided that contains hosts that are still vulnerable. A statistics table is also provided to be used with building graphs or charts. 20 | 21 | ### Generate Pentest Checklist - *BETA* 22 | The Pentest Checklist is an HTML document used for information managment while conducting a pentest. The generated report provides the analyst with a list of host and their open ports along with space for note taking. This is stil under development and provides basic functionalty. The data is retrieved from the Gauntlet database. The "-T" flag can be used to display out from tools such as Nessus but is very verbose. 23 | 24 | ## Usage 25 | ``` 26 | usage: Guinevere.py [-h] [-H DB_HOST] [-U DB_USER] [-P DB_PASS] [-p DB_PORT] 27 | [-l LINES] [-A] [-V] [-sC] [-sH] [-sM] [-sL] [-sI] [-aD] 28 | [-T] 29 | 30 | optional arguments: 31 | -h, --help show this help message and exit 32 | -H DB_HOST, --db-host DB_HOST 33 | MySQL Database Host. Default set in script 34 | -U DB_USER, --db-user DB_USER 35 | MySQL Database Username. Default set in script 36 | -P DB_PASS, --db-pass DB_PASS 37 | MySQL Database Password. Default set in script 38 | -p DB_PORT, --db-port DB_PORT 39 | MySQL Database Port. Default set in script 40 | -l LINES, --lines LINES 41 | Number of lines to display when selecting an engagement. Default is 10 42 | -A, --all-vulns Include all vulnerability headings when there are no associated report narratives 43 | -V, --all-verb Include all vureto vulnerability verbiage when there are no associated report narratives 44 | --ports Exclude port information vulnerability write-up portion of the report 45 | -sC Exclude Critical-Severity Vulnerabilities 46 | -sH Exclude High-Severity Vulnerabilities 47 | -sM Exclude Medium-Severity Vulnerabilities 48 | -sL Include Low-Severity Vulnerabilities 49 | -sI Include Informational-Severity Vulnerabilities 50 | -aD, --assessment-date 51 | Include the date when selecting an assessment to report on 52 | -T, --tool-output Include Tool Output When Printing G-Checklist 53 | ``` 54 | -------------------------------------------------------------------------------- /static/G-Checklist/G-Checklist.css: -------------------------------------------------------------------------------- 1 | table, th{ 2 | border:1px solid black; 3 | border-collapse:collapse; 4 | width:100%; 5 | height:20px; 6 | } 7 | 8 | td{ 9 | border:1px solid black; 10 | border-collapse:collapse; 11 | text-align:left; 12 | } 13 | 14 | .Host-Header{ 15 | border-left-style:none; 16 | text-align:left; 17 | font-weight:bold; 18 | font-size:125%; 19 | cursor:pointer; 20 | } 21 | 22 | input{ 23 | text-align:left; 24 | } 25 | 26 | .Port-Header{ 27 | text-align:left; 28 | font-weight:bold; 29 | font-size:70%; 30 | border-left-style:none; 31 | cursor:pointer; 32 | } 33 | 34 | .Tool-Header{ 35 | cursor:pointer; 36 | } 37 | 38 | .notes{ 39 | height:20px; 40 | border: 0px solid black; 41 | border-collapse:collapse; 42 | } 43 | 44 | .Host-Header-Check, .Port-Header-Check{ 45 | text-align:left; 46 | width:1px; 47 | border-right-style:none; 48 | } 49 | 50 | [class^=Tool-]{ 51 | text-align:left; 52 | font-size:90%; 53 | } 54 | 55 | .Tool_Output{ 56 | text-align:left; 57 | font-size:90%; 58 | } 59 | 60 | .Notes{ 61 | height:20px; 62 | border: 0px solid black; 63 | border-collapse:collapse; 64 | } 65 | 66 | .Notes-Text{ 67 | width:99%; 68 | text-align:left; 69 | border: 0px solid black; 70 | } 71 | 72 | /* Checklist Colors */ 73 | 74 | /* Host (main header) color, default is gold #D49F0E*/ 75 | .Host-Header, .Host-Header-Check{ 76 | background-color:#D49F0E; 77 | } 78 | 79 | /* Port Row Color, default is light grey #C6C8CA */ 80 | [class^=Port-Header]{ 81 | background-color:#C6C8CA; 82 | } 83 | 84 | /* Nessus Findings, default color is blue #3399FF */ 85 | [class$=-Nessus]{ 86 | background-color:#3399FF; 87 | } 88 | 89 | /* NMAP Findings, default color is blue #008A00 */ 90 | [class$=-Nmap]{ 91 | background-color:#008A00; 92 | } 93 | 94 | /* Tool Output, default is darker grey #9EA0A2 */ 95 | .Tool_Output{ 96 | background-color:#9EA0A2; 97 | } -------------------------------------------------------------------------------- /static/G-Checklist/G-Checklist_Template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pentest Checklist: $ASSESSMENT 6 | 7 | 38 | 39 | 42 | 43 |

$ASSESSMENT

44 | $DATA 45 | To save content, "Save As" -> "Webpage Complete" into a new filename 46 | 47 | --------------------------------------------------------------------------------