From ed3cdfc7eca3c44591e606178aff00744d3e4e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Lome=C3=B1a?= <73544334+mlomena@users.noreply.github.com> Date: Mon, 15 Feb 2021 18:53:31 +0100 Subject: [PATCH] Add organization direct collaborators check (#47) * Add organization direct collaborators check --- CHANGES.md | 4 + arboretum/__init__.py | 2 +- arboretum/permissions/README.md | 67 +++++++++- .../checks/test_org_collaborators.py | 114 ++++++++++++++++++ .../permissions/org_collaborators.md.tmpl | 59 +++++++++ 5 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 arboretum/permissions/checks/test_org_collaborators.py create mode 100644 arboretum/permissions/templates/reports/permissions/org_collaborators.md.tmpl diff --git a/CHANGES.md b/CHANGES.md index cae547a0..057cebdc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +# [0.10.0](https://github.com/ComplianceAsCode/auditree-arboretum/releases/tag/v0.10.0) + +- [ADDED] Organization repository direct collaborators check added to `permissions`. + # [0.9.0](https://github.com/ComplianceAsCode/auditree-arboretum/releases/tag/v0.9.0) - [ADDED] Kubernetes resources fetcher added. diff --git a/arboretum/__init__.py b/arboretum/__init__.py index 359e81a3..ccd2e172 100644 --- a/arboretum/__init__.py +++ b/arboretum/__init__.py @@ -14,4 +14,4 @@ # limitations under the License. """Arboretum - Checking your compliance & security posture, continuously.""" -__version__ = '0.9.0' +__version__ = '0.10.0' diff --git a/arboretum/permissions/README.md b/arboretum/permissions/README.md index 0a571344..b6b3253c 100644 --- a/arboretum/permissions/README.md +++ b/arboretum/permissions/README.md @@ -97,10 +97,73 @@ day. ## Checks -Checks coming soon... +### Organization Integrity (Repository Collaborators) + +* Class: [OrgCollaboratorsCheck][org-collaborators-check] +* Purpose: Ensure that `direct` collaborators do not exist in the organization repositories. +* Behavior: Collaborators are checked for every repository. +A failure is generated when direct collaborators are found in a repository. This check can be optionally +configured to accept exceptions, a warning instead of a failure is generated for those exceptions when +direct collaborators matching the exceptions are found. +* Evidence depended upon: + * `direct` collaborators found in organization repositories. + * `raw/permissions/_direct_collaborators_.json` + * NOTE: Only gh (Github) is currently supported by this check. Gitlab and Bitbucket support coming soon... +* Configuration elements: + * `org.permissions.org_integrity.orgs` + * Required + * List of dictionaries: + * `url` + * Required + * Organization URL (string). + * `exceptions` + * Optional + * List of dictionaries: + * `user` + * Required + * Github, Gitlab or Bitbucket user id (string). + * Use to define the user to be treated as an exception. + * NOTE: Only Github is currently supported by this check. Gitlab and Bitbucket support coming soon... + * `repos` + * Optional + * List of strings in the form of `["repo_a", "repo_b"]`. + * Defaults to all repositories in the organization. + * Use to limit the user exception to specific repositories in the organization. +* Example configuration: + + ```json + { + "org": { + "permissions": { + "org_integrity": { + "orgs": [ + { + "url": "https://github.com/my-org-1", + "collaborator_types": ["direct"], + "exceptions": [ + { + "user": "userid_1" + }, + { + "user": "userid_2", + "repos": ["repo_a", "repo_b"] + } + ] + } + ] + } + } + } + } + ``` +* Import statement: + ```python + from arboretum.permissions.checks.test_org_collaborators import OrgCollaboratorsCheck + ``` [auditree-framework]: https://github.com/ComplianceAsCode/auditree-framework [auditree-framework documentation]: https://complianceascode.github.io/auditree-framework/ [usage]: https://github.com/ComplianceAsCode/auditree-arboretum#usage [gh-org-fetcher]: https://github.com/ComplianceAsCode/auditree-arboretum/blob/main/arboretum/permissions/fetchers/github/fetch_org_collaborators.py -[repository-permissions]: https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization \ No newline at end of file +[repository-permissions]: https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization +[org-collaborators-check]: https://github.com/ComplianceAsCode/auditree-arboretum/blob/main/arboretum/permissions/checks/test_org_collaborators.py diff --git a/arboretum/permissions/checks/test_org_collaborators.py b/arboretum/permissions/checks/test_org_collaborators.py new file mode 100644 index 00000000..ca31400f --- /dev/null +++ b/arboretum/permissions/checks/test_org_collaborators.py @@ -0,0 +1,114 @@ +# -*- mode:python; coding:utf-8 -*- +# Copyright (c) 2021 IBM Corp. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Repository organization/owner collaborators check.""" + +from compliance.check import ComplianceCheck +from compliance.evidence import DAY, ReportEvidence, evidences +from compliance.utils.data_parse import get_sha256_hash + + +class OrgCollaboratorsCheck(ComplianceCheck): + """Checks for repository organization/owner collaborators.""" + + @property + def title(self): + """ + Return the title of the checks. + + :returns: the title of the checks + """ + return 'Repository Organization/Owner Collaborators' + + @classmethod + def setUpClass(cls): + """Initialize the check object with configuration settings.""" + cls.config.add_evidences( + [ + ReportEvidence( + 'org_collaborators.md', + 'permissions', + DAY, + 'Repository organization collaborators report.' + ) + ] + ) + return cls + + def test_org_direct_collaborators(self): + """Check that there are no direct collaborators in the org repos.""" + orgs = self.config.get('org.permissions.org_integrity.orgs') + evidence_paths = {} + exceptions = {} + for org in orgs: + if 'direct' not in org.get('collaborator_types', []): + continue + host, org_name = org['url'].rsplit('/', 1) + service = 'gh' + if 'gitlab' in host: + service = 'gl' + elif 'bitbucket' in host: + service = 'bb' + + url_hash = get_sha256_hash([org['url']], 10) + filename = f'{service}_direct_collaborators_{url_hash}.json' + path = f'raw/permissions/{filename}' + evidence_paths[org_name] = path + exceptions[org_name] = org.get('exceptions', []) + with evidences(self, evidence_paths) as raws: + self._generate_results(raws, exceptions) + + def _generate_results(self, evidences, exceptions): + for org, ev in evidences.items(): + for repo in ev.content_as_json: + all_users = [u['login'] for u in ev.content_as_json[repo]] + if not all_users: + continue + exception_users = [ + e['user'] + for e in exceptions[org] + if 'repos' not in e.keys() or repo in e['repos'] + ] + failed_users = set(all_users) - set(exception_users) + warning_users = set(all_users).intersection(exception_users) + if failed_users: + self.add_failures( + 'unexpected-org-collaborators', { + 'org': org, 'repo': repo, 'users': failed_users + } + ) + if warning_users: + self.add_warnings( + 'allowed-org-collaborators', { + 'org': org, 'repo': repo, 'users': warning_users + } + ) + + def get_notification_message(self): + """ + Repository organization collaborators check notifier. + + :returns: notification dictionary + """ + return { + 'subtitle': 'Repository organization collaborators', 'body': None + } + + def get_reports(self): + """ + Provide the check report name. + + :returns: the report(s) generated for this check + """ + return ['permissions/org_collaborators.md'] diff --git a/arboretum/permissions/templates/reports/permissions/org_collaborators.md.tmpl b/arboretum/permissions/templates/reports/permissions/org_collaborators.md.tmpl new file mode 100644 index 00000000..321f9f2e --- /dev/null +++ b/arboretum/permissions/templates/reports/permissions/org_collaborators.md.tmpl @@ -0,0 +1,59 @@ +{#- -*- mode:jinja2; coding: utf-8 -*- -#} +# {{ test.title }} for {{ now.strftime('%Y-%m-%d') }} + +This report flags direct collaborator access found in the organization repositories. + +Organizations and repositories checked: + +| Organization | Repositories | +| ---------------------- | ------------------------- | +{%- for org in test.config.get('org.permissions.org_integrity.orgs') -%} +{%- if 'collaborator_types' in org.keys() and 'direct' in org['collaborator_types'] %} +| {{ org['url'].rsplit('/', 1)[1] }} | {{ ', '.join(org['repos']) if 'repos' in org.keys() else 'All repositories' }} | +{%- endif %} +{%- endfor %} + +It issues failures when: + +- Users have organization repository access as direct collaborators. + +It issues warnings when: + +- Direct collaborators are found but are listed as exceptions in the configuration. + +If a failure or warning is not present for the above category of check this +means that the check passed without issue. + +{% if (all_failures | length == 0) and (all_warnings | length == 0) %} +No issues found. +{%- else -%} + +{% for category, failures in all_failures.items() %} + +{% if category == 'unexpected-org-collaborators' %} +## Failure: Users found in organizations as direct repo collaborators + +| User | Organization | Repository | +| ---------------------- | ------------------------- | ------------------------- | +{%- for failure in failures %} +{%- for user in failure['users'] %} +| {{ user }} | {{ failure['org'] }} | {{ failure['repo'] }} | +{%- endfor -%} +{%- endfor -%} +{%- endif -%} +{%- endfor -%} + +{% for category, warnings in all_warnings.items() %} +{% if category == 'allowed-org-collaborators' %} +## Warning: Users found in organizations as direct repo collaborators but allowed as exceptions + +| User | Organization | Repository | +| ---------------------- | ------------------------- | ------------------------- | +{%- for warning in warnings %} +{%- for user in warning['users'] %} +| {{ user }} | {{ warning['org'] }} | {{ warning['repo'] }} | +{%- endfor -%} +{%- endfor -%} +{%- endif -%} +{%- endfor -%} +{%- endif -%}