added on Jira local download an extra field with affected assets in json format for further processing in Splunk/ELK
This commit is contained in:
@ -226,32 +226,44 @@ class JiraAPI(object):
|
|||||||
def ticket_get_unique_fields(self, ticket):
|
def ticket_get_unique_fields(self, ticket):
|
||||||
title = ticket.raw.get('fields', {}).get('summary').encode("ascii").strip()
|
title = ticket.raw.get('fields', {}).get('summary').encode("ascii").strip()
|
||||||
ticketid = ticket.key.encode("ascii")
|
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:
|
assets = self.get_assets_from_description(ticket)
|
||||||
self.logger.error("Ticket IPs regex failed. Ticket ID: {}. Reason: {}".format(ticketid, e))
|
|
||||||
assets = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not assets:
|
if not assets:
|
||||||
#check if attachment, if so, get assets from attachment
|
#check if attachment, if so, get assets from attachment
|
||||||
affected_assets_section = self.check_ips_attachment(ticket)
|
assets = self.get_assets_from_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))
|
|
||||||
|
|
||||||
return ticketid, title, assets
|
return ticketid, title, assets
|
||||||
|
|
||||||
def check_ips_attachment(self, ticket):
|
def get_assets_from_description(self, ticket, _raw = False):
|
||||||
affected_assets_section = []
|
# 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:
|
try:
|
||||||
fields = self.jira.issue(ticket.key).raw.get('fields', {})
|
fields = self.jira.issue(ticket.key).raw.get('fields', {})
|
||||||
attachments = fields.get('attachment', {})
|
attachments = fields.get('attachment', {})
|
||||||
affected_assets_section = ""
|
affected_assets = ""
|
||||||
#we will make sure we get the latest version of the file
|
#we will make sure we get the latest version of the file
|
||||||
latest = ''
|
latest = ''
|
||||||
attachment_id = ''
|
attachment_id = ''
|
||||||
@ -265,12 +277,43 @@ class JiraAPI(object):
|
|||||||
if latest < item.get('created'):
|
if latest < item.get('created'):
|
||||||
latest = item.get('created')
|
latest = item.get('created')
|
||||||
attachment_id = item.get('id')
|
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:
|
except Exception as e:
|
||||||
self.logger.error("Failed to get assets from ticket attachment. Ticket ID: {}. Reason: {}".format(ticket, 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):
|
def clean_old_attachments(self, ticket):
|
||||||
fields = ticket.raw.get('fields')
|
fields = ticket.raw.get('fields')
|
||||||
@ -522,7 +565,7 @@ class JiraAPI(object):
|
|||||||
def close_obsolete_tickets(self):
|
def close_obsolete_tickets(self):
|
||||||
# Close tickets older than 12 months, vulnerabilities not solved will get created a new ticket
|
# 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))
|
self.logger.info("Closing obsolete tickets older than {} months".format(self.max_time_tracking))
|
||||||
jql = "labels=vulnerability_management AND created <startOfMonth(-{}) and resolution=Unresolved".format(self.max_time_tracking)
|
jql = "labels=vulnerability_management AND NOT labels=advisory AND created <startOfMonth(-{}) and resolution=Unresolved".format(self.max_time_tracking)
|
||||||
tickets_to_close = self.jira.search_issues(jql, maxResults=0)
|
tickets_to_close = self.jira.search_issues(jql, maxResults=0)
|
||||||
|
|
||||||
comment = '''This ticket is being closed for hygiene, as it is more than {} months old.
|
comment = '''This ticket is being closed for hygiene, as it is more than {} months old.
|
||||||
@ -553,9 +596,36 @@ class JiraAPI(object):
|
|||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
self.logger.info("Saving locally tickets from the last {} months".format(self.max_time_tracking))
|
self.logger.info("Saving locally tickets from the last {} months".format(self.max_time_tracking))
|
||||||
jql = "labels=vulnerability_management AND created >=startOfMonth(-{})".format(self.max_time_tracking)
|
jql = "labels=vulnerability_management AND NOT labels=advisory AND created >=startOfMonth(-{})".format(self.max_time_tracking)
|
||||||
tickets_data = self.jira.search_issues(jql, maxResults=0)
|
tickets_data = self.jira.search_issues(jql, maxResults=0)
|
||||||
|
|
||||||
|
#TODO process tickets, creating a new field called "_metadata" with all the affected assets well structured
|
||||||
|
# for future processing in ELK/Splunk; this includes downloading attachments with assets and processing them
|
||||||
|
|
||||||
|
processed_tickets = []
|
||||||
|
|
||||||
|
for ticket in tickets_data:
|
||||||
|
assets = self.get_assets_from_description(ticket, _raw=True)
|
||||||
|
if not assets:
|
||||||
|
# check if attachment, if so, get assets from attachment
|
||||||
|
assets = self.get_assets_from_attachment(ticket, _raw=True)
|
||||||
|
# process the affected assets to save them as json structure on a new field from the JSON
|
||||||
|
_metadata = {"affected_hosts": []}
|
||||||
|
if assets:
|
||||||
|
if "\n" in assets:
|
||||||
|
for asset in assets.split("\n"):
|
||||||
|
assets_json = self.parse_asset_to_json(asset)
|
||||||
|
_metadata["affected_hosts"].append(assets_json)
|
||||||
|
else:
|
||||||
|
assets_json = self.parse_asset_to_json(assets)
|
||||||
|
_metadata["affected_hosts"].append(assets_json)
|
||||||
|
|
||||||
|
|
||||||
|
temp_ticket = ticket.raw.get('fields')
|
||||||
|
temp_ticket['_metadata'] = _metadata
|
||||||
|
|
||||||
|
processed_tickets.append(temp_ticket)
|
||||||
|
|
||||||
#end of line needed, as writelines() doesn't add it automatically, otherwise one big line
|
#end of line needed, as writelines() doesn't add it automatically, otherwise one big line
|
||||||
to_save = [json.dumps(ticket.raw.get('fields'))+"\n" for ticket in tickets_data]
|
to_save = [json.dumps(ticket.raw.get('fields'))+"\n" for ticket in tickets_data]
|
||||||
with open(fname, 'w') as outfile:
|
with open(fname, 'w') as outfile:
|
||||||
|
@ -204,6 +204,7 @@ class vulnWhispererBase(object):
|
|||||||
def get_latest_results(self, source, scan_name):
|
def get_latest_results(self, source, scan_name):
|
||||||
processed = 0
|
processed = 0
|
||||||
results = []
|
results = []
|
||||||
|
reported = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.conn.text_factory = str
|
self.conn.text_factory = str
|
||||||
@ -222,6 +223,7 @@ class vulnWhispererBase(object):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error("Error when getting latest results from {}.{} : {}".format(source, scan_name, e))
|
self.logger.error("Error when getting latest results from {}.{} : {}".format(source, scan_name, e))
|
||||||
|
|
||||||
return results, reported
|
return results, reported
|
||||||
|
|
||||||
def get_scan_profiles(self):
|
def get_scan_profiles(self):
|
||||||
@ -871,7 +873,7 @@ class vulnWhispererQualysVuln(vulnWhispererBase):
|
|||||||
scan_reference=None,
|
scan_reference=None,
|
||||||
output_format='json',
|
output_format='json',
|
||||||
cleanup=True):
|
cleanup=True):
|
||||||
launched_date
|
|
||||||
if 'Z' in launched_date:
|
if 'Z' in launched_date:
|
||||||
launched_date = self.qualys_scan.utils.iso_to_epoch(launched_date)
|
launched_date = self.qualys_scan.utils.iso_to_epoch(launched_date)
|
||||||
report_name = 'qualys_vuln_' + report_id.replace('/','_') \
|
report_name = 'qualys_vuln_' + report_id.replace('/','_') \
|
||||||
|
Reference in New Issue
Block a user