From 98da709202a1f1f45cca2854060760417fd3868a Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Fri, 16 May 2025 08:29:45 +0200 Subject: [PATCH] feat(repository): add new check `repository_default_branch_protection_applies_to_admins` (#6205) Co-authored-by: MrCloudSec --- prowler/CHANGELOG.md | 1 + .../__init__.py | 0 ...protection_applies_to_admins.metadata.json | 30 +++++ ...ult_branch_protection_applies_to_admins.py | 38 ++++++ .../services/repository/repository_service.py | 5 + ...ranch_protection_applies_to_admins_test.py | 110 ++++++++++++++++++ .../repository/repository_service_test.py | 2 + 7 files changed, 186 insertions(+) create mode 100644 prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/__init__.py create mode 100644 prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins.metadata.json create mode 100644 prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins.py create mode 100644 tests/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 2d3f9ac51b..3e093dab11 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add `repository_default_branch_disallows_force_push` check for GitHub provider. [(#6197)](https://github.com/prowler-cloud/prowler/pull/6197) - Add `repository_default_branch_deletion_disabled` check for GitHub provider. [(#6200)](https://github.com/prowler-cloud/prowler/pull/6200) - Add `repository_default_branch_status_checks_required` check for GitHub provider. [(#6204)](https://github.com/prowler-cloud/prowler/pull/6204) +- Add `repository_default_branch_protection_applies_to_admins` check for GitHub provider. [(#6205)](https://github.com/prowler-cloud/prowler/pull/6205) - Add `organization_members_mfa_required` check for GitHub provider. [(#6304)](https://github.com/prowler-cloud/prowler/pull/6304) - Add GitHub provider documentation and CIS v1.0.0 compliance. [(#6116)](https://github.com/prowler-cloud/prowler/pull/6116) diff --git a/prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/__init__.py b/prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins.metadata.json b/prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins.metadata.json new file mode 100644 index 0000000000..a20ff5f930 --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "github", + "CheckID": "repository_default_branch_protection_applies_to_admins", + "CheckTitle": "Check if repository enforces admin branch protection", + "CheckType": [], + "ServiceName": "repository", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "GithubRepository", + "Description": "Ensure that the repository enforces branch protection rules for administrators.", + "Risk": "Excluding administrators from branch protection rules introduces a significant risk of unauthorized or unreviewed changes being pushed to protected branches. This can lead to vulnerabilities, including the potential insertion of malicious code, especially if an administrator account is compromised.", + "RelatedUrl": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#do-not-allow-bypassing-the-above-settings", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enforce branch protection rules for administrators to ensure they adhere to the same security and quality standards as other users. This mitigates the risk of unreviewed or untrusted code being introduced, enhancing the overall integrity of the codebase.", + "Url": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins.py b/prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins.py new file mode 100644 index 0000000000..916ddd35c5 --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins.py @@ -0,0 +1,38 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGithub +from prowler.providers.github.services.repository.repository_client import ( + repository_client, +) + + +class repository_default_branch_protection_applies_to_admins(Check): + """Check if a repository enforces administrators to be subject to the same branch protection rules as other users + + This class verifies whether each repository enforces administrators to be subject to the same branch protection rules as other users. + """ + + def execute(self) -> List[CheckReportGithub]: + """Execute the Github Repository Enforces Admin Branch Protection check + + Iterates over all repositories and checks if they enforce administrators to be subject to the same branch protection rules as other users. + + Returns: + List[CheckReportGithub]: A list of reports for each repository. + """ + findings = [] + for repo in repository_client.repositories.values(): + if repo.enforce_admins is not None: + report = CheckReportGithub( + metadata=self.metadata(), resource=repo, repository=repo.name + ) + report.status = "FAIL" + report.status_extended = f"Repository {repo.name} does not enforce administrators to be subject to the same branch protection rules as other users." + + if repo.enforce_admins: + report.status = "PASS" + report.status_extended = f"Repository {repo.name} does enforce administrators to be subject to the same branch protection rules as other users." + + findings.append(report) + + return findings diff --git a/prowler/providers/github/services/repository/repository_service.py b/prowler/providers/github/services/repository/repository_service.py index c9a62f8086..3adcc8a968 100644 --- a/prowler/providers/github/services/repository/repository_service.py +++ b/prowler/providers/github/services/repository/repository_service.py @@ -37,6 +37,7 @@ class Repository(GithubService): allow_force_pushes = True branch_deletion = True status_checks = False + enforce_admins = False try: branch = repo.get_branch(default_branch) if branch.protected: @@ -58,6 +59,7 @@ class Repository(GithubService): status_checks = ( protection.required_status_checks is not None ) + enforce_admins = protection.enforce_admins branch_protection = True except Exception as error: # If the branch is not found, it is not protected @@ -74,6 +76,7 @@ class Repository(GithubService): allow_force_pushes = None branch_deletion = None status_checks = None + enforce_admins = None logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) @@ -91,6 +94,7 @@ class Repository(GithubService): allow_force_pushes=allow_force_pushes, default_branch_deletion=branch_deletion, status_checks=status_checks, + enforce_admins=enforce_admins, default_branch_protection=branch_protection, ) @@ -116,4 +120,5 @@ class Repo(BaseModel): allow_force_pushes: Optional[bool] default_branch_deletion: Optional[bool] status_checks: Optional[bool] + enforce_admins: Optional[bool] approval_count: Optional[int] diff --git a/tests/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins_test.py b/tests/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins_test.py new file mode 100644 index 0000000000..f85bbdced9 --- /dev/null +++ b/tests/providers/github/services/repository/repository_default_branch_protection_applies_to_admins/repository_default_branch_protection_applies_to_admins_test.py @@ -0,0 +1,110 @@ +from unittest import mock + +from prowler.providers.github.services.repository.repository_service import Repo +from tests.providers.github.github_fixtures import set_mocked_github_provider + + +class Test_repository_default_branch_protection_applies_to_admins_test: + def test_no_repositories(self): + repository_client = mock.MagicMock + repository_client.repositories = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins import ( + repository_default_branch_protection_applies_to_admins, + ) + + check = repository_default_branch_protection_applies_to_admins() + result = check.execute() + assert len(result) == 0 + + def test_enforce_status_checks_disabled(self): + repository_client = mock.MagicMock + repo_name = "repo1" + default_branch = "main" + repository_client.repositories = { + 1: Repo( + id=1, + name=repo_name, + full_name="account-name/repo1", + default_branch=default_branch, + private=False, + securitymd=False, + enforce_admins=False, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins import ( + repository_default_branch_protection_applies_to_admins, + ) + + check = repository_default_branch_protection_applies_to_admins() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == "repo1" + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Repository {repo_name} does not enforce administrators to be subject to the same branch protection rules as other users." + ) + + def test_enforce_status_checks_enabled(self): + repository_client = mock.MagicMock + repo_name = "repo1" + default_branch = "main" + repository_client.repositories = { + 1: Repo( + id=1, + name=repo_name, + full_name="account-name/repo1", + private=False, + default_branch=default_branch, + enforce_admins=True, + securitymd=True, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins import ( + repository_default_branch_protection_applies_to_admins, + ) + + check = repository_default_branch_protection_applies_to_admins() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == "repo1" + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Repository {repo_name} does enforce administrators to be subject to the same branch protection rules as other users." + ) diff --git a/tests/providers/github/services/repository/repository_service_test.py b/tests/providers/github/services/repository/repository_service_test.py index 54aec2936b..a24c1625a7 100644 --- a/tests/providers/github/services/repository/repository_service_test.py +++ b/tests/providers/github/services/repository/repository_service_test.py @@ -23,6 +23,7 @@ def mock_list_repositories(_): default_branch_deletion=True, status_checks=True, approval_count=2, + enforce_admins=True, ), } @@ -53,4 +54,5 @@ class Test_Repository_Service: assert repository_service.repositories[1].allow_force_pushes assert repository_service.repositories[1].default_branch_deletion assert repository_service.repositories[1].status_checks + assert repository_service.repositories[1].enforce_admins assert repository_service.repositories[1].approval_count == 2