def fetch_gh_repo_branch_protection_details(self):
     """Fetch Github repository branch protection metadata."""
     branches = self.config.get(
         'org.auditree.repo_integrity.branches',
         {self.config.get('locker.repo_url'): ['master']})
     current_url = None
     github = None
     for repo_url, repo_branches in branches.items():
         parsed = urlparse(repo_url)
         base_url = f'{parsed.scheme}://{parsed.hostname}'
         repo = parsed.path.strip('/')
         for branch in repo_branches:
             file_prefix_parts = [
                 repo.lower().replace('/', '_').replace('-', '_'),
                 branch.lower().replace('-', '_')
             ]
             file_prefix = '_'.join(file_prefix_parts)
             path = ['auditree', f'gh_{file_prefix}_branch_protection.json']
             if base_url != current_url:
                 github = Github(self.config.creds, base_url)
                 current_url = base_url
             self.config.add_evidences([
                 RepoBranchProtectionEvidence(
                     path[1], path[0], DAY,
                     (f'Github branch protection for {repo} repo '
                      f'{branch} branch'))
             ])
             joined_path = os.path.join(*path)
             with raw_evidence(self.locker, joined_path) as evidence:
                 if evidence:
                     evidence.set_content(
                         json.dumps(
                             github.get_branch_protection_details(
                                 repo, branch)))
Example #2
0
 def fetch_workspaces(self):
     """Fetch Github repository Zenhub workspaces."""
     for config in self.configs:
         gh_host = config.get('github_host', GH_HOST_URL)
         zh_root = config.get('api_root', ZH_API_ROOT)
         repo = config['github_repo']
         repo_hash = get_sha256_hash([gh_host, repo], 10)
         fname = f'zh_repo_{repo_hash}_workspaces.json'
         self.config.add_evidences([
             RawEvidence(
                 fname, 'issues', DAY,
                 f'Zenhub workspaces for {gh_host}/{repo} repository')
         ])
         with raw_evidence(self.locker, f'issues/{fname}') as evidence:
             if evidence:
                 if gh_host not in self.gh_pool.keys():
                     self.gh_pool[gh_host] = Github(base_url=gh_host)
                 if zh_root not in self.zh_pool.keys():
                     self.zh_pool[zh_root] = BaseSession(zh_root)
                     service = 'zenhub'
                     if zh_root != ZH_API_ROOT:
                         service = 'zenhub_enterprise'
                     token = self.config.creds[service].token
                     self.zh_pool[zh_root].headers.update({
                         'Content-Type':
                         'application/json',
                         'X-Authentication-Token':
                         token
                     })
                 workspaces = self._get_workspaces(repo,
                                                   config.get('workspaces'),
                                                   gh_host, zh_root)
                 evidence.set_content(json.dumps(workspaces))
Example #3
0
 def fetch_gh_org_collaborators(self):
     """Fetch collaborators from GH organization repositories."""
     for config in self.config.get('org.permissions.org_integrity.orgs'):
         host, org = config['url'].rsplit('/', 1)
         for aff in config.get('collaborator_types', GH_ALL_COLLABORATORS):
             url_hash = get_sha256_hash([config['url']], 10)
             json_file = f'gh_{aff}_collaborators_{url_hash}.json'
             path = ['permissions', json_file]
             description = (
                 f'{aff.title()} collaborators of the {org} GH org')
             self.config.add_evidences(
                 [RawEvidence(path[1], path[0], DAY, description)])
             with raw_evidence(self.locker, '/'.join(path)) as evidence:
                 if evidence:
                     if host not in self.gh_pool:
                         self.gh_pool[host] = Github(base_url=host)
                     if not config.get('repos'):
                         repos = self.gh_pool[host].paginate_api(
                             f'orgs/{org}/repos')
                         config['repos'] = [repo['name'] for repo in repos]
                     collabs = {}
                     for repo in config['repos']:
                         collabs_url = f'repos/{org}/{repo}/collaborators'
                         collabs[repo] = self.gh_pool[host].paginate_api(
                             collabs_url, affiliation=aff)
                     evidence.set_content(json.dumps(collabs))
Example #4
0
 def fetch_gh_repo_branch_recent_commits_details(self):
     """Fetch Github repository branch recent commits metadata."""
     branches = self.config.get(
         'org.auditree.repo_integrity.branches',
         {self.config.get('locker.repo_url'): ['master']})
     current_url = None
     github = None
     for repo_url, repo_branches in branches.items():
         parsed = urlparse(repo_url)
         base_url = f'{parsed.scheme}://{parsed.hostname}'
         repo = parsed.path.strip('/')
         for branch in repo_branches:
             file_prefix_parts = [
                 repo.lower().replace('/', '_').replace('-', '_'),
                 branch.lower().replace('-', '_')
             ]
             file_prefix = '_'.join(file_prefix_parts)
             path = ['auditree', f'gh_{file_prefix}_recent_commits.json']
             if base_url != current_url:
                 github = Github(self.config.creds, base_url)
                 current_url = base_url
             ttl = DAY
             # To ensure signed commits check picks up locker commits
             if (repo_url == self.locker.repo_url
                     and branch == self.locker.branch):
                 ttl = DAY * 2
             self.config.add_evidences([
                 RepoCommitEvidence(
                     path[1], path[0], ttl,
                     (f'Github recent commits for {repo} repo '
                      f'{branch} branch'))
             ])
             joined_path = os.path.join(*path)
             with raw_evidence(self.locker, joined_path) as evidence:
                 if evidence:
                     meta = self.locker.get_evidence_metadata(evidence.path)
                     if meta is None:
                         meta = {}
                     now = datetime.utcnow().strftime(LOCKER_DTTM_FORMAT)
                     since = datetime.strptime(meta.get('last_update', now),
                                               LOCKER_DTTM_FORMAT)
                     evidence.set_content(
                         json.dumps(
                             github.get_commit_details(repo, since,
                                                       branch)))
 def fetch_gh_repo_branch_file_path_recent_commits_details(self):
     """Fetch Github repository branch file path recent commits metadata."""
     filepaths = self.config.get('org.auditree.repo_integrity.filepaths')
     current_url = None
     github = None
     for repo_url, repo_branches in filepaths.items():
         parsed = urlparse(repo_url)
         base_url = f'{parsed.scheme}://{parsed.hostname}'
         repo = parsed.path.strip('/')
         for branch, repo_filepaths in repo_branches.items():
             for filepath in repo_filepaths:
                 ev_file_prefix = f'{repo}_{branch}_{filepath}'.lower()
                 for symbol in [' ', '/', '-', '.']:
                     ev_file_prefix = ev_file_prefix.replace(symbol, '_')
                 path = [
                     'auditree', f'gh_{ev_file_prefix}_recent_commits.json'
                 ]
                 if base_url != current_url:
                     github = Github(self.config.creds, base_url)
                     current_url = base_url
                 self.config.add_evidences([
                     RepoCommitEvidence(
                         path[1], path[0], DAY,
                         (f'Github recent commits for {repo} repo '
                          f'{branch} branch, {filepath} file path'))
                 ])
                 joined_path = os.path.join(*path)
                 with raw_evidence(self.locker, joined_path) as evidence:
                     if evidence:
                         meta = self.locker.get_evidence_metadata(
                             evidence.path)
                         if meta is None:
                             meta = {}
                         utcnow = datetime.utcnow()
                         now = utcnow.strftime(LOCKER_DTTM_FORMAT)
                         since = datetime.strptime(
                             meta.get('last_update', now),
                             LOCKER_DTTM_FORMAT)
                         evidence.set_content(
                             json.dumps(
                                 github.get_commit_details(
                                     repo, since, branch, filepath)))
Example #6
0
    def __init__(self, results, controls, push_error=False):
        """
        Construct and initialize the Github notifier object.

        :param results: dictionary generated by
          :py:class:`compliance.runners.CheckMode` at the end of the execution.
        :param controls: the control descriptor that manages accreditations.
        """
        super(GHIssuesNotifier, self).__init__(results, controls, push_error)

        self._config = get_config().get('notify.gh_issues')
        if not self._config:
            # Ensure that legacy ghe_issues config still works
            self._config = get_config().get('notify.ghe_issues', {})
        # Using the locker repo url to define the base url.  The expectation
        # is that the Github issues repository will share the base url.
        parsed_locker_url = urlparse(get_config().get('locker.repo_url'))
        self._github = Github(
            get_config().creds,
            f'{parsed_locker_url.scheme}://{parsed_locker_url.hostname}')
 def fetch_gh_repo_details(self):
     """Fetch Github repository metadata."""
     repo_urls = self.config.get('org.auditree.repo_integrity.repos',
                                 [self.config.get('locker.repo_url')])
     current_url = None
     github = None
     for repo_url in repo_urls:
         parsed = urlparse(repo_url)
         base_url = f'{parsed.scheme}://{parsed.hostname}'
         repo = parsed.path.strip('/')
         file_prefix = repo.lower().replace('/', '_').replace('-', '_')
         path = ['auditree', f'gh_{file_prefix}_repo_metadata.json']
         if base_url != current_url:
             github = Github(self.config.creds, base_url)
             current_url = base_url
         self.config.add_evidences([
             RepoMetadataEvidence(path[1], path[0], DAY,
                                  f'Github {repo} repo metadata details')
         ])
         with raw_evidence(self.locker, os.path.join(*path)) as evidence:
             if evidence:
                 evidence.set_content(
                     json.dumps(github.get_repo_details(repo)))
 def fetch_issues(self):
     """Fetch Github repository issues."""
     for config in self.configs:
         host = config.get('host', GH_HOST_URL)
         repo = config['repo']
         fname = f'gh_repo_{get_sha256_hash([host, repo], 10)}_issues.json'
         self.config.add_evidences([
             RawEvidence(fname, 'issues', DAY,
                         f'Github issues for {host}/{repo} repository')
         ])
         with raw_evidence(self.locker, f'issues/{fname}') as evidence:
             if evidence:
                 if host not in self.gh_pool.keys():
                     self.gh_pool[host] = Github(base_url=host)
                 issues = []
                 for search in self._compose_searches(config, host):
                     issue_ids = [i['id'] for i in issues]
                     for result in self.gh_pool[host].search_issues(search):
                         if result['id'] not in issue_ids:
                             issues.append(result)
                 evidence.set_content(json.dumps(issues))
Example #9
0
class GHIssuesNotifier(_BaseMDNotifier):
    """
    Github notifier class.

    Notifications are sent to Github as repository issues.  This
    notifier is configurable via :class:`compliance.config.ComplianceConfig`.
    """
    def __init__(self, results, controls, push_error=False):
        """
        Construct and initialize the Github notifier object.

        :param results: dictionary generated by
          :py:class:`compliance.runners.CheckMode` at the end of the execution.
        :param controls: the control descriptor that manages accreditations.
        """
        super(GHIssuesNotifier, self).__init__(results, controls, push_error)

        self._config = get_config().get('notify.gh_issues')
        if not self._config:
            # Ensure that legacy ghe_issues config still works
            self._config = get_config().get('notify.ghe_issues', {})
        # Using the locker repo url to define the base url.  The expectation
        # is that the Github issues repository will share the base url.
        parsed_locker_url = urlparse(get_config().get('locker.repo_url'))
        self._github = Github(
            get_config().creds,
            f'{parsed_locker_url.scheme}://{parsed_locker_url.hostname}')

    def notify(self):
        """Send notifications to Github as repository issues."""
        self.logger.info('Running the Github Issues notifier...')
        if not self._config:
            self.logger.warning('Using Github Issues notifier without config')

        messages = list(self._messages_by_accreditations().items())
        messages.sort(key=lambda x: x[0])
        for accreditation, results in messages:
            if accreditation not in self._config:
                continue
            passed, failed, warned, errored = self._split_by_status(results)
            results_by_status = {
                'pass': passed,
                'fail': failed,
                'warn': warned,
                'error': errored
            }
            if self._config[accreditation].get('summary_issue'):
                self._notify_by_summary_issue(accreditation, results_by_status)
            elif self._push_error:
                self.logger.error('Remote locker push failed.  '
                                  'Github Issues notifier not triggered.')
            else:
                self._notify_by_check_issues(accreditation, results_by_status)

    def _notify_by_summary_issue(self, accred, results):
        issue = [self._generate_summary_issue(accred, results)]
        repos = self._config[accred].get('repo', [])
        for repo in repos:
            owner, repository = repo.split('/')
            issue_urls = self._process_new_alerts(
                owner, repository, issue,
                self._config[accred]['summary_issue'].get('message'))
            self._assign_projects(issue_urls, repo, accred)

    def _generate_summary_issue(self, accred, results):
        summary_config = self._config[accred]['summary_issue']
        title = summary_config['title']
        labels = summary_config.get('labels', [])
        assignees = summary_config.get('assignees', [])
        frequency = summary_config.get('frequency')
        rotation = summary_config.get('rotation')
        rotation_index = None
        now = datetime.utcnow()
        if frequency == 'day':
            today = now.strftime('%Y-%m-%d')
            title = f'{today} - {title}'
            labels.extend([frequency, today])
            rotation_index = now.timetuple().tm_yday
        elif frequency == 'week':
            year, week, _ = now.isocalendar()
            title = f'{year}, {week}W - {title}'
            labels.extend([frequency, str(year), f'{week}W'])
            rotation_index = week
        elif frequency == 'month':
            year = now.strftime('%Y')
            month = now.strftime('%mM')
            title = f'{year}, {month} - {title}'
            labels.extend([frequency, year, month])
            rotation_index = int(month[:-1])
        elif frequency == 'year':
            year = now.strftime('%Y')
            title = f'{year} - {title}'
            labels.extend([frequency, year])
            rotation_index = int(year)
        if rotation and rotation_index:
            assignees = rotation[divmod(rotation_index, len(rotation))[1]]
        issue = {'title': title, 'labels': labels, 'assignees': assignees}
        issue['body'] = '\n'.join(
            self._generate_accred_content(accred, results))
        return issue

    def _notify_by_check_issues(self, accred, results):
        issues = []
        statuses = self._config[accred].get('status', ['fail'])
        repos = self._config[accred].get('repo', [])
        for status, result in results.items():
            if status in statuses:
                issues += self._generate_issues(accred, result)
        for repo in repos:
            owner, repository = repo.split('/')
            issue_urls = self._process_new_alerts(owner, repository, issues)
            self._assign_projects(issue_urls, repo, accred)
        if 'pass' not in statuses:
            for repo in repos:
                owner, repository = repo.split('/')
                issues = self._generate_issues(accred, results['pass'])
                issue_urls = self._process_old_alerts(owner, repository,
                                                      issues)
                self._assign_projects(issue_urls, repo, accred)

    def _generate_issues(self, accred, results):
        issues = []
        if not results:
            return issues
        for check_path, result, message in results:
            now = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
            body = [f'## Compliance check alert - {now}']
            body.append(f'- Check: {check_path}')
            test_obj = result['test'].test
            check_name = check_path.rsplit('.', 1).pop()
            doc = getattr(test_obj.__class__, check_name).__doc__
            if doc:
                doc = doc.strip()
                newline = doc.find('\n')
                if newline > -1:
                    doc = doc[:newline]
                body.append(f'- Description: {doc}')
            body.append(f'- Accreditation: {accred}')
            status = ''.join(
                self._get_summary_and_body(
                    result,
                    message,
                    include_title=False,
                    summary_format='{status} ({issues})',
                    link_format='[{name}]({url})'))
            body.append(f'- Run Status: **{status}**')
            run_dttm = datetime.fromtimestamp(result['timestamp'])
            body.append(f'- Run Date/Time: {run_dttm}')
            report_links = self._get_report_links(
                result, link_format='[{name}]({url})')
            if report_links:
                body.append(f'- Reports: {", ".join(report_links)}')

            issue = {
                'title':
                message['title'],
                'body':
                '\n'.join(body),
                'labels': [
                    f'accreditation: {accred}',
                    f'run status: {result["status"]}'
                ]
            }
            issues.append(issue)

        return issues

    def _process_new_alerts(self, owner, repository, issues, message=None):
        issue_urls = {}
        for issue in issues:
            gh_issue = self._find_gh_issue('/'.join([owner, repository]),
                                           issue['title'])
            if gh_issue is None:
                body = issue['body']
                if message:
                    joined_msg = '\n'.join(message)
                    body = f'{joined_msg}\n\n{issue["body"]}'
                gh_issue = self._github.add_issue(owner,
                                                  repository,
                                                  issue['title'],
                                                  body,
                                                  labels=issue['labels'],
                                                  assignees=issue.get(
                                                      'assignees', []))
            else:
                self._update_issue_labels(owner, repository, gh_issue,
                                          issue['labels'])
                self._github.add_issue_comment(owner, repository,
                                               gh_issue['number'],
                                               issue['body'])
            issue_urls[gh_issue['id']] = gh_issue['url']
        return issue_urls

    def _process_old_alerts(self, owner, repository, issues):
        issue_urls = {}
        for issue in issues:
            gh_issue = self._find_gh_issue('/'.join([owner, repository]),
                                           issue['title'])
            if gh_issue:
                self._update_issue_labels(owner, repository, gh_issue,
                                          issue['labels'])
                self._github.add_issue_comment(owner, repository,
                                               gh_issue['number'],
                                               issue['body'])
                issue_urls[gh_issue['id']] = gh_issue['url']
        return issue_urls

    def _find_gh_issue(self, repo, title):
        gh_issues = self._github.search_issues(
            f'{title} type:issue in:title is:open repo:{repo}')
        found = None
        for issue in gh_issues:
            if issue['title'] == title:
                found = issue
                break
        return found

    def _update_issue_labels(self, owner, repository, issue, labels):
        current_labels = [label['name'] for label in issue['labels']]
        new_labels = list(set(labels) - set(current_labels))
        if new_labels:
            current_labels = [
                label for label in current_labels
                if not label.startswith('run status: ')
            ]
            self._github.patch_issue(owner,
                                     repository,
                                     issue['number'],
                                     labels=current_labels + new_labels)

    def _assign_projects(self, issues, repo, accred):
        config_projects = self._config[accred].get('project')
        if not config_projects:
            return
        all_projects = {
            p['name']: p['id']
            for p in self._github.get_all_projects(repo)
        }
        for project, column in config_projects.items():
            if project not in all_projects.keys():
                self.logger.warning(f'Project {project} not found in {repo}')
                continue
            columns = {
                c['name']: c['id']
                for c in self._github.get_columns(all_projects[project])
            }
            if column not in columns.keys():
                self.logger.warning(f'Column {column} not found '
                                    f'in {project} project, {repo} repo')
                continue
            card_lists = self._github.get_all_cards(columns.values()).values()
            issue_urls = [
                c.get('content_url') for cl in card_lists for c in cl
            ]
            for issue_id, issue_url in issues.items():
                if issue_url in issue_urls:
                    continue
                self._github.add_card(columns[column], issue=issue_id)