added automatic jira server_decommission label removal after x time
This commit is contained in:
@ -9,7 +9,7 @@ from bottle import template
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
class JiraAPI(object):
|
class JiraAPI(object):
|
||||||
def __init__(self, hostname=None, username=None, password=None, path="", debug=False, clean_obsolete=True, max_time_window=12):
|
def __init__(self, hostname=None, username=None, password=None, path="", debug=False, clean_obsolete=True, max_time_window=12, decommission_time_window=3):
|
||||||
self.logger = logging.getLogger('JiraAPI')
|
self.logger = logging.getLogger('JiraAPI')
|
||||||
if debug:
|
if debug:
|
||||||
self.logger.setLevel(logging.DEBUG)
|
self.logger.setLevel(logging.DEBUG)
|
||||||
@ -23,11 +23,8 @@ class JiraAPI(object):
|
|||||||
self.all_tickets = []
|
self.all_tickets = []
|
||||||
self.JIRA_REOPEN_ISSUE = "Reopen Issue"
|
self.JIRA_REOPEN_ISSUE = "Reopen Issue"
|
||||||
self.JIRA_CLOSE_ISSUE = "Close Issue"
|
self.JIRA_CLOSE_ISSUE = "Close Issue"
|
||||||
self.max_time_tracking = max_time_window #in months
|
|
||||||
#<JIRA Resolution: name=u'Obsolete', id=u'11'>
|
|
||||||
self.JIRA_RESOLUTION_OBSOLETE = "Obsolete"
|
self.JIRA_RESOLUTION_OBSOLETE = "Obsolete"
|
||||||
self.JIRA_RESOLUTION_FIXED = "Fixed"
|
self.JIRA_RESOLUTION_FIXED = "Fixed"
|
||||||
self.clean_obsolete = clean_obsolete
|
|
||||||
self.template_path = 'vulnwhisp/reporting/resources/ticket.tpl'
|
self.template_path = 'vulnwhisp/reporting/resources/ticket.tpl'
|
||||||
self.max_ips_ticket = 30
|
self.max_ips_ticket = 30
|
||||||
self.attachment_filename = "vulnerable_assets.txt"
|
self.attachment_filename = "vulnerable_assets.txt"
|
||||||
@ -35,6 +32,20 @@ class JiraAPI(object):
|
|||||||
self.download_tickets(path)
|
self.download_tickets(path)
|
||||||
else:
|
else:
|
||||||
self.logger.warn("No local path specified, skipping Jira ticket download.")
|
self.logger.warn("No local path specified, skipping Jira ticket download.")
|
||||||
|
self.max_time_tracking = max_time_window #in months
|
||||||
|
self.max_decommission_time = decommission_time_window #in months
|
||||||
|
# [HIGIENE] close tickets older than 12 months as obsolete (max_time_window defined)
|
||||||
|
if clean_obsolete:
|
||||||
|
self.close_obsolete_tickets()
|
||||||
|
# deletes the tag "server_decommission" from those tickets closed <=3 months ago
|
||||||
|
self.decommission_cleanup()
|
||||||
|
|
||||||
|
self.jira_still_vulnerable_comment = '''This ticket has been reopened due to the vulnerability not having been fixed (if multiple assets are affected, all need to be fixed; if the server is down, lastest known vulnerability might be the one reported).
|
||||||
|
- In the case of the team accepting the risk and wanting to close the ticket, please add the label "*risk_accepted*" to the ticket before closing it.
|
||||||
|
- If server has been decommissioned, please add the label "*server_decommission*" to the ticket before closing it.
|
||||||
|
- If when checking the vulnerability it looks like a false positive, _+please elaborate in a comment+_ and add the label "*false_positive*" before closing it; we will review it and report it to the vendor.
|
||||||
|
|
||||||
|
If you have further doubts, please contact the Security Team.'''
|
||||||
|
|
||||||
def create_ticket(self, title, desc, project="IS", components=[], tags=[], attachment_contents = []):
|
def create_ticket(self, title, desc, project="IS", components=[], tags=[], attachment_contents = []):
|
||||||
labels = ['vulnerability_management']
|
labels = ['vulnerability_management']
|
||||||
@ -88,11 +99,6 @@ class JiraAPI(object):
|
|||||||
#JIRA structure of each vulnerability: [source, scan_name, title, diagnosis, consequence, solution, ips, risk, references]
|
#JIRA structure of each vulnerability: [source, scan_name, title, diagnosis, consequence, solution, ips, risk, references]
|
||||||
self.logger.info("JIRA Sync started")
|
self.logger.info("JIRA Sync started")
|
||||||
|
|
||||||
# [HIGIENE] close tickets older than 12 months as obsolete
|
|
||||||
# Higiene clean up affects to all tickets created by the module, filters by label 'vulnerability_management'
|
|
||||||
if self.clean_obsolete:
|
|
||||||
self.close_obsolete_tickets()
|
|
||||||
|
|
||||||
for vuln in vulnerabilities:
|
for vuln in vulnerabilities:
|
||||||
# JIRA doesn't allow labels with spaces, so making sure that the scan_name doesn't have spaces
|
# JIRA doesn't allow labels with spaces, so making sure that the scan_name doesn't have spaces
|
||||||
# if it has, they will be replaced by "_"
|
# if it has, they will be replaced by "_"
|
||||||
@ -107,7 +113,7 @@ class JiraAPI(object):
|
|||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
# If ticket "resolved" -> reopen, as vulnerability is still existent
|
# If ticket "resolved" -> reopen, as vulnerability is still existent
|
||||||
self.reopen_ticket(ticketid)
|
self.reopen_ticket(ticketid=ticketid, comment=self.jira_still_vulnerable_comment)
|
||||||
self.add_label(ticketid, vuln['risk'])
|
self.add_label(ticketid, vuln['risk'])
|
||||||
continue
|
continue
|
||||||
elif to_update:
|
elif to_update:
|
||||||
@ -251,7 +257,6 @@ class JiraAPI(object):
|
|||||||
|
|
||||||
start = datetime(created[0],created[1],created[2],created[3],created[4],created[5])
|
start = datetime(created[0],created[1],created[2],created[3],created[4],created[5])
|
||||||
end = datetime(resolved[0],resolved[1],resolved[2],resolved[3],resolved[4],resolved[5])
|
end = datetime(resolved[0],resolved[1],resolved[2],resolved[3],resolved[4],resolved[5])
|
||||||
|
|
||||||
return (end-start).days
|
return (end-start).days
|
||||||
else:
|
else:
|
||||||
self.logger.error("Ticket {ticket} is not resolved, can't calculate resolution time".format(ticket=ticket))
|
self.logger.error("Ticket {ticket} is not resolved, can't calculate resolution time".format(ticket=ticket))
|
||||||
@ -272,7 +277,7 @@ class JiraAPI(object):
|
|||||||
if self.is_ticket_resolved(ticket_obj):
|
if self.is_ticket_resolved(ticket_obj):
|
||||||
if self.is_risk_accepted(ticket_obj):
|
if self.is_risk_accepted(ticket_obj):
|
||||||
return 0
|
return 0
|
||||||
self.reopen_ticket(ticketid)
|
self.reopen_ticket(ticketid=ticketid, comment=self.jira_still_vulnerable_comment)
|
||||||
|
|
||||||
#First will do the comparison of assets
|
#First will do the comparison of assets
|
||||||
ticket_obj.update()
|
ticket_obj.update()
|
||||||
@ -336,8 +341,27 @@ class JiraAPI(object):
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def remove_label(self, ticketid, label):
|
||||||
|
ticket_obj = self.jira.issue(ticketid)
|
||||||
|
|
||||||
|
if label in [x.encode('utf8') for x in ticket_obj.fields.labels]:
|
||||||
|
ticket_obj.fields.labels.remove(label)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ticket_obj.update(fields={"labels":ticket_obj.fields.labels})
|
||||||
|
self.logger.info("Removed label {label} from ticket {ticket}".format(label=label, ticket=ticketid))
|
||||||
|
except:
|
||||||
|
self.logger.error("Error while trying to remove label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
|
||||||
|
else:
|
||||||
|
self.logger.error("Error: label {label} not in ticket {ticket}".format(label=label, ticket=ticketid))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
def close_fixed_tickets(self, vulnerabilities):
|
def close_fixed_tickets(self, vulnerabilities):
|
||||||
# close tickets which vulnerabilities have been resolved and are still open
|
'''
|
||||||
|
Close tickets which vulnerabilities have been resolved and are still open.
|
||||||
|
Higiene clean up affects to all tickets created by the module, filters by label 'vulnerability_management'
|
||||||
|
'''
|
||||||
found_vulns = []
|
found_vulns = []
|
||||||
for vuln in vulnerabilities:
|
for vuln in vulnerabilities:
|
||||||
found_vulns.append(vuln['title'])
|
found_vulns.append(vuln['title'])
|
||||||
@ -399,23 +423,18 @@ class JiraAPI(object):
|
|||||||
self.logger.info("Ticket {} risk has not been accepted".format(ticket_obj))
|
self.logger.info("Ticket {} risk has not been accepted".format(ticket_obj))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def reopen_ticket(self, ticketid):
|
def reopen_ticket(self, ticketid, ignore_labels=False, comment=""):
|
||||||
self.logger.debug("Ticket {} exists, REOPEN requested".format(ticketid))
|
self.logger.debug("Ticket {} exists, REOPEN requested".format(ticketid))
|
||||||
# this will reopen a ticket by ticketid
|
# this will reopen a ticket by ticketid
|
||||||
ticket_obj = self.jira.issue(ticketid)
|
ticket_obj = self.jira.issue(ticketid)
|
||||||
|
|
||||||
if self.is_ticket_resolved(ticket_obj):
|
if self.is_ticket_resolved(ticket_obj):
|
||||||
if not self.is_risk_accepted(ticket_obj):
|
if (not self.is_risk_accepted(ticket_obj) or ignore_labels):
|
||||||
try:
|
try:
|
||||||
if self.is_ticket_reopenable(ticket_obj):
|
if self.is_ticket_reopenable(ticket_obj):
|
||||||
comment = '''This ticket has been reopened due to the vulnerability not having been fixed (if multiple assets are affected, all need to be fixed; if the server is down, lastest known vulnerability might be the one reported).
|
|
||||||
- In the case of the team accepting the risk and wanting to close the ticket, please add the label "*risk_accepted*" to the ticket before closing it.
|
|
||||||
- If server has been decommissioned, please add the label "*server_decommission*" to the ticket before closing it.
|
|
||||||
- If when checking the vulnerability it looks like a false positive, _+please elaborate in a comment+_ and add the label "*false_positive*" before closing it; we will review it and report it to the vendor.
|
|
||||||
|
|
||||||
If you have further doubts, please contact the Security Team.'''
|
|
||||||
error = self.jira.transition_issue(issue=ticketid, transition=self.JIRA_REOPEN_ISSUE, comment = comment)
|
error = self.jira.transition_issue(issue=ticketid, transition=self.JIRA_REOPEN_ISSUE, comment = comment)
|
||||||
self.logger.info("Ticket {} reopened successfully".format(ticketid))
|
self.logger.info("Ticket {} reopened successfully".format(ticketid))
|
||||||
|
if not ignore_labels:
|
||||||
self.add_label(ticketid, 'reopened')
|
self.add_label(ticketid, 'reopened')
|
||||||
return 1
|
return 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -449,8 +468,8 @@ class JiraAPI(object):
|
|||||||
jql = "labels=vulnerability_management AND created <startOfMonth(-{}) and resolution=Unresolved".format(self.max_time_tracking)
|
jql = "labels=vulnerability_management 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 12 months old.
|
comment = '''This ticket is being closed for hygiene, as it is more than {} months old.
|
||||||
If the vulnerability still exists, a new ticket will be opened.'''
|
If the vulnerability still exists, a new ticket will be opened.'''.format(self.max_time_tracking)
|
||||||
|
|
||||||
for ticket in tickets_to_close:
|
for ticket in tickets_to_close:
|
||||||
self.close_ticket(ticket, self.JIRA_RESOLUTION_OBSOLETE, comment)
|
self.close_ticket(ticket, self.JIRA_RESOLUTION_OBSOLETE, comment)
|
||||||
@ -466,7 +485,9 @@ class JiraAPI(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def download_tickets(self, path):
|
def download_tickets(self, path):
|
||||||
#saves all tickets locally, local snapshot of vulnerability_management ticktes
|
'''
|
||||||
|
saves all tickets locally, local snapshot of vulnerability_management ticktes
|
||||||
|
'''
|
||||||
#check if file already exists
|
#check if file already exists
|
||||||
check_date = str(date.today())
|
check_date = str(date.today())
|
||||||
fname = '{}jira_{}.json'.format(path, check_date)
|
fname = '{}jira_{}.json'.format(path, check_date)
|
||||||
@ -490,3 +511,26 @@ class JiraAPI(object):
|
|||||||
self.logger.error("Tickets could not be saved locally: {}.".format(e))
|
self.logger.error("Tickets could not be saved locally: {}.".format(e))
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def decommission_cleanup(self):
|
||||||
|
'''
|
||||||
|
deletes the server_decomission tag from those tickets that have been
|
||||||
|
closed already for more than x months (default is 3 months) in order to clean solved issues
|
||||||
|
for statistics purposes
|
||||||
|
'''
|
||||||
|
self.logger.info("Deleting 'server_decommission' tag from tickets closed more than {} months ago".format(self.max_decommission_time))
|
||||||
|
|
||||||
|
jql = "labels=vulnerability_management AND labels=server_decommission and resolutiondate <=startOfMonth(-{})".format(self.max_decommission_time)
|
||||||
|
decommissioned_tickets = self.jira.search_issues(jql, maxResults=0)
|
||||||
|
|
||||||
|
comment = '''This ticket is having deleted the *server_decommission* tag deleted, as it is more than {} months old and is expected to already have been decommissioned.
|
||||||
|
If that is not the case and the vulnerability still exists, the vulnerability will be opened again.'''.format(self.max_decommission_time)
|
||||||
|
|
||||||
|
for ticket in decommissioned_tickets:
|
||||||
|
#we open first the ticket, as we want to make sure the process is not blocked due to
|
||||||
|
#an unexisting jira workflow or unallowed edit from closed tickets
|
||||||
|
self.reopen_ticket(ticketid=ticket, ignore_labels=True)
|
||||||
|
self.remove_label(ticket, 'server_decommission')
|
||||||
|
self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
Reference in New Issue
Block a user