Exemplo n.º 1
0
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):
        self.logger = logging.getLogger('JiraAPI')
        if debug:
            self.logger.setLevel(logging.DEBUG)

        if "https://" not in hostname:
            hostname = "https://{}".format(hostname)
        self.username = username
        self.password = password
        self.jira = JIRA(options={'server': hostname},
                         basic_auth=(self.username, self.password))
        self.logger.info("Created vjira service for {}".format(hostname))
        self.all_tickets = []
        self.excluded_tickets = []
        self.JIRA_REOPEN_ISSUE = "Reopen Issue"
        self.JIRA_CLOSE_ISSUE = "Close Issue"
        self.JIRA_RESOLUTION_OBSOLETE = "Obsolete"
        self.JIRA_RESOLUTION_FIXED = "Fixed"
        self.template_path = 'vulnwhisp/reporting/resources/ticket.tpl'
        self.max_ips_ticket = 30
        self.attachment_filename = "vulnerable_assets.txt"
        self.max_time_tracking = max_time_window  #in months
        if path:
            self.download_tickets(path)
        else:
            self.logger.warn(
                "No local path specified, skipping Jira ticket download.")
        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=[]):
        labels = ['vulnerability_management']
        for tag in tags:
            labels.append(str(tag))

        self.logger.info("Creating ticket for project {} title: {}".format(
            project, title[:20]))
        self.logger.debug("project {} has a component requirement: {}".format(
            project, components))
        project_obj = self.jira.project(project)
        components_ticket = []
        for component in components:
            exists = False
            for c in project_obj.components:
                if component == c.name:
                    self.logger.debug(
                        "resolved component name {} to id {}".format(
                            c.name, c.id))
                    components_ticket.append({"id": c.id})
                    exists = True
            if not exists:
                self.logger.error(
                    "Error creating Ticket: component {} not found".format(
                        component))
                return 0

        new_issue = self.jira.create_issue(project=project,
                                           summary=title,
                                           description=desc,
                                           issuetype={'name': 'Bug'},
                                           labels=labels,
                                           components=components_ticket)

        self.logger.info("Ticket {} created successfully".format(new_issue))

        if attachment_contents:
            self.add_content_as_attachment(new_issue, attachment_contents)

        return new_issue

    #Basic JIRA Metrics
    def metrics_open_tickets(self, project=None):
        jql = "labels= vulnerability_management and resolution = Unresolved"
        if project:
            jql += " and (project='{}')".format(project)
        self.logger.debug('Executing: {}'.format(jql))
        return len(self.jira.search_issues(jql, maxResults=0))

    def metrics_closed_tickets(self, project=None):
        jql = "labels= vulnerability_management and NOT resolution = Unresolved AND created >=startOfMonth(-{})".format(
            self.max_time_tracking)
        if project:
            jql += " and (project='{}')".format(project)
        return len(self.jira.search_issues(jql, maxResults=0))

    def sync(self, vulnerabilities, project, components=[]):
        #JIRA structure of each vulnerability: [source, scan_name, title, diagnosis, consequence, solution, ips, risk, references]
        self.logger.info("JIRA Sync started")

        for vuln in vulnerabilities:
            # 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 " " in vuln['scan_name']:
                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*
            vuln = self.exclude_accepted_assets(vuln)

            # make sure after exclusion of risk_accepted assets there are still assets
            if vuln['ips']:
                exists = False
                to_update = False
                ticketid = ""
                ticket_assets = []
                exists, to_update, ticketid, ticket_assets = self.check_vuln_already_exists(
                    vuln)

                if exists:
                    # If ticket "resolved" -> reopen, as vulnerability is still existent
                    self.reopen_ticket(
                        ticketid=ticketid,
                        comment=self.jira_still_vulnerable_comment)
                    self.add_label(ticketid, vuln['risk'])
                    continue
                elif to_update:
                    self.ticket_update_assets(vuln, ticketid, ticket_assets)
                    self.add_label(ticketid, vuln['risk'])
                    continue
                attachment_contents = []
                # if assets >30, add as attachment
                # create local text file with assets, attach it to ticket
                if len(vuln['ips']) > self.max_ips_ticket:
                    attachment_contents = vuln['ips']
                    vuln['ips'] = [
                        "Affected hosts ({assets}) exceed Jira's allowed character limit, added as an attachment."
                        .format(assets=len(attachment_contents))
                    ]
                try:
                    tpl = template(self.template_path, vuln)
                except Exception as e:
                    self.logger.error('Exception templating: {}'.format(
                        str(e)))
                    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)
            else:
                self.logger.info(
                    "Ignoring vulnerability as all assets are already reported in a risk_accepted ticket"
                )

        self.close_fixed_tickets(vulnerabilities)
        # we reinitialize so the next sync redoes the query with their specific variables
        self.all_tickets = []
        self.excluded_tickets = []
        return True

    def exclude_accepted_assets(self, vuln):
        # we want to check JIRA tickets with risk_accepted/server_decommission or false_positive labels sharing the same source
        # will exclude tickets older than 12 months, old tickets will get closed for higiene and recreated if still vulnerable
        labels = [
            vuln['source'], vuln['scan_name'], 'vulnerability_management',
            'vulnerability'
        ]

        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)
            self.excluded_tickets = self.jira.search_issues(jql, maxResults=0)

        title = vuln['title']
        #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
        self.logger.info("Comparing vulnerability to risk_accepted tickets")
        assets_to_exclude = []
        tickets_excluded_assets = []
        for index in range(len(self.excluded_tickets)):
            checking_ticketid, checking_title, checking_assets = self.ticket_get_unique_fields(
                self.excluded_tickets[index])
            if title.encode('ascii') == checking_title.encode('ascii'):
                if checking_assets:
                    #checking_assets is a list, we add to our full list for later delete all assets
                    assets_to_exclude += checking_assets
                    tickets_excluded_assets.append(checking_ticketid)

        if assets_to_exclude:
            assets_to_remove = []
            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']))
            #assets in vulnerability have the structure "ip - hostname - port", so we need to match by partial
            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
                # 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]:
                    if exclusion == vuln['ips'][index].split(" - ")[0]:
                        self.logger.debug(
                            "Deleting asset {} from vulnerability {}, seen in risk_accepted."
                            .format(vuln['ips'][index], title))
                        vuln['ips'].pop(index)
            self.logger.debug("Modified assets: {}".format(vuln['ips']))

        return vuln

    def check_vuln_already_exists(self, vuln):
        '''
        This function compares a vulnerability with a collection of tickets.
        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
        #function returns array [duplicated(bool), update(bool), ticketid, ticket_assets]
        title = vuln['title']
        labels = [
            vuln['source'], vuln['scan_name'], 'vulnerability_management',
            'vulnerability'
        ]
        #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']))))

        if not self.all_tickets:
            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
            # 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)

            self.all_tickets = self.jira.search_issues(jql, maxResults=0)

        #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
        self.logger.info("Comparing Vulnerabilities to created tickets")
        for index in range(len(self.all_tickets)):
            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
            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))
                #to check intersection - set(assets) & set(checking_assets)
                if difference:
                    self.logger.info(
                        "Asset mismatch, ticket to update. Ticket ID: {}".
                        format(checking_ticketid))
                    return False, True, checking_ticketid, checking_assets  #this will automatically validate
                else:
                    self.logger.info(
                        "Confirmed duplicated. TickedID: {}".format(
                            checking_ticketid))
                    return True, False, checking_ticketid, [
                    ]  #this will automatically validate
        return False, False, "", []

    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))

        return ticketid, title, assets

    def check_ips_attachment(self, ticket):
        affected_assets_section = []
        try:
            fields = self.jira.issue(ticket.key).raw.get('fields', {})
            attachments = fields.get('attachment', {})
            affected_assets_section = ""
            #we will make sure we get the latest version of the file
            latest = ''
            attachment_id = ''
            if attachments:
                for item in attachments:
                    if item.get('filename') == self.attachment_filename:
                        if not latest:
                            latest = item.get('created')
                            attachment_id = item.get('id')
                        else:
                            if latest < item.get('created'):
                                latest = item.get('created')
                                attachment_id = item.get('id')
            affected_assets_section = 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

    def clean_old_attachments(self, ticket):
        fields = ticket.raw.get('fields')
        attachments = fields.get('attachment')
        if attachments:
            for item in attachments:
                if item.get('filename') == self.attachment_filename:
                    self.jira.delete_attachment(item.get('id'))

    def add_content_as_attachment(self, issue, contents):
        try:
            #Create the file locally with the data
            attachment_file = open(self.attachment_filename, "w")
            attachment_file.write("\n".join(contents))
            attachment_file.close()
            #Push the created file to the ticket
            attachment_file = open(self.attachment_filename, "rb")
            self.jira.add_attachment(issue, attachment_file,
                                     self.attachment_filename)
            attachment_file.close()
            #remove the temp file
            os.remove(self.attachment_filename)
            self.logger.info("Added attachment successfully.")
        except:
            self.logger.error("Error while attaching file to ticket.")
            return False

        return True

    def get_ticket_reported_assets(self, ticket):
        #[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))))

    def get_resolution_time(self, ticket):
        #get time a ticket took to be resolved
        ticket_obj = self.jira.issue(ticket)
        if self.is_ticket_resolved(ticket_obj):
            ticket_data = ticket_obj.raw.get('fields')
            #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('-')
            ]
            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])
            end = datetime(resolved[0], resolved[1], resolved[2], resolved[3],
                           resolved[4], resolved[5])
            return (end - start).days
        else:
            self.logger.error(
                "Ticket {ticket} is not resolved, can't calculate resolution time"
                .format(ticket=ticket))

        return False

    def ticket_update_assets(self, vuln, ticketid, ticket_assets):
        # 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))

        #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
        #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
        risk_accepted = False
        ticket_obj = self.jira.issue(ticketid)
        if self.is_ticket_resolved(ticket_obj):
            if self.is_risk_accepted(ticket_obj):
                return 0
            self.reopen_ticket(ticketid=ticketid,
                               comment=self.jira_still_vulnerable_comment)

        #First will do the comparison of assets
        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']))))
        difference = list(set(assets).symmetric_difference(ticket_assets))

        comment = ''
        added = ''
        removed = ''
        #put a comment with the assets that have been added/removed
        for asset in difference:
            if asset in assets:
                if not added:
                    added = '\nThe following assets *have been newly detected*:\n'
                added += '* {}\n'.format(asset)
            elif asset in ticket_assets:
                if not removed:
                    removed = '\nThe following assets *have been resolved*:\n'
                removed += '* {}\n'.format(asset)

        comment = added + removed

        #then will check if assets are too many that need to be added as an attachment
        attachment_contents = []
        if len(vuln['ips']) > self.max_ips_ticket:
            attachment_contents = vuln['ips']
            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
        try:
            tpl = template(self.template_path, vuln)
        except Exception as e:
            self.logger.error('Exception updating assets: {}'.format(str(e)))
            return 0

        #proceed checking if it requires adding as an attachment
        try:
            #update attachment with hosts and delete the old versions
            if attachment_contents:
                self.clean_old_attachments(ticket_obj)
                self.add_content_as_attachment(ticket_obj, attachment_contents)

            ticket_obj.update(description=tpl,
                              comment=comment,
                              fields={"labels": ticket_obj.fields.labels})
            self.logger.info("Ticket {} updated successfully".format(ticketid))
            self.add_label(ticketid, 'updated')
        except Exception as e:
            self.logger.error(
                "Error while trying up update ticket {ticketid}.\nReason: {e}".
                format(ticketid=ticketid, e=e))
        return 0

    def add_label(self, ticketid, label):
        ticket_obj = self.jira.issue(ticketid)

        if label not in [x.encode('utf8') for x in ticket_obj.fields.labels]:
            ticket_obj.fields.labels.append(label)

            try:
                ticket_obj.update(fields={"labels": ticket_obj.fields.labels})
                self.logger.info(
                    "Added label {label} to ticket {ticket}".format(
                        label=label, ticket=ticketid))
            except:
                self.logger.error(
                    "Error while trying to add label {label} to ticket {ticket}"
                    .format(label=label, ticket=ticketid))

        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):
        '''
        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 = []
        for vuln in vulnerabilities:
            found_vulns.append(vuln['title'])

        comment = '''This ticket is being closed as it appears that the vulnerability no longer exists.
        If the vulnerability reappears, a new ticket will be opened.'''

        for ticket in self.all_tickets:
            if ticket.raw['fields']['summary'].strip() in found_vulns:
                self.logger.info(
                    "Ticket {} is still vulnerable".format(ticket))
                continue
            self.logger.info(
                "Ticket {} is no longer vulnerable".format(ticket))
            self.close_ticket(ticket, self.JIRA_RESOLUTION_FIXED, comment)
        return 0

    def is_ticket_reopenable(self, ticket_obj):
        transitions = self.jira.transitions(ticket_obj)
        for transition in transitions:
            if transition.get('name') == self.JIRA_REOPEN_ISSUE:
                self.logger.debug("Ticket is reopenable")
                return True
        self.logger.warn("Ticket can't be opened. Check Jira transitions.")
        return False

    def is_ticket_closeable(self, ticket_obj):
        transitions = self.jira.transitions(ticket_obj)
        for transition in transitions:
            if transition.get('name') == self.JIRA_CLOSE_ISSUE:
                return True
        self.logger.warn("Ticket can't closed. Check Jira transitions.")
        return False

    def is_ticket_resolved(self, ticket_obj):
        #Checks if a ticket is resolved or not
        if ticket_obj is not None:
            if ticket_obj.raw['fields'].get('resolution') is not None:
                if ticket_obj.raw['fields'].get('resolution').get(
                        'name') != 'Unresolved':
                    self.logger.debug(
                        "Checked ticket {} is already closed".format(
                            ticket_obj))
                    self.logger.info("Ticket {} is closed".format(ticket_obj))
                    return True
        self.logger.debug(
            "Checked ticket {} is already open".format(ticket_obj))
        return False

    def is_risk_accepted(self, ticket_obj):
        if ticket_obj is not None:
            if ticket_obj.raw['fields'].get('labels') is not None:
                labels = ticket_obj.raw['fields'].get('labels')
                if "risk_accepted" in labels:
                    self.logger.warn(
                        "Ticket {} accepted risk, will be ignored".format(
                            ticket_obj))
                    return True
                elif "server_decommission" in labels:
                    self.logger.warn(
                        "Ticket {} server decommissioned, will be ignored".
                        format(ticket_obj))
                    return True
                elif "false_positive" in labels:
                    self.logger.warn(
                        "Ticket {} flagged false positive, will be ignored".
                        format(ticket_obj))
                    return True
        self.logger.info(
            "Ticket {} risk has not been accepted".format(ticket_obj))
        return False

    def reopen_ticket(self, ticketid, ignore_labels=False, comment=""):
        self.logger.debug(
            "Ticket {} exists, REOPEN requested".format(ticketid))
        # this will reopen a ticket by ticketid
        ticket_obj = self.jira.issue(ticketid)

        if self.is_ticket_resolved(ticket_obj):
            if (not self.is_risk_accepted(ticket_obj) or ignore_labels):
                try:
                    if self.is_ticket_reopenable(ticket_obj):
                        error = self.jira.transition_issue(
                            issue=ticketid,
                            transition=self.JIRA_REOPEN_ISSUE,
                            comment=comment)
                        self.logger.info(
                            "Ticket {} reopened successfully".format(ticketid))
                        if not ignore_labels:
                            self.add_label(ticketid, 'reopened')
                        return 1
                except Exception as e:
                    # continue with ticket data so that a new ticket is created in place of the "lost" one
                    self.logger.error("error reopening ticket {}: {}".format(
                        ticketid, e))
                    return 0
        return 0

    def close_ticket(self, ticketid, resolution, comment):
        # this will close a ticket by ticketid
        self.logger.debug("Ticket {} exists, CLOSE requested".format(ticketid))
        ticket_obj = self.jira.issue(ticketid)
        if not self.is_ticket_resolved(ticket_obj):
            try:
                if self.is_ticket_closeable(ticket_obj):
                    #need to add the label before closing the ticket
                    self.add_label(ticketid, 'closed')
                    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))
                    return 1
            except Exception as e:
                # continue with ticket data so that a new ticket is created in place of the "lost" one
                self.logger.error("error closing ticket {}: {}".format(
                    ticketid, e))
                return 0

        return 0

    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 <startOfMonth(-{}) and resolution=Unresolved".format(
            self.max_time_tracking)
        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.
        If the vulnerability still exists, a new ticket will be opened.'''.format(
            self.max_time_tracking)

        for ticket in tickets_to_close:
            self.close_ticket(ticket, self.JIRA_RESOLUTION_OBSOLETE, comment)

        return 0

    def project_exists(self, project):
        try:
            self.jira.project(project)
            return True
        except:
            return False
        return False

    def download_tickets(self, path):
        '''
        saves all tickets locally, local snapshot of vulnerability_management ticktes
        '''
        #check if file already exists
        check_date = str(date.today())
        fname = '{}jira_{}.json'.format(path, check_date)
        if os.path.isfile(fname):
            self.logger.info(
                "File {} already exists, skipping ticket download".format(
                    fname))
            return True
        try:
            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)
            tickets_data = self.jira.search_issues(jql, maxResults=0)

            #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
            ]
            with open(fname, 'w') as outfile:
                outfile.writelines(to_save)
                self.logger.info("Tickets saved succesfully.")

            return True

        except Exception as e:
            self.logger.error(
                "Tickets could not be saved locally: {}.".format(e))

        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, 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
Exemplo n.º 2
0
 #If we do find it, update the csv file and description
 for issue in issuesList:
     if (issue.raw['fields']["summary"] == group
             and (scanName in issue.raw['fields']['labels'])
             and ("Tenable" in issue.raw['fields']['labels'])):
         if (issue.raw['fields']["customfield_10202"] ==
                 creation_date):
             print(
                 group,
                 "already exists and has latest entry. Skipping.")
             created = True
             break
         print(group, "is outdated. Updating ticket...")
         issueObj = jira.issue(issue.key)
         for attachment in issueObj.fields.attachment:
             jira.delete_attachment(attachment.id)
         issueObj.update(
             notify=False,
             description=(str(count) + " unique problems as of " +
                          creation_date))
         issueObj.update(
             notify=False,
             fields={"customfield_10202": creation_date})
         jira.add_attachment(issueObj,
                             filename,
                             filename=group + ".csv")
         ticketCount += 1
         created = True
         break
 blockNum += 1
 if created == True: