From adb770030007665be77cbe20aab1909576325baf Mon Sep 17 00:00:00 2001 From: Quim Date: Fri, 21 Feb 2020 11:00:07 +0100 Subject: [PATCH 1/2] added on Jira local download an extra field with affected assets in json format for further processing in Splunk/ELK --- vulnwhisp/reporting/jira_api.py | 120 +++++++++++++++++++++++++------- vulnwhisp/vulnwhisp.py | 4 +- 2 files changed, 98 insertions(+), 26 deletions(-) diff --git a/vulnwhisp/reporting/jira_api.py b/vulnwhisp/reporting/jira_api.py index 12b3360..d7e9b41 100644 --- a/vulnwhisp/reporting/jira_api.py +++ b/vulnwhisp/reporting/jira_api.py @@ -226,32 +226,44 @@ class JiraAPI(object): def ticket_get_unique_fields(self, ticket): title = ticket.raw.get('fields', {}).get('summary').encode("ascii").strip() ticketid = ticket.key.encode("ascii") - assets = [] - try: - affected_assets_section = ticket.raw.get('fields', {}).get('description').encode("ascii").split("{panel:title=Affected Assets}")[1].split("{panel}")[0] - assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", affected_assets_section))) - - except Exception as e: - self.logger.error("Ticket IPs regex failed. Ticket ID: {}. Reason: {}".format(ticketid, e)) - assets = [] - - try: - if not assets: - #check if attachment, if so, get assets from attachment - affected_assets_section = self.check_ips_attachment(ticket) - if affected_assets_section: - assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", affected_assets_section))) - except Exception as e: - self.logger.error("Ticket IPs Attachment regex failed. Ticket ID: {}. Reason: {}".format(ticketid, e)) + + assets = self.get_assets_from_description(ticket) + if not assets: + #check if attachment, if so, get assets from attachment + assets = self.get_assets_from_attachment(ticket) return ticketid, title, assets - def check_ips_attachment(self, ticket): - affected_assets_section = [] + def get_assets_from_description(self, ticket, _raw = False): + # Get the assets as a string "host - protocol/port - hostname" separated by "\n" + # structure the text to have the same structure as the assets from the attachment + affected_assets = "" + try: + affected_assets = ticket.raw.get('fields', {}).get('description').encode("ascii").split("{panel:title=Affected Assets}")[1].split("{panel}")[0].replace('\n','').replace(' * ','\n').replace('\n', '', 1) + except Exception as e: + self.logger.error("Unable to process the Ticket's 'Affected Assets'. Ticket ID: {}. Reason: {}".format(ticket, e)) + + if affected_assets: + if _raw: + # from line 406 check if the text in the panel corresponds to having added an attachment + if "added as an attachment" in affected_assets: + return False + return affected_assets + + try: + # if _raw is not true, we return only the IPs of the affected assets + return list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", affected_assets))) + except Exception as e: + self.logger.error("Ticket IPs regex failed. Ticket ID: {}. Reason: {}".format(ticket, e)) + return False + + def get_assets_from_attachment(self, ticket, _raw = False): + # Get the assets as a string "host - protocol/port - hostname" separated by "\n" + affected_assets = [] try: fields = self.jira.issue(ticket.key).raw.get('fields', {}) attachments = fields.get('attachment', {}) - affected_assets_section = "" + affected_assets = "" #we will make sure we get the latest version of the file latest = '' attachment_id = '' @@ -265,12 +277,43 @@ class JiraAPI(object): if latest < item.get('created'): latest = item.get('created') attachment_id = item.get('id') - affected_assets_section = self.jira.attachment(attachment_id).get() + affected_assets = self.jira.attachment(attachment_id).get() except Exception as e: self.logger.error("Failed to get assets from ticket attachment. Ticket ID: {}. Reason: {}".format(ticket, e)) - return affected_assets_section + if affected_assets: + if _raw: + return affected_assets + + try: + # if _raw is not true, we return only the IPs of the affected assets + affected_assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", affected_assets))) + return affected_assets + except Exception as e: + self.logger.error("Ticket IPs Attachment regex failed. Ticket ID: {}. Reason: {}".format(ticket, e)) + + return False + + def parse_asset_to_json(self, asset): + hostname, protocol, port = "", "", "" + asset_info = asset.split(" - ") + ip = asset_info[0] + proto_port = asset_info[1] + # in case there is some case where hostname is not reported at all + if len(asset_info) == 3: + hostname = asset_info[2] + if proto_port != "N/A/N/A": + protocol, port = proto_port.split("/") + + asset_dict = { + "host": ip, + "protocol": protocol, + "port": port, + "hostname": hostname + } + + return asset_dict def clean_old_attachments(self, ticket): fields = ticket.raw.get('fields') @@ -522,7 +565,7 @@ class JiraAPI(object): def close_obsolete_tickets(self): # Close tickets older than 12 months, vulnerabilities not solved will get created a new ticket self.logger.info("Closing obsolete tickets older than {} months".format(self.max_time_tracking)) - jql = "labels=vulnerability_management AND created Date: Fri, 21 Feb 2020 15:50:14 +0100 Subject: [PATCH 2/2] fixed issue where when actioning all actions, if one failed it exited the program --- bin/vuln_whisperer | 19 ++++++----- vulnwhisp/vulnwhisp.py | 71 +++++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/bin/vuln_whisperer b/bin/vuln_whisperer index 09ed142..b934063 100644 --- a/bin/vuln_whisperer +++ b/bin/vuln_whisperer @@ -83,14 +83,17 @@ def main(): enabled_sections = config.get_sections_with_attribute('enabled') for section in enabled_sections: - vw = vulnWhisperer(config=args.config, - profile=section, - verbose=args.verbose, - username=args.username, - password=args.password, - source=args.source, - scanname=args.scanname) - exit_code += vw.whisper_vulnerabilities() + try: + vw = vulnWhisperer(config=args.config, + profile=section, + verbose=args.verbose, + username=args.username, + password=args.password, + source=args.source, + scanname=args.scanname) + exit_code += vw.whisper_vulnerabilities() + except Exception as e: + logger.error("VulnWhisperer was unable to perform the processing on '{}'".format(args.source)) else: logger.info('Running vulnwhisperer for section {}'.format(args.section)) vw = vulnWhisperer(config=args.config, diff --git a/vulnwhisp/vulnwhisp.py b/vulnwhisp/vulnwhisp.py index af791d1..97158e4 100755 --- a/vulnwhisp/vulnwhisp.py +++ b/vulnwhisp/vulnwhisp.py @@ -319,7 +319,8 @@ class vulnWhispererNessus(vulnWhispererBase): e=e)) except Exception as e: self.logger.error('Could not properly load your config!\nReason: {e}'.format(e=e)) - sys.exit(1) + return False + #sys.exit(1) @@ -575,8 +576,11 @@ class vulnWhispererQualys(vulnWhispererBase): self.logger = logging.getLogger('vulnWhispererQualys') if debug: self.logger.setLevel(logging.DEBUG) - - self.qualys_scan = qualysScanReport(config=config) + try: + self.qualys_scan = qualysScanReport(config=config) + except Exception as e: + self.logger.error("Unable to establish connection with Qualys scanner. Reason: {}".format(e)) + return False self.latest_scans = self.qualys_scan.qw.get_all_scans() self.directory_check() self.scans_to_process = None @@ -747,10 +751,14 @@ class vulnWhispererOpenVAS(vulnWhispererBase): self.develop = True self.purge = purge self.scans_to_process = None - self.openvas_api = OpenVAS_API(hostname=self.hostname, - port=self.port, - username=self.username, - password=self.password) + try: + self.openvas_api = OpenVAS_API(hostname=self.hostname, + port=self.port, + username=self.username, + password=self.password) + except Exception as e: + self.logger.error("Unable to establish connection with OpenVAS scanner. Reason: {}".format(e)) + return False def whisper_reports(self, output_format='json', launched_date=None, report_id=None, cleanup=True): report = None @@ -861,8 +869,11 @@ class vulnWhispererQualysVuln(vulnWhispererBase): self.logger = logging.getLogger('vulnWhispererQualysVuln') if debug: self.logger.setLevel(logging.DEBUG) - - self.qualys_scan = qualysVulnScan(config=config) + try: + self.qualys_scan = qualysVulnScan(config=config) + except Exception as e: + self.logger.error("Unable to create connection with Qualys. Reason: {}".format(e)) + return False self.directory_check() self.scans_to_process = None @@ -1009,7 +1020,8 @@ class vulnWhispererJIRA(vulnWhispererBase): raise Exception( 'Could not connect to nessus -- Please verify your settings in {config} are correct and try again.\nReason: {e}'.format( config=self.config.config_in, e=e)) - sys.exit(1) + return False + #sys.exit(1) profiles = [] profiles = self.get_scan_profiles() @@ -1261,7 +1273,10 @@ class vulnWhispererJIRA(vulnWhispererBase): if autoreport_sections: for scan in autoreport_sections: - self.jira_sync(self.config.get(scan, 'source'), self.config.get(scan, 'scan_name')) + try: + self.jira_sync(self.config.get(scan, 'source'), self.config.get(scan, 'scan_name')) + except Exception as e: + self.logger.error("VulnWhisperer wasn't able to report the vulnerabilities from the '{}'s source".format(self.config.get(scan, 'source'))) return True return False @@ -1294,36 +1309,42 @@ class vulnWhisperer(object): if self.profile == 'nessus': vw = vulnWhispererNessus(config=self.config, profile=self.profile) - self.exit_code += vw.whisper_nessus() + if vw: + self.exit_code += vw.whisper_nessus() elif self.profile == 'qualys_web': vw = vulnWhispererQualys(config=self.config) - self.exit_code += vw.process_web_assets() + if vw: + self.exit_code += vw.process_web_assets() elif self.profile == 'openvas': vw_openvas = vulnWhispererOpenVAS(config=self.config) - self.exit_code += vw_openvas.process_openvas_scans() + if vw: + self.exit_code += vw_openvas.process_openvas_scans() elif self.profile == 'tenable': vw = vulnWhispererNessus(config=self.config, profile=self.profile) - self.exit_code += vw.whisper_nessus() + if vw: + self.exit_code += vw.whisper_nessus() elif self.profile == 'qualys_vuln': vw = vulnWhispererQualysVuln(config=self.config) - self.exit_code += vw.process_vuln_scans() + if vw: + self.exit_code += vw.process_vuln_scans() elif self.profile == 'jira': #first we check config fields are created, otherwise we create them vw = vulnWhispererJIRA(config=self.config) - if not (self.source and self.scanname): - self.logger.info('No source/scan_name selected, all enabled scans will be synced') - success = vw.sync_all() - if not success: - self.logger.error('All scans sync failed!') - self.logger.error('Source scanner and scan name needed!') - return 0 - else: - vw.jira_sync(self.source, self.scanname) + if vw: + if not (self.source and self.scanname): + self.logger.info('No source/scan_name selected, all enabled scans will be synced') + success = vw.sync_all() + if not success: + self.logger.error('All scans sync failed!') + self.logger.error('Source scanner and scan name needed!') + return 0 + else: + vw.jira_sync(self.source, self.scanname) return self.exit_code