|
|
@ -1,15 +1,18 @@
|
|
|
|
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import os
|
|
|
|
from datetime import datetime, date, timedelta
|
|
|
|
from datetime import datetime, date
|
|
|
|
|
|
|
|
|
|
|
|
from jira import JIRA
|
|
|
|
from jira import JIRA
|
|
|
|
import requests
|
|
|
|
|
|
|
|
import logging
|
|
|
|
import logging
|
|
|
|
from bottle import template
|
|
|
|
from bottle import template
|
|
|
|
import re
|
|
|
|
import re
|
|
|
|
|
|
|
|
from six.moves import range
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, decommission_time_window=3):
|
|
|
|
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)
|
|
|
@ -29,26 +32,31 @@ class JiraAPI(object):
|
|
|
|
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"
|
|
|
|
self.max_time_tracking = max_time_window #in months
|
|
|
|
self.max_time_tracking = max_time_window # in months
|
|
|
|
if path:
|
|
|
|
if path:
|
|
|
|
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_decommission_time = decommission_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)
|
|
|
|
# [HIGIENE] close tickets older than 12 months as obsolete (max_time_window defined)
|
|
|
|
if clean_obsolete:
|
|
|
|
if clean_obsolete:
|
|
|
|
self.close_obsolete_tickets()
|
|
|
|
self.close_obsolete_tickets()
|
|
|
|
# deletes the tag "server_decommission" from those tickets closed <=3 months ago
|
|
|
|
# deletes the tag "server_decommission" from those tickets closed <=3 months ago
|
|
|
|
self.decommission_cleanup()
|
|
|
|
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).
|
|
|
|
self.jira_still_vulnerable_comment = '''This ticket has been reopened due to the vulnerability not having been \
|
|
|
|
- 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.
|
|
|
|
fixed (if multiple assets are affected, all need to be fixed; if the server is down, lastest known \
|
|
|
|
- If server has been decommissioned, please add the label "*server_decommission*" to the ticket before closing it.
|
|
|
|
vulnerability might be the one reported).
|
|
|
|
- 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.
|
|
|
|
- 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.'''
|
|
|
|
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']
|
|
|
|
for tag in tags:
|
|
|
|
for tag in tags:
|
|
|
|
labels.append(str(tag))
|
|
|
|
labels.append(str(tag))
|
|
|
@ -62,8 +70,8 @@ class JiraAPI(object):
|
|
|
|
for c in project_obj.components:
|
|
|
|
for c in project_obj.components:
|
|
|
|
if component == c.name:
|
|
|
|
if component == c.name:
|
|
|
|
self.logger.debug("resolved component name {} to id {}".format(c.name, c.id))
|
|
|
|
self.logger.debug("resolved component name {} to id {}".format(c.name, c.id))
|
|
|
|
components_ticket.append({ "id": c.id })
|
|
|
|
components_ticket.append({"id": c.id})
|
|
|
|
exists=True
|
|
|
|
exists = True
|
|
|
|
if not exists:
|
|
|
|
if not exists:
|
|
|
|
self.logger.error("Error creating Ticket: component {} not found".format(component))
|
|
|
|
self.logger.error("Error creating Ticket: component {} not found".format(component))
|
|
|
|
return 0
|
|
|
|
return 0
|
|
|
@ -82,7 +90,7 @@ class JiraAPI(object):
|
|
|
|
|
|
|
|
|
|
|
|
return new_issue
|
|
|
|
return new_issue
|
|
|
|
|
|
|
|
|
|
|
|
#Basic JIRA Metrics
|
|
|
|
# Basic JIRA Metrics
|
|
|
|
def metrics_open_tickets(self, project=None):
|
|
|
|
def metrics_open_tickets(self, project=None):
|
|
|
|
jql = "labels= vulnerability_management and resolution = Unresolved"
|
|
|
|
jql = "labels= vulnerability_management and resolution = Unresolved"
|
|
|
|
if project:
|
|
|
|
if project:
|
|
|
@ -91,13 +99,15 @@ class JiraAPI(object):
|
|
|
|
return len(self.jira.search_issues(jql, maxResults=0))
|
|
|
|
return len(self.jira.search_issues(jql, maxResults=0))
|
|
|
|
|
|
|
|
|
|
|
|
def metrics_closed_tickets(self, project=None):
|
|
|
|
def metrics_closed_tickets(self, project=None):
|
|
|
|
jql = "labels= vulnerability_management and NOT resolution = Unresolved AND created >=startOfMonth(-{})".format(self.max_time_tracking)
|
|
|
|
jql = "labels= vulnerability_management and NOT resolution = Unresolved AND created >=startOfMonth(-{})".format(
|
|
|
|
|
|
|
|
self.max_time_tracking)
|
|
|
|
if project:
|
|
|
|
if project:
|
|
|
|
jql += " and (project='{}')".format(project)
|
|
|
|
jql += " and (project='{}')".format(project)
|
|
|
|
return len(self.jira.search_issues(jql, maxResults=0))
|
|
|
|
return len(self.jira.search_issues(jql, maxResults=0))
|
|
|
|
|
|
|
|
|
|
|
|
def sync(self, vulnerabilities, project, components=[]):
|
|
|
|
def sync(self, vulnerabilities, project, components=[]):
|
|
|
|
#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")
|
|
|
|
|
|
|
|
|
|
|
|
for vuln in vulnerabilities:
|
|
|
|
for vuln in vulnerabilities:
|
|
|
@ -106,7 +116,8 @@ class JiraAPI(object):
|
|
|
|
if " " in vuln['scan_name']:
|
|
|
|
if " " in vuln['scan_name']:
|
|
|
|
vuln['scan_name'] = "_".join(vuln['scan_name'].split(" "))
|
|
|
|
vuln['scan_name'] = "_".join(vuln['scan_name'].split(" "))
|
|
|
|
|
|
|
|
|
|
|
|
# we exclude from the vulnerabilities to report those assets that already exist with *risk_accepted*/*server_decommission*
|
|
|
|
# we exclude from the vulnerabilities to report those assets that already exist
|
|
|
|
|
|
|
|
# with *risk_accepted*/*server_decommission*
|
|
|
|
vuln = self.exclude_accepted_assets(vuln)
|
|
|
|
vuln = self.exclude_accepted_assets(vuln)
|
|
|
|
|
|
|
|
|
|
|
|
# make sure after exclusion of risk_accepted assets there are still assets
|
|
|
|
# make sure after exclusion of risk_accepted assets there are still assets
|
|
|
@ -131,13 +142,17 @@ class JiraAPI(object):
|
|
|
|
# create local text file with assets, attach it to ticket
|
|
|
|
# create local text file with assets, attach it to ticket
|
|
|
|
if len(vuln['ips']) > self.max_ips_ticket:
|
|
|
|
if len(vuln['ips']) > self.max_ips_ticket:
|
|
|
|
attachment_contents = vuln['ips']
|
|
|
|
attachment_contents = vuln['ips']
|
|
|
|
vuln['ips'] = ["Affected hosts ({assets}) exceed Jira's allowed character limit, added as an attachment.".format(assets = len(attachment_contents))]
|
|
|
|
vuln['ips'] = [
|
|
|
|
|
|
|
|
"Affected hosts ({assets}) exceed Jira's allowed character limit, added as an attachment.".format(
|
|
|
|
|
|
|
|
assets=len(attachment_contents))]
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
tpl = template(self.template_path, vuln)
|
|
|
|
tpl = template(self.template_path, vuln)
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.error('Exception templating: {}'.format(str(e)))
|
|
|
|
self.logger.error('Exception templating: {}'.format(str(e)))
|
|
|
|
return 0
|
|
|
|
return 0
|
|
|
|
self.create_ticket(title=vuln['title'], desc=tpl, project=project, components=components, tags=[vuln['source'], vuln['scan_name'], 'vulnerability', vuln['risk']], attachment_contents = attachment_contents)
|
|
|
|
self.create_ticket(title=vuln['title'], desc=tpl, project=project, components=components,
|
|
|
|
|
|
|
|
tags=[vuln['source'], vuln['scan_name'], 'vulnerability', vuln['risk']],
|
|
|
|
|
|
|
|
attachment_contents=attachment_contents)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.logger.info("Ignoring vulnerability as all assets are already reported in a risk_accepted ticket")
|
|
|
|
self.logger.info("Ignoring vulnerability as all assets are already reported in a risk_accepted ticket")
|
|
|
|
|
|
|
|
|
|
|
@ -153,34 +168,39 @@ class JiraAPI(object):
|
|
|
|
labels = [vuln['source'], vuln['scan_name'], 'vulnerability_management', 'vulnerability']
|
|
|
|
labels = [vuln['source'], vuln['scan_name'], 'vulnerability_management', 'vulnerability']
|
|
|
|
|
|
|
|
|
|
|
|
if not self.excluded_tickets:
|
|
|
|
if not self.excluded_tickets:
|
|
|
|
jql = "{} AND labels in (risk_accepted,server_decommission, false_positive) AND NOT labels=advisory AND created >=startOfMonth(-{})".format(" AND ".join(["labels={}".format(label) for label in labels]), self.max_time_tracking)
|
|
|
|
jql = "{} AND labels in (risk_accepted,server_decommission, false_positive) AND NOT labels=advisory AND created >=startOfMonth(-{})".format(
|
|
|
|
|
|
|
|
" AND ".join(["labels={}".format(label) for label in labels]), self.max_time_tracking)
|
|
|
|
self.excluded_tickets = self.jira.search_issues(jql, maxResults=0)
|
|
|
|
self.excluded_tickets = self.jira.search_issues(jql, maxResults=0)
|
|
|
|
|
|
|
|
|
|
|
|
title = vuln['title']
|
|
|
|
title = vuln['title']
|
|
|
|
#WARNING: function IGNORES DUPLICATES, after finding a "duplicate" will just return it exists
|
|
|
|
# WARNING: function IGNORES DUPLICATES, after finding a "duplicate" will just return it exists
|
|
|
|
#it wont iterate over the rest of tickets looking for other possible duplicates/similar issues
|
|
|
|
# it wont iterate over the rest of tickets looking for other possible duplicates/similar issues
|
|
|
|
self.logger.info("Comparing vulnerability to risk_accepted tickets")
|
|
|
|
self.logger.info("Comparing vulnerability to risk_accepted tickets")
|
|
|
|
assets_to_exclude = []
|
|
|
|
assets_to_exclude = []
|
|
|
|
tickets_excluded_assets = []
|
|
|
|
tickets_excluded_assets = []
|
|
|
|
for index in range(len(self.excluded_tickets)):
|
|
|
|
for index in range(len(self.excluded_tickets)):
|
|
|
|
checking_ticketid, checking_title, checking_assets = self.ticket_get_unique_fields(self.excluded_tickets[index])
|
|
|
|
checking_ticketid, checking_title, checking_assets = self.ticket_get_unique_fields(
|
|
|
|
|
|
|
|
self.excluded_tickets[index])
|
|
|
|
if title.encode('ascii') == checking_title.encode('ascii'):
|
|
|
|
if title.encode('ascii') == checking_title.encode('ascii'):
|
|
|
|
if checking_assets:
|
|
|
|
if checking_assets:
|
|
|
|
#checking_assets is a list, we add to our full list for later delete all assets
|
|
|
|
# checking_assets is a list, we add to our full list for later delete all assets
|
|
|
|
assets_to_exclude+=checking_assets
|
|
|
|
assets_to_exclude += checking_assets
|
|
|
|
tickets_excluded_assets.append(checking_ticketid)
|
|
|
|
tickets_excluded_assets.append(checking_ticketid)
|
|
|
|
|
|
|
|
|
|
|
|
if assets_to_exclude:
|
|
|
|
if assets_to_exclude:
|
|
|
|
assets_to_remove = []
|
|
|
|
assets_to_remove = []
|
|
|
|
self.logger.warn("Vulnerable Assets seen on an already existing risk_accepted Jira ticket: {}".format(', '.join(tickets_excluded_assets)))
|
|
|
|
self.logger.warn("Vulnerable Assets seen on an already existing risk_accepted Jira ticket: {}".format(
|
|
|
|
|
|
|
|
', '.join(tickets_excluded_assets)))
|
|
|
|
self.logger.debug("Original assets: {}".format(vuln['ips']))
|
|
|
|
self.logger.debug("Original assets: {}".format(vuln['ips']))
|
|
|
|
#assets in vulnerability have the structure "ip - hostname - port", so we need to match by partial
|
|
|
|
# assets in vulnerability have the structure "ip - hostname - port", so we need to match by partial
|
|
|
|
for exclusion in assets_to_exclude:
|
|
|
|
for exclusion in assets_to_exclude:
|
|
|
|
# for efficiency, we walk the backwards the array of ips from the scanners, as we will be popping out the matches
|
|
|
|
# for efficiency, we walk the backwards the array of ips from the scanners, as we will be popping out the matches
|
|
|
|
# and we don't want it to affect the rest of the processing (otherwise, it would miss the asset right after the removed one)
|
|
|
|
# and we don't want it to affect the rest of the processing (otherwise, it would miss the asset right after the removed one)
|
|
|
|
for index in range(len(vuln['ips']))[::-1]:
|
|
|
|
for index in range(len(vuln['ips']))[::-1]:
|
|
|
|
if exclusion == vuln['ips'][index].split(" - ")[0]:
|
|
|
|
if exclusion == vuln['ips'][index].split(" - ")[0]:
|
|
|
|
self.logger.debug("Deleting asset {} from vulnerability {}, seen in risk_accepted.".format(vuln['ips'][index], title))
|
|
|
|
self.logger.debug(
|
|
|
|
|
|
|
|
"Deleting asset {} from vulnerability {}, seen in risk_accepted.".format(vuln['ips'][index],
|
|
|
|
|
|
|
|
title))
|
|
|
|
vuln['ips'].pop(index)
|
|
|
|
vuln['ips'].pop(index)
|
|
|
|
self.logger.debug("Modified assets: {}".format(vuln['ips']))
|
|
|
|
self.logger.debug("Modified assets: {}".format(vuln['ips']))
|
|
|
|
|
|
|
|
|
|
|
@ -192,35 +212,37 @@ class JiraAPI(object):
|
|
|
|
Returns [exists (bool), is equal (bool), ticketid (str), assets (array)]
|
|
|
|
Returns [exists (bool), is equal (bool), ticketid (str), assets (array)]
|
|
|
|
'''
|
|
|
|
'''
|
|
|
|
# we need to return if the vulnerability has already been reported and the ID of the ticket for further processing
|
|
|
|
# we need to return if the vulnerability has already been reported and the ID of the ticket for further processing
|
|
|
|
#function returns array [duplicated(bool), update(bool), ticketid, ticket_assets]
|
|
|
|
# function returns array [duplicated(bool), update(bool), ticketid, ticket_assets]
|
|
|
|
title = vuln['title']
|
|
|
|
title = vuln['title']
|
|
|
|
labels = [vuln['source'], vuln['scan_name'], 'vulnerability_management', 'vulnerability']
|
|
|
|
labels = [vuln['source'], vuln['scan_name'], 'vulnerability_management', 'vulnerability']
|
|
|
|
#list(set()) to remove duplicates
|
|
|
|
# list(set()) to remove duplicates
|
|
|
|
assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ",".join(vuln['ips']))))
|
|
|
|
assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ",".join(vuln['ips']))))
|
|
|
|
|
|
|
|
|
|
|
|
if not self.all_tickets:
|
|
|
|
if not self.all_tickets:
|
|
|
|
self.logger.info("Retrieving all JIRA tickets with the following tags {}".format(labels))
|
|
|
|
self.logger.info("Retrieving all JIRA tickets with the following tags {}".format(labels))
|
|
|
|
# we want to check all JIRA tickets, to include tickets moved to other queues
|
|
|
|
# we want to check all JIRA tickets, to include tickets moved to other queues
|
|
|
|
# will exclude tickets older than 12 months, old tickets will get closed for higiene and recreated if still vulnerable
|
|
|
|
# will exclude tickets older than 12 months, old tickets will get closed for higiene and recreated if still vulnerable
|
|
|
|
jql = "{} AND NOT labels=advisory AND created >=startOfMonth(-{})".format(" AND ".join(["labels={}".format(label) for label in labels]), self.max_time_tracking)
|
|
|
|
jql = "{} AND NOT labels=advisory AND created >=startOfMonth(-{})".format(
|
|
|
|
|
|
|
|
" AND ".join(["labels={}".format(label) for label in labels]), self.max_time_tracking)
|
|
|
|
|
|
|
|
|
|
|
|
self.all_tickets = self.jira.search_issues(jql, maxResults=0)
|
|
|
|
self.all_tickets = self.jira.search_issues(jql, maxResults=0)
|
|
|
|
|
|
|
|
|
|
|
|
#WARNING: function IGNORES DUPLICATES, after finding a "duplicate" will just return it exists
|
|
|
|
# WARNING: function IGNORES DUPLICATES, after finding a "duplicate" will just return it exists
|
|
|
|
#it wont iterate over the rest of tickets looking for other possible duplicates/similar issues
|
|
|
|
# it wont iterate over the rest of tickets looking for other possible duplicates/similar issues
|
|
|
|
self.logger.info("Comparing Vulnerabilities to created tickets")
|
|
|
|
self.logger.info("Comparing Vulnerabilities to created tickets")
|
|
|
|
for index in range(len(self.all_tickets)):
|
|
|
|
for index in range(len(self.all_tickets)):
|
|
|
|
checking_ticketid, checking_title, checking_assets = self.ticket_get_unique_fields(self.all_tickets[index])
|
|
|
|
checking_ticketid, checking_title, checking_assets = self.ticket_get_unique_fields(self.all_tickets[index])
|
|
|
|
# added "not risk_accepted", as if it is risk_accepted, we will create a new ticket excluding the accepted assets
|
|
|
|
# added "not risk_accepted", as if it is risk_accepted, we will create a new ticket excluding the accepted assets
|
|
|
|
if title.encode('ascii') == checking_title.encode('ascii') and not self.is_risk_accepted(self.jira.issue(checking_ticketid)):
|
|
|
|
if title.encode('ascii') == checking_title.encode('ascii') and not self.is_risk_accepted(
|
|
|
|
|
|
|
|
self.jira.issue(checking_ticketid)):
|
|
|
|
difference = list(set(assets).symmetric_difference(checking_assets))
|
|
|
|
difference = list(set(assets).symmetric_difference(checking_assets))
|
|
|
|
#to check intersection - set(assets) & set(checking_assets)
|
|
|
|
# to check intersection - set(assets) & set(checking_assets)
|
|
|
|
if difference:
|
|
|
|
if difference:
|
|
|
|
self.logger.info("Asset mismatch, ticket to update. Ticket ID: {}".format(checking_ticketid))
|
|
|
|
self.logger.info("Asset mismatch, ticket to update. Ticket ID: {}".format(checking_ticketid))
|
|
|
|
return False, True, checking_ticketid, checking_assets #this will automatically validate
|
|
|
|
return False, True, checking_ticketid, checking_assets # this will automatically validate
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.logger.info("Confirmed duplicated. TickedID: {}".format(checking_ticketid))
|
|
|
|
self.logger.info("Confirmed duplicated. TickedID: {}".format(checking_ticketid))
|
|
|
|
return True, False, checking_ticketid, [] #this will automatically validate
|
|
|
|
return True, False, checking_ticketid, [] # this will automatically validate
|
|
|
|
return False, False, "", []
|
|
|
|
return False, False, "", []
|
|
|
|
|
|
|
|
|
|
|
|
def ticket_get_unique_fields(self, ticket):
|
|
|
|
def ticket_get_unique_fields(self, ticket):
|
|
|
@ -229,19 +251,22 @@ class JiraAPI(object):
|
|
|
|
|
|
|
|
|
|
|
|
assets = self.get_assets_from_description(ticket)
|
|
|
|
assets = self.get_assets_from_description(ticket)
|
|
|
|
if not assets:
|
|
|
|
if not assets:
|
|
|
|
#check if attachment, if so, get assets from attachment
|
|
|
|
# check if attachment, if so, get assets from attachment
|
|
|
|
assets = self.get_assets_from_attachment(ticket)
|
|
|
|
assets = self.get_assets_from_attachment(ticket)
|
|
|
|
|
|
|
|
|
|
|
|
return ticketid, title, assets
|
|
|
|
return ticketid, title, assets
|
|
|
|
|
|
|
|
|
|
|
|
def get_assets_from_description(self, ticket, _raw = False):
|
|
|
|
def get_assets_from_description(self, ticket, _raw=False):
|
|
|
|
# Get the assets as a string "host - protocol/port - hostname" separated by "\n"
|
|
|
|
# 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
|
|
|
|
# structure the text to have the same structure as the assets from the attachment
|
|
|
|
affected_assets = ""
|
|
|
|
affected_assets = ""
|
|
|
|
try:
|
|
|
|
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)
|
|
|
|
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:
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.error("Unable to process the Ticket's 'Affected Assets'. Ticket ID: {}. Reason: {}".format(ticket, e))
|
|
|
|
self.logger.error(
|
|
|
|
|
|
|
|
"Unable to process the Ticket's 'Affected Assets'. Ticket ID: {}. Reason: {}".format(ticket, e))
|
|
|
|
|
|
|
|
|
|
|
|
if affected_assets:
|
|
|
|
if affected_assets:
|
|
|
|
if _raw:
|
|
|
|
if _raw:
|
|
|
@ -257,14 +282,14 @@ class JiraAPI(object):
|
|
|
|
self.logger.error("Ticket IPs regex failed. Ticket ID: {}. Reason: {}".format(ticket, e))
|
|
|
|
self.logger.error("Ticket IPs regex failed. Ticket ID: {}. Reason: {}".format(ticket, e))
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def get_assets_from_attachment(self, ticket, _raw = False):
|
|
|
|
def get_assets_from_attachment(self, ticket, _raw=False):
|
|
|
|
# Get the assets as a string "host - protocol/port - hostname" separated by "\n"
|
|
|
|
# Get the assets as a string "host - protocol/port - hostname" separated by "\n"
|
|
|
|
affected_assets = []
|
|
|
|
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 = ""
|
|
|
|
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 = ''
|
|
|
|
if attachments:
|
|
|
|
if attachments:
|
|
|
@ -280,7 +305,8 @@ class JiraAPI(object):
|
|
|
|
affected_assets = 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))
|
|
|
|
|
|
|
|
|
|
|
|
if affected_assets:
|
|
|
|
if affected_assets:
|
|
|
|
if _raw:
|
|
|
|
if _raw:
|
|
|
@ -326,15 +352,15 @@ class JiraAPI(object):
|
|
|
|
|
|
|
|
|
|
|
|
def add_content_as_attachment(self, issue, contents):
|
|
|
|
def add_content_as_attachment(self, issue, contents):
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
#Create the file locally with the data
|
|
|
|
# Create the file locally with the data
|
|
|
|
attachment_file = open(self.attachment_filename, "w")
|
|
|
|
attachment_file = open(self.attachment_filename, "w")
|
|
|
|
attachment_file.write("\n".join(contents))
|
|
|
|
attachment_file.write("\n".join(contents))
|
|
|
|
attachment_file.close()
|
|
|
|
attachment_file.close()
|
|
|
|
#Push the created file to the ticket
|
|
|
|
# Push the created file to the ticket
|
|
|
|
attachment_file = open(self.attachment_filename, "rb")
|
|
|
|
attachment_file = open(self.attachment_filename, "rb")
|
|
|
|
self.jira.add_attachment(issue, attachment_file, self.attachment_filename)
|
|
|
|
self.jira.add_attachment(issue, attachment_file, self.attachment_filename)
|
|
|
|
attachment_file.close()
|
|
|
|
attachment_file.close()
|
|
|
|
#remove the temp file
|
|
|
|
# remove the temp file
|
|
|
|
os.remove(self.attachment_filename)
|
|
|
|
os.remove(self.attachment_filename)
|
|
|
|
self.logger.info("Added attachment successfully.")
|
|
|
|
self.logger.info("Added attachment successfully.")
|
|
|
|
except:
|
|
|
|
except:
|
|
|
@ -344,21 +370,23 @@ class JiraAPI(object):
|
|
|
|
return True
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def get_ticket_reported_assets(self, ticket):
|
|
|
|
def get_ticket_reported_assets(self, ticket):
|
|
|
|
#[METRICS] return a list with all the affected assets for that vulnerability (including already resolved ones)
|
|
|
|
# [METRICS] return a list with all the affected assets for that vulnerability (including already resolved ones)
|
|
|
|
return list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b",str(self.jira.issue(ticket).raw))))
|
|
|
|
return list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", str(self.jira.issue(ticket).raw))))
|
|
|
|
|
|
|
|
|
|
|
|
def get_resolution_time(self, ticket):
|
|
|
|
def get_resolution_time(self, ticket):
|
|
|
|
#get time a ticket took to be resolved
|
|
|
|
# get time a ticket took to be resolved
|
|
|
|
ticket_obj = self.jira.issue(ticket)
|
|
|
|
ticket_obj = self.jira.issue(ticket)
|
|
|
|
if self.is_ticket_resolved(ticket_obj):
|
|
|
|
if self.is_ticket_resolved(ticket_obj):
|
|
|
|
ticket_data = ticket_obj.raw.get('fields')
|
|
|
|
ticket_data = ticket_obj.raw.get('fields')
|
|
|
|
#dates follow format '2018-11-06T10:36:13.849+0100'
|
|
|
|
# dates follow format '2018-11-06T10:36:13.849+0100'
|
|
|
|
created = [int(x) for x in ticket_data['created'].split('.')[0].replace('T', '-').replace(':','-').split('-')]
|
|
|
|
created = [int(x) for x in
|
|
|
|
resolved =[int(x) for x in ticket_data['resolutiondate'].split('.')[0].replace('T', '-').replace(':','-').split('-')]
|
|
|
|
ticket_data['created'].split('.')[0].replace('T', '-').replace(':', '-').split('-')]
|
|
|
|
|
|
|
|
resolved = [int(x) for x in
|
|
|
|
|
|
|
|
ticket_data['resolutiondate'].split('.')[0].replace('T', '-').replace(':', '-').split('-')]
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
@ -368,11 +396,11 @@ class JiraAPI(object):
|
|
|
|
# correct description will always be in the vulnerability to report, only needed to update description to new one
|
|
|
|
# correct description will always be in the vulnerability to report, only needed to update description to new one
|
|
|
|
self.logger.info("Ticket {} exists, UPDATE requested".format(ticketid))
|
|
|
|
self.logger.info("Ticket {} exists, UPDATE requested".format(ticketid))
|
|
|
|
|
|
|
|
|
|
|
|
#for now, if a vulnerability has been accepted ('accepted_risk'), ticket is completely ignored and not updated (no new assets)
|
|
|
|
# for now, if a vulnerability has been accepted ('accepted_risk'), ticket is completely ignored and not updated (no new assets)
|
|
|
|
|
|
|
|
|
|
|
|
#TODO when vulnerability accepted, create a new ticket with only the non-accepted vulnerable assets
|
|
|
|
# TODO when vulnerability accepted, create a new ticket with only the non-accepted vulnerable assets
|
|
|
|
#this would require go through the downloaded tickets, check duplicates/accepted ones, and if so,
|
|
|
|
# this would require go through the downloaded tickets, check duplicates/accepted ones, and if so,
|
|
|
|
#check on their assets to exclude them from the new ticket
|
|
|
|
# check on their assets to exclude them from the new ticket
|
|
|
|
risk_accepted = False
|
|
|
|
risk_accepted = False
|
|
|
|
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):
|
|
|
@ -380,7 +408,7 @@ class JiraAPI(object):
|
|
|
|
return 0
|
|
|
|
return 0
|
|
|
|
self.reopen_ticket(ticketid=ticketid, comment=self.jira_still_vulnerable_comment)
|
|
|
|
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()
|
|
|
|
assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ",".join(vuln['ips']))))
|
|
|
|
assets = list(set(re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", ",".join(vuln['ips']))))
|
|
|
|
difference = list(set(assets).symmetric_difference(ticket_assets))
|
|
|
|
difference = list(set(assets).symmetric_difference(ticket_assets))
|
|
|
@ -388,7 +416,7 @@ class JiraAPI(object):
|
|
|
|
comment = ''
|
|
|
|
comment = ''
|
|
|
|
added = ''
|
|
|
|
added = ''
|
|
|
|
removed = ''
|
|
|
|
removed = ''
|
|
|
|
#put a comment with the assets that have been added/removed
|
|
|
|
# put a comment with the assets that have been added/removed
|
|
|
|
for asset in difference:
|
|
|
|
for asset in difference:
|
|
|
|
if asset in assets:
|
|
|
|
if asset in assets:
|
|
|
|
if not added:
|
|
|
|
if not added:
|
|
|
@ -396,36 +424,39 @@ class JiraAPI(object):
|
|
|
|
added += '* {}\n'.format(asset)
|
|
|
|
added += '* {}\n'.format(asset)
|
|
|
|
elif asset in ticket_assets:
|
|
|
|
elif asset in ticket_assets:
|
|
|
|
if not removed:
|
|
|
|
if not removed:
|
|
|
|
removed= '\nThe following assets *have been resolved*:\n'
|
|
|
|
removed = '\nThe following assets *have been resolved*:\n'
|
|
|
|
removed += '* {}\n'.format(asset)
|
|
|
|
removed += '* {}\n'.format(asset)
|
|
|
|
|
|
|
|
|
|
|
|
comment = added + removed
|
|
|
|
comment = added + removed
|
|
|
|
|
|
|
|
|
|
|
|
#then will check if assets are too many that need to be added as an attachment
|
|
|
|
# then will check if assets are too many that need to be added as an attachment
|
|
|
|
attachment_contents = []
|
|
|
|
attachment_contents = []
|
|
|
|
if len(vuln['ips']) > self.max_ips_ticket:
|
|
|
|
if len(vuln['ips']) > self.max_ips_ticket:
|
|
|
|
attachment_contents = vuln['ips']
|
|
|
|
attachment_contents = vuln['ips']
|
|
|
|
vuln['ips'] = ["Affected hosts ({assets}) exceed Jira's allowed character limit, added as an attachment.".format(assets = len(attachment_contents))]
|
|
|
|
vuln['ips'] = [
|
|
|
|
|
|
|
|
"Affected hosts ({assets}) exceed Jira's allowed character limit, added as an attachment.".format(
|
|
|
|
|
|
|
|
assets=len(attachment_contents))]
|
|
|
|
|
|
|
|
|
|
|
|
#fill the ticket description template
|
|
|
|
# fill the ticket description template
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
tpl = template(self.template_path, vuln)
|
|
|
|
tpl = template(self.template_path, vuln)
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.error('Exception updating assets: {}'.format(str(e)))
|
|
|
|
self.logger.error('Exception updating assets: {}'.format(str(e)))
|
|
|
|
return 0
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
#proceed checking if it requires adding as an attachment
|
|
|
|
# proceed checking if it requires adding as an attachment
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
#update attachment with hosts and delete the old versions
|
|
|
|
# update attachment with hosts and delete the old versions
|
|
|
|
if attachment_contents:
|
|
|
|
if attachment_contents:
|
|
|
|
self.clean_old_attachments(ticket_obj)
|
|
|
|
self.clean_old_attachments(ticket_obj)
|
|
|
|
self.add_content_as_attachment(ticket_obj, attachment_contents)
|
|
|
|
self.add_content_as_attachment(ticket_obj, attachment_contents)
|
|
|
|
|
|
|
|
|
|
|
|
ticket_obj.update(description=tpl, comment=comment, fields={"labels":ticket_obj.fields.labels})
|
|
|
|
ticket_obj.update(description=tpl, comment=comment, fields={"labels": ticket_obj.fields.labels})
|
|
|
|
self.logger.info("Ticket {} updated successfully".format(ticketid))
|
|
|
|
self.logger.info("Ticket {} updated successfully".format(ticketid))
|
|
|
|
self.add_label(ticketid, 'updated')
|
|
|
|
self.add_label(ticketid, 'updated')
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.error("Error while trying up update ticket {ticketid}.\nReason: {e}".format(ticketid = ticketid, e=e))
|
|
|
|
self.logger.error(
|
|
|
|
|
|
|
|
"Error while trying up update ticket {ticketid}.\nReason: {e}".format(ticketid=ticketid, e=e))
|
|
|
|
return 0
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def add_label(self, ticketid, label):
|
|
|
|
def add_label(self, ticketid, label):
|
|
|
@ -435,10 +466,11 @@ class JiraAPI(object):
|
|
|
|
ticket_obj.fields.labels.append(label)
|
|
|
|
ticket_obj.fields.labels.append(label)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
ticket_obj.update(fields={"labels":ticket_obj.fields.labels})
|
|
|
|
ticket_obj.update(fields={"labels": ticket_obj.fields.labels})
|
|
|
|
self.logger.info("Added label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
|
|
|
|
self.logger.info("Added label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
|
|
|
|
except:
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.error("Error while trying to add label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
|
|
|
|
self.logger.error(
|
|
|
|
|
|
|
|
"Error while trying to add label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
|
|
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
@ -449,10 +481,11 @@ class JiraAPI(object):
|
|
|
|
ticket_obj.fields.labels.remove(label)
|
|
|
|
ticket_obj.fields.labels.remove(label)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
ticket_obj.update(fields={"labels":ticket_obj.fields.labels})
|
|
|
|
ticket_obj.update(fields={"labels": ticket_obj.fields.labels})
|
|
|
|
self.logger.info("Removed label {label} from ticket {ticket}".format(label=label, ticket=ticketid))
|
|
|
|
self.logger.info("Removed label {label} from ticket {ticket}".format(label=label, ticket=ticketid))
|
|
|
|
except:
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.error("Error while trying to remove label {label} to ticket {ticket}".format(label=label, ticket=ticketid))
|
|
|
|
self.logger.error("Error while trying to remove label {label} to ticket {ticket}".format(label=label,
|
|
|
|
|
|
|
|
ticket=ticketid))
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.logger.error("Error: label {label} not in ticket {ticket}".format(label=label, ticket=ticketid))
|
|
|
|
self.logger.error("Error: label {label} not in ticket {ticket}".format(label=label, ticket=ticketid))
|
|
|
|
|
|
|
|
|
|
|
@ -478,7 +511,6 @@ class JiraAPI(object):
|
|
|
|
self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment)
|
|
|
|
self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment)
|
|
|
|
return 0
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_ticket_reopenable(self, ticket_obj):
|
|
|
|
def is_ticket_reopenable(self, ticket_obj):
|
|
|
|
transitions = self.jira.transitions(ticket_obj)
|
|
|
|
transitions = self.jira.transitions(ticket_obj)
|
|
|
|
for transition in transitions:
|
|
|
|
for transition in transitions:
|
|
|
@ -497,7 +529,7 @@ class JiraAPI(object):
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def is_ticket_resolved(self, ticket_obj):
|
|
|
|
def is_ticket_resolved(self, ticket_obj):
|
|
|
|
#Checks if a ticket is resolved or not
|
|
|
|
# Checks if a ticket is resolved or not
|
|
|
|
if ticket_obj is not None:
|
|
|
|
if ticket_obj is not None:
|
|
|
|
if ticket_obj.raw['fields'].get('resolution') is not None:
|
|
|
|
if ticket_obj.raw['fields'].get('resolution') is not None:
|
|
|
|
if ticket_obj.raw['fields'].get('resolution').get('name') != 'Unresolved':
|
|
|
|
if ticket_obj.raw['fields'].get('resolution').get('name') != 'Unresolved':
|
|
|
@ -507,7 +539,6 @@ class JiraAPI(object):
|
|
|
|
self.logger.debug("Checked ticket {} is already open".format(ticket_obj))
|
|
|
|
self.logger.debug("Checked ticket {} is already open".format(ticket_obj))
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_risk_accepted(self, ticket_obj):
|
|
|
|
def is_risk_accepted(self, ticket_obj):
|
|
|
|
if ticket_obj is not None:
|
|
|
|
if ticket_obj is not None:
|
|
|
|
if ticket_obj.raw['fields'].get('labels') is not None:
|
|
|
|
if ticket_obj.raw['fields'].get('labels') is not None:
|
|
|
@ -533,7 +564,8 @@ class JiraAPI(object):
|
|
|
|
if (not self.is_risk_accepted(ticket_obj) or ignore_labels):
|
|
|
|
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):
|
|
|
|
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:
|
|
|
|
if not ignore_labels:
|
|
|
|
self.add_label(ticketid, 'reopened')
|
|
|
|
self.add_label(ticketid, 'reopened')
|
|
|
@ -551,9 +583,10 @@ class JiraAPI(object):
|
|
|
|
if not self.is_ticket_resolved(ticket_obj):
|
|
|
|
if not self.is_ticket_resolved(ticket_obj):
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
if self.is_ticket_closeable(ticket_obj):
|
|
|
|
if self.is_ticket_closeable(ticket_obj):
|
|
|
|
#need to add the label before closing the ticket
|
|
|
|
# need to add the label before closing the ticket
|
|
|
|
self.add_label(ticketid, 'closed')
|
|
|
|
self.add_label(ticketid, 'closed')
|
|
|
|
error = self.jira.transition_issue(issue=ticketid, transition=self.JIRA_CLOSE_ISSUE, comment = comment, resolution = {"name": resolution })
|
|
|
|
error = self.jira.transition_issue(issue=ticketid, transition=self.JIRA_CLOSE_ISSUE,
|
|
|
|
|
|
|
|
comment=comment, resolution={"name": resolution})
|
|
|
|
self.logger.info("Ticket {} closed successfully".format(ticketid))
|
|
|
|
self.logger.info("Ticket {} closed successfully".format(ticketid))
|
|
|
|
return 1
|
|
|
|
return 1
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
@ -566,7 +599,8 @@ 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 NOT labels=advisory 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.
|
|
|
@ -589,7 +623,7 @@ class JiraAPI(object):
|
|
|
|
'''
|
|
|
|
'''
|
|
|
|
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)
|
|
|
|
if os.path.isfile(fname):
|
|
|
|
if os.path.isfile(fname):
|
|
|
@ -597,10 +631,11 @@ 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 NOT labels=advisory 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
|
|
|
|
# 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
|
|
|
|
# for future processing in ELK/Splunk; this includes downloading attachments with assets and processing them
|
|
|
|
|
|
|
|
|
|
|
|
processed_tickets = []
|
|
|
|
processed_tickets = []
|
|
|
@ -621,14 +656,13 @@ class JiraAPI(object):
|
|
|
|
assets_json = self.parse_asset_to_json(assets)
|
|
|
|
assets_json = self.parse_asset_to_json(assets)
|
|
|
|
_metadata["affected_hosts"].append(assets_json)
|
|
|
|
_metadata["affected_hosts"].append(assets_json)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
temp_ticket = ticket.raw.get('fields')
|
|
|
|
temp_ticket = ticket.raw.get('fields')
|
|
|
|
temp_ticket['_metadata'] = _metadata
|
|
|
|
temp_ticket['_metadata'] = _metadata
|
|
|
|
|
|
|
|
|
|
|
|
processed_tickets.append(temp_ticket)
|
|
|
|
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:
|
|
|
|
outfile.writelines(to_save)
|
|
|
|
outfile.writelines(to_save)
|
|
|
|
self.logger.info("Tickets saved succesfully.")
|
|
|
|
self.logger.info("Tickets saved succesfully.")
|
|
|
@ -646,17 +680,20 @@ class JiraAPI(object):
|
|
|
|
closed already for more than x months (default is 3 months) in order to clean solved issues
|
|
|
|
closed already for more than x months (default is 3 months) in order to clean solved issues
|
|
|
|
for statistics purposes
|
|
|
|
for statistics purposes
|
|
|
|
'''
|
|
|
|
'''
|
|
|
|
self.logger.info("Deleting 'server_decommission' tag from tickets closed more than {} months ago".format(self.max_decommission_time))
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
decommissioned_tickets = self.jira.search_issues(jql, maxResults=0)
|
|
|
|
|
|
|
|
|
|
|
|
comment = '''This ticket is having deleted the *server_decommission* tag, as it is more than {} months old and is expected to already have been decommissioned.
|
|
|
|
comment = '''This ticket is having deleted the *server_decommission* tag, 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)
|
|
|
|
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:
|
|
|
|
for ticket in decommissioned_tickets:
|
|
|
|
#we open first the ticket, as we want to make sure the process is not blocked due to
|
|
|
|
# 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
|
|
|
|
# an unexisting jira workflow or unallowed edit from closed tickets
|
|
|
|
self.reopen_ticket(ticketid=ticket, ignore_labels=True)
|
|
|
|
self.reopen_ticket(ticketid=ticket, ignore_labels=True)
|
|
|
|
self.remove_label(ticket, 'server_decommission')
|
|
|
|
self.remove_label(ticket, 'server_decommission')
|
|
|
|
self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment)
|
|
|
|
self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment)
|
|
|
|