diff --git a/README.md b/README.md index 42e8f89..2e51604 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ **Author** : **Nikhil Raj ( nikhilraj149@gmail.com )** - **Version: 1.0** + **Version: 1.1** - **Last Updated** : 13 Aug 2017 + **Last Updated** : 21 Dec 2020 **Description**: @@ -16,41 +16,42 @@ **Usage**: 1) Display help +``` +$ usage: python nessus_report_downloader.py -i <127.0.0.1> -u -p [OPTIONS]... - $ python nessus_report_downloader.py -h - - Output: - - usage: python nessus_report_downloader.py -i <127.0.0.1> -u -p [OPTIONS]... - - A python script for automating the report download from nessus server - - optional arguments: - -h, --help show this help message and exit - -i SERVER, --server SERVER - IP[:PORT] of nessus server - -u USER, --user USER username of nessus server - -p PASSWD, --passwd PASSWD - password of nessus server - -s SCAN_ID, --scan-id SCAN_ID - use comma separated list of scan id(s) or 'all' - -d FOLDER_ID, --folder-id FOLDER_ID - use comma separated list of folder id(s) - -f FORMAT, --format FORMAT - use comma separated list of report format; [0]-nessus - (Default), [1]-pdf, [2]-html, [3]-csv, [4]-nessus-db - -c CHAPTER, --chapter CHAPTER - use comma separated list of chapters; - [0]-vuln_hosts_summary, [1]-vuln_by_host (Default), - [2]-vuln_by_plugin, [3]-compliance_exec, - [4]-compliance, [5]-remediations - --db-pass DB_PASS password for encrypting nessus-db file(s), if none - specified use 'nessus' - - Report bugs at nikhilraj149@gmail.com +A python script for automating the report download from nessus server + +optional arguments: + -h, --help show this help message and exit + -i SERVER, --server SERVER + IP[:PORT] of nessus server + -u USER, --user USER username of nessus server + -p PASSWD, --passwd PASSWD + password of nessus server + -s SCAN_ID, --scan-id SCAN_ID + use comma separated list of scan id(s) or 'all' + -d FOLDER_ID, --folder-id FOLDER_ID + use comma separated list of folder id(s) + -D DEBUG, --debug DEBUG + enable debugging + -m MODIFIED_AFTER, --modified-after MODIFIED_AFTER + download only scans modified after x days in history + -f FORMAT, --format FORMAT + use comma separated list of report format; [0]-nessus + (Default), [1]-pdf, [2]-html, [3]-csv, [4]-nessus-db + -c CHAPTER, --chapter CHAPTER + use comma separated list of chapters; + [0]-vuln_hosts_summary, [1]-vuln_by_host (Default), + [2]-vuln_by_plugin, [3]-compliance_exec, + [4]-compliance, [5]-remediations + --db-pass DB_PASS password for encrypting nessus-db file(s), if none + specified use 'nessus' + +Report bugs at nikhilraj149@gmail.com +``` 2) Display nessus scan summary table having scan_id, name and folder_id - +``` $ python nessus_report_downloader.py -i -u -p $ python nessus_report_downloader.py -i 127.0.0.1 -u nessus -p pass@123 @@ -66,12 +67,11 @@ | 14 | Win7 Test -Post Wnycry | 3 | completed | 2017-07-23 18:40:30 | 2017-07-23 18:40:46 | | 11 | Win7 Test | 3 | completed | 2017-07-23 13:35:18 | 2017-07-23 13:35:36 | +----+------------------------------+-----------+-----------+---------------------+------------------------+ - +``` 3) Download nessus report (Optional parameter description): - Scan Id (-s): - List of comma seperated scan id for download diff --git a/nessus_report_downloader.py b/nessus_report_downloader.py index 26fddca..f5f0b52 100755 --- a/nessus_report_downloader.py +++ b/nessus_report_downloader.py @@ -4,8 +4,8 @@ # Name: Nessus Report downloader # Author: Nikhil Raj ( nikhilraj149@gmail.com ) # -# Version: 1.0 -# Last Updated: 13 Aug 2017 +# Version: 1.1 +# Last Updated: 21 Dec 2020 # # Description: A python script for automating the download of nessus reports in multiple formats. # @@ -19,16 +19,19 @@ # ######################################################################################################### -import requests -import json import argparse +import json +import logging +import os import time from datetime import datetime +import requests + try: from prettytable import PrettyTable except ImportError: - print "[-] Unable to load PrettyTable library, will print data in generic format" + logging.warning("Unable to load PrettyTable library, will print data in generic format") HAS_PRETTYTABLE = False else: HAS_PRETTYTABLE = True @@ -36,7 +39,8 @@ # Disable ssl error warning requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) -SLEEP_TIME=1.0 +SLEEP_TIME = 1.0 + # Send HTTP GET request def sendGetRequest(url, headers): @@ -44,8 +48,8 @@ def sendGetRequest(url, headers): r = requests.get(url, verify=False, headers=headers) return r except requests.exceptions.ConnectionError: - print "[-] Failed to establish connection" - exit(-1) + logging.error("Failed to establish connection") + raise # Send HTTP POST request @@ -54,8 +58,9 @@ def sendPostRequest(url, json_data={}, headers={}): r = requests.post(url, verify=False, headers=headers, json=json_data) return r except requests.exceptions.ConnectionError: - print "[-] Failed to establish connection" - exit(-1) + logging.error("Failed to establish connection") + raise + # Send HTTP DELETE request def sendDeleteRequest(url, json_data={}, headers={}): @@ -63,28 +68,17 @@ def sendDeleteRequest(url, json_data={}, headers={}): r = requests.delete(url, verify=False, headers=headers, json=json_data) return r except requests.exceptions.ConnectionError: - print "[-] Failed to establish connection" - exit(-1) - -# Print message on stdout -def printMessage(msg, flag=1): - if flag == 1: - print "[+] " + msg - elif flag == 0: - print "[-] " + msg - elif flag == 2: - print "[*] " + msg - else: - print msg + logging.error("Failed to establish connection") + raise # Check response code for an HTTP Response and print req message def checkStatus(resp, status_msg, error_msg): if resp.status_code == 200: - printMessage(status_msg, 1) + logging.debug(status_msg) return True else: - printMessage(error_msg, 0) + logging.error(error_msg) return False @@ -100,7 +94,7 @@ def printTable(data, table_headers): else: l.append(str(row[header])) tab.add_row(l) - print tab + print(tab) def printScanData(scan_data): @@ -113,13 +107,20 @@ def printScanData(scan_data): printTable(scan_data["scans"], ["id", "name", "folder_id", "status", "creation_date", "last_modification_date"]) else: # print scan header - print '\t %-10s %-20s %-20s %-40s %-20s %-20s' %("Scan Id", "Folder Name (id)", "Scan status","Scan Name","creation_date", "last_modification_date") - print '\t %-10s %-20s %-20s %-40s %-20s %-20s' %("-------", "---------------", "------------", "-----------------","-------------------", "--------------------") + print('\t %-10s %-20s %-20s %-40s %-20s %-20s' % ( + "Scan Id", "Folder Name (id)", "Scan status", "Scan Name", "creation_date", "last_modification_date")) + print('\t %-10s %-20s %-20s %-40s %-20s %-20s' % ( + "-------", "---------------", "------------", "-----------------", "-------------------", + "--------------------")) for scan in scan_data["scans"]: - print '\t %-10s %-20s %-20s %-40s %-20s %-20s' %(str(scan["id"]),folder_info[scan["folder_id"]] + ' (' + str( - scan["folder_id"]) + ') ', scan["status"], scan["name"],datetime.fromtimestamp(int(scan["creation_date"])).strftime('%Y-%m-%d %H:%M:%S'),datetime.fromtimestamp(int(scan["last_modification_date"])).strftime('%Y-%m-%d %H:%M:%S')) + print('\t %-10s %-20s %-20s %-40s %-20s %-20s' % ( + str(scan["id"]), folder_info[scan["folder_id"]] + ' (' + str( + scan["folder_id"]) + ') ', scan["status"], scan["name"], + datetime.fromtimestamp(int(scan["creation_date"])).strftime('%Y-%m-%d %H:%M:%S'), + datetime.fromtimestamp(int(scan["last_modification_date"])).strftime('%Y-%m-%d %H:%M:%S'))) + + print('\n') - print '\n' # Verify user specified folder Id def verifyScanId(scan_data, ui_scan_id): @@ -136,7 +137,7 @@ def verifyScanId(scan_data, ui_scan_id): if int(scan) in master_scan_id_list: valid_scan_list.append(scan) else: - printMessage("Omitting invalid Scan ID: " + scan, 0) + logging.warning("Omitting invalid Scan ID: %s " % scan) return valid_scan_list @@ -154,7 +155,7 @@ def verifyFolderId(scan_data, ui_folder_id): if int(folder_id) in master_folder_id_list: valid_folder_id_list.append(folder_id) else: - printMessage("Omitting invalid folder ID: " + folder_id, 0) + logging.warning("Omitting invalid folder ID: %s " % folder_id) for scan in scan_data["scans"]: for folder_id in valid_folder_id_list: @@ -165,7 +166,7 @@ def verifyFolderId(scan_data, ui_folder_id): def getFormatAndChapterList(nessus_format_list, chapter_list, db_pass): - data=list() + data = list() for nessus_format in nessus_format_list: if nessus_format == "0": @@ -201,73 +202,111 @@ def getFormatAndChapterList(nessus_format_list, chapter_list, db_pass): if nessus_format == "3": data.append({'format': 'csv', 'chapters': ''}) if nessus_format == "4": - data.append({'format': 'db', 'chapters': '', 'password' : db_pass}) + data.append({'format': 'db', 'chapters': '', 'password': db_pass}) return data -def downloadNessusReport(base_url, token, scan_id_list, json_user_data): + +def downloadNessusReport(base_url, token, scan_id_list, modified_after, json_user_data): for scan_id in scan_id_list: - printMessage("Format: {0} | Chapter: {1}".format(json_user_data["format"], json_user_data["chapters"])) - printMessage("Initiating download request for scan id: " + str(scan_id), 1) + logging.debug("Format: {0} | Chapter: {1}".format(json_user_data["format"], json_user_data["chapters"])) + logging.debug("Getting info about scan id: " + str(scan_id)) + + token_header = {'X-Cookie': 'token=' + token['token']} + + url = base_url + "/scans/{0}?limit=1".format(str(scan_id)) + resp = sendGetRequest(url, headers=token_header) + info = json.loads(resp.text) + name = info["info"]["name"] + filename = name + ".nessus" + after = time.time() - int(modified_after) * 3600 * 24 + last = 0 + if "history" in info and info["history"]: + for h in info["history"]: + if h["status"] != "completed": + continue + if int(h["creation_date"]) > int(after): + last = h["history_id"] + lastdate = h["creation_date"] + if last == 0: + logging.warning("No report %s/%s within specified timerange!" % (name, scan_id)) + continue + else: + logging.warning("No history available for scan %s/%s!" % (name, scan_id)) + continue - token_header={'X-Cookie': 'token=' + token['token']} + logging.warning( + "Found scan {0}/{1} result from day {2}".format(name, str(scan_id), datetime.fromtimestamp(lastdate))) + + if os.path.exists(filename): + st = os.stat(filename) + if st.st_mtime>=lastdate: + logging.warning("Report %s already downloaded (newer than scan date)" % filename) + continue # Initiate download request for given scan id - url = base_url + "/scans/{0}/export".format(str(scan_id)) - resp = sendPostRequest(url, json_data=json_user_data,headers=token_header) + logging.warning("Initiating scan %s export" % filename) + url = base_url + "/scans/{0}/export?history_id={1}".format(str(scan_id), str(last)) + resp = sendPostRequest(url, json_data=json_user_data, headers=token_header) file_token = json.loads(resp.text) # Check if file is ready for download - url = base_url + "/scans/{0}/export/{1}/status".format(str(scan_id),str(file_token["file"])) - resp2 = sendGetRequest(url,headers=token_header) + url = base_url + "/scans/{0}/export/{1}/status".format(str(scan_id), str(file_token["file"])) + resp2 = sendGetRequest(url, headers=token_header) while json.loads(resp2.text)["status"] == "loading": - printMessage("Report is not ready yet, waiting for {0} seconds".format(SLEEP_TIME),0) + logging.debug("Report is not ready yet, waiting for {0} seconds".format(SLEEP_TIME)) time.sleep(SLEEP_TIME) resp2 = sendGetRequest(url, headers=token_header) # If nessus report is ready for download, then write the response in external file - url= base_url + "/scans/exports/{0}/download".format(str(file_token["token"])) + url = base_url + "/scans/exports/{0}/download".format(str(file_token["token"])) if json.loads(resp2.text)["status"] == "ready": - printMessage("Download link is available now", 1) - resp3 = sendGetRequest(url,headers=token_header) + logging.debug("Download link is available now") + resp3 = sendGetRequest(url, headers=token_header) + resp3.encoding = 'utf-8' if checkStatus(resp3, "Started downloading the nessus report", "Unable to download scan: " + str(scan_id)): - filename = resp3.headers["Content-Disposition"].split('"')[1] try: nessus_file = open(filename, "w") nessus_file.write(resp3.text) nessus_file.close() - printMessage("Report was saved in " + filename, 1) - printMessage("\n", 99) + logging.warning("Report was saved in %s" % filename) except IOError: - printMessage("Error occurred while writing to file : " + filename, 0) + logging.error("Error occurred while writing to file %s" % filename) + raise except UnicodeEncodeError: # Append the chapter type in file name - filename2=filename.split(".")[0]+'_'+json_user_data["chapters"]+'.'+filename.split(".")[-1] - nessus_file = open(filename2, "wb") + nessus_file = open(filename, "wb") nessus_file.write(resp3.content) nessus_file.close() - printMessage("Report was saved in " + filename2, 1) - printMessage("\n", 99) + logging.warning("Report was saved in %s" % filename) def main(): - # Parsing command line options - parser = argparse.ArgumentParser(description="A python script for automating the report download from nessus server", - epilog="Report bugs at nikhilraj149@gmail.com", - prog='python nessus_report_downloader.py', usage='%(prog)s -i <127.0.0.1> -u -p [OPTIONS]... ') + parser = argparse.ArgumentParser( + description="A python script for automating the report download from nessus server", + epilog="Report bugs at nikhilraj149@gmail.com", + prog='python nessus_report_downloader.py', + usage='%(prog)s -i <127.0.0.1> -u -p [OPTIONS]... ') parser.add_argument("-i", "--server", help="IP[:PORT] of nessus server", required=True) parser.add_argument("-u", "--user", help="username of nessus server", required=True) parser.add_argument("-p", "--passwd", help="password of nessus server", required=True) parser.add_argument("-s", "--scan-id", help="use comma separated list of scan id(s) or 'all' ") parser.add_argument("-d", "--folder-id", help="use comma separated list of folder id(s)") - parser.add_argument("-f", "--format", help="use comma separated list of report format; [0]-nessus (Default), [1]-pdf, [2]-html, [3]-csv, [4]-nessus-db",default="0") - parser.add_argument("-c", "--chapter", help="use comma separated list of chapters; [0]-vuln_hosts_summary, [1]-vuln_by_host (Default), " - "[2]-vuln_by_plugin, [3]-compliance_exec, [4]-compliance, [5]-remediations",default="1") - parser.add_argument("--db-pass", help="password for encrypting nessus-db file(s), if none specified use 'nessus'",default="nessus") + parser.add_argument("-D", "--debug", help="enable debugging") + parser.add_argument("-m", "--modified-after", help="download only scans modified after x days in history", + default=30) + parser.add_argument("-f", "--format", + help="use comma separated list of report format; [0]-nessus (Default), [1]-pdf, [2]-html, [3]-csv, [4]-nessus-db", + default="0") + parser.add_argument("-c", "--chapter", + help="use comma separated list of chapters; [0]-vuln_hosts_summary, [1]-vuln_by_host (Default), " + "[2]-vuln_by_plugin, [3]-compliance_exec, [4]-compliance, [5]-remediations", default="1") + parser.add_argument("--db-pass", help="password for encrypting nessus-db file(s), if none specified use 'nessus'", + default="nessus") args = parser.parse_args() # Nessus server url @@ -283,6 +322,8 @@ def main(): # Login credentials creds = {'username': args.user, 'password': args.passwd} + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) # Checking connection to nessus server resp = sendGetRequest(base_url, "") @@ -310,26 +351,28 @@ def main(): elif args.folder_id: scan_id_list = verifyFolderId(scan_data, args.folder_id) - printMessage("Identified " + str(len(scan_id_list)) + " scan(s) for download\n", 2) + logging.warning("Identified " + str(len(scan_id_list)) + " scan(s) for download\n") # Choose default values if not supplied via std input if not args.format: - printMessage("Missing -f option, using default [0]-nessus format\n",0) + logging.debug("Missing -f option, using default [0]-nessus format\n") if not args.chapter and (("1" in args.format) or ("2" in args.format)): - printMessage("Missing -g option, If required vuln_by_host will be default chapter\n", 0) + logging.debug("Missing -g option, If required vuln_by_host will be default chapter\n") if not args.db_pass and "4" in args.format: - printMessage("Missing --db-pass option, using default db password: 'nessus' \n", 0) + logging.debug("Missing --db-pass option, using default db password: 'nessus' \n") # Create a list of format and chapters for report creation format_specification = getFormatAndChapterList(args.format, args.chapter, args.db_pass) for report_format in format_specification: - downloadNessusReport(base_url, token, scan_id_list, json_user_data=report_format) + downloadNessusReport(base_url, token, scan_id_list, args.modified_after, + json_user_data=report_format) # Logout - resp = sendDeleteRequest(base_url+"/session",headers={'X-Cookie': 'token=' + token['token']}) + resp = sendDeleteRequest(base_url + "/session", headers={'X-Cookie': 'token=' + token['token']}) checkStatus(resp, "Successfully logged out user session\n", "Unable to logout the current active session") - printMessage("Thanks, See you again!") + logging.warning("Thanks, See you again!") + if __name__ == '__main__': - main() \ No newline at end of file + main()