Created
April 29, 2025 21:22
-
-
Save colehocking/1e2a6ab8a930d7f638f77eaeb7c5e7c7 to your computer and use it in GitHub Desktop.
Create a vulnerability spreadsheet for a list of servers as input
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/python3 | |
| # Grab vulnerability reports for a list of hosts from Rapid7 InsightIVM | |
| # usage: ./vuln_reports.py -f <hostfile> | |
| # -- Cole Hocking | |
| import xlsxwriter, configparser, argparse, requests, json, os, urllib3, re | |
| from requests.auth import HTTPBasicAuth | |
| # Disable warning from self-signed cert | |
| urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | |
| def is_between(num, lower_bound, upper_bound): | |
| """ | |
| Generate the CVSS eval for severity | |
| return: boolean | |
| """ | |
| return lower_bound <= num <= upper_bound | |
| def sanitize_name(host): | |
| """ | |
| Sanitize excel sheet name | |
| Worksheet names must be < 32 chars, not containing: '[]:*?\/' | |
| :return string sanitized hostname | |
| """ | |
| maxlength = 31 | |
| badchars = "[]:*?\/" | |
| translate_table = str.maketrans("", "", badchars) | |
| host = host.translate(translate_table) | |
| # strip chars longer than maxlength | |
| if len(host) > maxlength: | |
| host = host[:maxlength] | |
| return host | |
| else: | |
| return host | |
| def is_ipv4(host): | |
| """ | |
| Check if hostname is actually IPv4 address | |
| :return boolean | |
| """ | |
| # Define an IPv4 via regex | |
| ipv4_pattern = r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" | |
| if re.match(ipv4_pattern, host): | |
| return True | |
| else: | |
| return False | |
| def get_host_ids(base_url, uname, passw, hostfile): | |
| """ | |
| Fetch Host IDs for a list of assets | |
| :return dict hostid key & hostname value | |
| """ | |
| # POST to search asset and fetch ID | |
| url = f'{base_url}api/3/assets/search' | |
| # list of hostnames | |
| hosts = {} | |
| try: | |
| with open(hostfile, 'r') as host_list: | |
| for host in host_list: | |
| if is_ipv4(host): | |
| # Query parameters for IPv4 | |
| # query: ip-address is <host> | |
| data = { | |
| "match": "all", | |
| "filters": [ | |
| { "field": "ip-address", "operator": "is", "value": host } | |
| ] | |
| } | |
| else: | |
| #remove leading/trailing whitespace | |
| host = str(host.strip()) | |
| # Query parameters for hostname | |
| # query: host-name contains <host> | |
| data = { | |
| "match": "all", | |
| "filters": [ | |
| { "field": "host-name", "operator": "contains", "value": host } | |
| ] | |
| } | |
| # run the search to get the IDs | |
| try: | |
| response = requests.post(url, | |
| auth=HTTPBasicAuth(uname, passw), json=data, verify=False) | |
| except requests.exceptions.RequestException as e: | |
| print(f"An error occurred: {e}") | |
| sys.exit(1) | |
| result = response.json() | |
| # if there was a resources list, the host was found | |
| if len(result["resources"]) > 0: | |
| hostid = result["resources"][0]["id"] | |
| # should return an integer | |
| if type(hostid) == int: | |
| # add hostid key and host value | |
| hosts[hostid] = host | |
| else: | |
| print(f"host: {host} not found") | |
| except FileNotFoundError: | |
| return "Cannot find host file." | |
| sys.exit(1) | |
| return hosts | |
| def read_configs(filename, header, value): | |
| """ | |
| Authenticate via config.ini file | |
| :return config item string | |
| """ | |
| config = configparser.ConfigParser() | |
| config.read(filename) | |
| value = config[header][value] | |
| return value | |
| def get_vuln_reports(base_url, uname, passw, hosts): | |
| """ | |
| get vuln results for a list of asset IDs and write them to excel | |
| it always needs an excel sheet... | |
| """ | |
| # Create Excel sheet; will overwrite if same name | |
| filename = 'VulnerabilityReport.xlsx' | |
| workbook = xlsxwriter.Workbook(filename) | |
| # Define headers for each sheet | |
| headers = [ 'Title', 'CVSS Score', 'Risk Score', 'Malware', 'Exploits', | |
| 'Instances', 'Exceptions', 'First Found', 'Reintroduced', | |
| 'Solution' ] | |
| # Iterate the Asset IDs and export the vulnerabilities | |
| for host_id, host in hosts.items(): | |
| url = f'{base_url}api/3/assets/{host_id}/vulnerabilities' | |
| try: | |
| response = requests.get(url, | |
| auth=HTTPBasicAuth(uname, passw), verify=False) | |
| except requests.exceptions.RequestException as e: | |
| print(f"An error occurred: {e}") | |
| sys.exit(1) | |
| result = response.json() | |
| # Create the new sheet within naming params | |
| row = 0 | |
| col = 0 | |
| # each sheet is the name of a host | |
| sheetname = sanitize_name(host) | |
| worksheet = workbook.add_worksheet(sheetname) | |
| # Write out the header row | |
| for header in headers: | |
| worksheet.write(row, col, header) | |
| col += 1 | |
| for vuln in result["resources"]: | |
| # fetch additional data on the vuln | |
| vurl = f'{base_url}api/3/vulnerabilities/{vuln["id"]}' | |
| try: | |
| vresp = requests.get(vurl, auth=HTTPBasicAuth(uname, | |
| passw), verify=False) | |
| except requests.exceptions.RequestException as e: | |
| print(f"An error occurred: {e}") | |
| sys.exit(1) | |
| vres = vresp.json() | |
| # Reset the column each time we iterate a vuln | |
| row += 1 | |
| col = 0 | |
| # Write vulnerability title | |
| worksheet.write(row, col, vuln["id"]) | |
| col += 1 | |
| # Write CVSS Score | |
| cvss = vres["cvss"]["v2"]["score"] | |
| # Add workbook format for color fill | |
| cell_format = workbook.add_format() | |
| # Define severity of CVSS score | |
| # is critical | |
| if is_between(cvss, 9.0, 10.0): | |
| print(f'critical: {cvss}') | |
| cell_format.set_bg_color('red') | |
| worksheet.write(row, col, cvss, cell_format) | |
| # is high | |
| elif is_between(cvss, 7.0, 8.9): | |
| print(f'high: {cvss}') | |
| cell_format.set_bg_color('orange') | |
| worksheet.write(row, col, cvss, cell_format) | |
| # is medium | |
| elif is_between(cvss, 4.0, 6.9): | |
| print(f'medium: {cvss}') | |
| cell_format.set_bg_color('yellow') | |
| worksheet.write(row, col, cvss, cell_format) | |
| # is low | |
| elif is_between(cvss, 0.1 - 3.9): | |
| print(f'low: {cvss}') | |
| cell_format.set_bg_color('green') | |
| worksheet.write(row, col, cvss, cell_format) | |
| # likely zero | |
| else: | |
| worksheet.write(row, col, cvss) | |
| col += 1 | |
| # Write Risk Score | |
| worksheet.write(row, col, vres["riskScore"]) | |
| col += 1 | |
| # Write Malware | |
| worksheet.write(row, col, vres["malwareKits"]) | |
| col += 1 | |
| # Write Exploits | |
| worksheet.write(row, col, vres["exploits"]) | |
| col += 1 | |
| # Write Instances | |
| worksheet.write(row, col, vuln["instances"]) | |
| col += 1 | |
| # Write Exceptions | |
| # some results are conditional; will except if not found | |
| if 'exceptions' in vuln["results"][0]: | |
| worksheet.write(row, col, vuln["results"][0]["exceptions"][0]) | |
| else: | |
| # else, enter 0 | |
| worksheet.write(row, col, 0) | |
| col += 1 | |
| # Write First Found date | |
| worksheet.write(row, col, vuln["since"]) | |
| col += 1 | |
| # Write Reintroduced date | |
| if 'reintroduced' in vuln["results"][0]: | |
| worksheet.write(row, col, vuln["results"][0]["reintroduced"]) | |
| col += 1 | |
| # Write Solutions | |
| for link in vres["links"]: | |
| if link["rel"] == 'Vulnerability Solutions': | |
| worksheet.write(row, col, link["href"]) | |
| # autofit cells | |
| worksheet.autofit() | |
| workbook.close() | |
| print(f"Results exported to: {filename}") | |
| def main(f): | |
| """ | |
| main calls | |
| """ | |
| # path to config.ini | |
| config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'config.ini')) | |
| #API v3 user/pass keys for base64 auth | |
| passw = read_configs(config_path, 'IVM', 'R7_IVM_PASSW') | |
| uname = read_configs(config_path, 'IVM', 'R7_IVM_USER') | |
| base_url = read_configs(config_path, 'IVM', 'IVM_URL') | |
| # Host file from the script argument | |
| hostfile = f | |
| # Get the list of hosts as IVM host IDs | |
| print("Fetching asset IDs for vulns...") | |
| hosts = get_host_ids(base_url, uname, passw, hostfile) | |
| # get vuln reports for a list of hostnames | |
| print("Exporting to spreadsheet...") | |
| print("Have you considered a better tool than excel?") | |
| get_vuln_reports(base_url, uname, passw, hosts) | |
| print("Complete!") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser( | |
| description="Fetch vuln reports in IVM for a list of hosts" | |
| ) | |
| parser.add_argument("-f", required=True, type=str, | |
| help="The file with the list of hostnames") | |
| args = parser.parse_args() | |
| f = args.f | |
| main(f) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment