From 1c874d1283cf61cca99d70981c450dcfc709ccf5 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Thu, 15 May 2025 08:33:36 +0200 Subject: [PATCH] feat(repository): add new check `repository_default_branch_deletion_disabled` (#6200) Co-authored-by: MrCloudSec Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com> --- prowler/CHANGELOG.md | 1 + .../__init__.py | 0 ...ult_branch_deletion_disabled.metadata.json | 30 +++++ ...sitory_default_branch_deletion_disabled.py | 42 +++++++ .../services/repository/repository_service.py | 5 + ...y_default_branch_deletion_disabled_test.py | 110 ++++++++++++++++++ .../repository/repository_service_test.py | 2 + 7 files changed, 190 insertions(+) create mode 100644 prowler/providers/github/services/repository/repository_default_branch_deletion_disabled/__init__.py create mode 100644 prowler/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled.metadata.json create mode 100644 prowler/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled.py create mode 100644 tests/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index f2771630ad..8c6d158800 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add `repository_default_branch_protection_enabled` check for GitHub provider. [(#6161)](https://github.com/prowler-cloud/prowler/pull/6161) - Add `repository_default_branch_requires_linear_history` check for GitHub provider. [(#6162)](https://github.com/prowler-cloud/prowler/pull/6162) - 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 `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_deletion_disabled/__init__.py b/prowler/providers/github/services/repository/repository_default_branch_deletion_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled.metadata.json b/prowler/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled.metadata.json new file mode 100644 index 0000000000..422ff6dbe4 --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "github", + "CheckID": "repository_default_branch_deletion_disabled", + "CheckTitle": "Check if a repository denies default branch deletion", + "CheckType": [], + "ServiceName": "repository", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "GitHubRepository", + "Description": "Ensure that the repository denies default branch deletion.", + "Risk": "Allowing the deletion of protected branches by users with push access increases the risk of accidental or intentional branch removal, potentially resulting in significant data loss or disruption to the development process.", + "RelatedUrl": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#allow-deletions", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Deny the ability to delete protected branches to ensure the preservation of critical branch data. This prevents accidental or malicious deletions and helps maintain the integrity and stability of the repository.", + "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_deletion_disabled/repository_default_branch_deletion_disabled.py b/prowler/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled.py new file mode 100644 index 0000000000..12d504a926 --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled.py @@ -0,0 +1,42 @@ +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_deletion_disabled(Check): + """Check if a repository denies branch deletion + + This class verifies whether each repository denies default branch deletion. + """ + + def execute(self) -> List[CheckReportGithub]: + """Execute the Github Repository Denies Default Branch Deletion check + + Iterates over all repositories and checks if they deny default branch deletion. + + Returns: + List[CheckReportGithub]: A list of reports for each repository + """ + findings = [] + for repo in repository_client.repositories.values(): + if repo.default_branch_deletion is not None: + report = CheckReportGithub( + metadata=self.metadata(), resource=repo, repository=repo.name + ) + report.status = "FAIL" + report.status_extended = ( + f"Repository {repo.name} does allow default branch deletion." + ) + + if not repo.default_branch_deletion: + report.status = "PASS" + report.status_extended = ( + f"Repository {repo.name} does deny default branch deletion." + ) + + 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 7e6054a79e..1925a30566 100644 --- a/prowler/providers/github/services/repository/repository_service.py +++ b/prowler/providers/github/services/repository/repository_service.py @@ -35,6 +35,7 @@ class Repository(GithubService): branch_protection = False required_linear_history = False allow_force_pushes = True + branch_deletion = True try: branch = repo.get_branch(default_branch) if branch.protected: @@ -52,6 +53,7 @@ class Repository(GithubService): protection.required_linear_history ) allow_force_pushes = protection.allow_force_pushes + branch_deletion = protection.allow_deletions branch_protection = True except Exception as error: # If the branch is not found, it is not protected @@ -66,6 +68,7 @@ class Repository(GithubService): branch_protection = None required_linear_history = None allow_force_pushes = None + branch_deletion = None logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) @@ -81,6 +84,7 @@ class Repository(GithubService): approval_count=approval_cnt, required_linear_history=required_linear_history, allow_force_pushes=allow_force_pushes, + default_branch_deletion=branch_deletion, default_branch_protection=branch_protection, ) @@ -104,4 +108,5 @@ class Repo(BaseModel): require_pull_request: Optional[bool] required_linear_history: Optional[bool] allow_force_pushes: Optional[bool] + default_branch_deletion: Optional[bool] approval_count: Optional[int] diff --git a/tests/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled_test.py b/tests/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled_test.py new file mode 100644 index 0000000000..825e4bdc6e --- /dev/null +++ b/tests/providers/github/services/repository/repository_default_branch_deletion_disabled/repository_default_branch_deletion_disabled_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_deletion_disabled_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_deletion_disabled.repository_default_branch_deletion_disabled.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_deletion_disabled.repository_default_branch_deletion_disabled import ( + repository_default_branch_deletion_disabled, + ) + + check = repository_default_branch_deletion_disabled() + result = check.execute() + assert len(result) == 0 + + def test_allow_branch_deletion_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", + default_branch=default_branch, + default_branch_deletion=True, + private=False, + securitymd=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_deletion_disabled.repository_default_branch_deletion_disabled.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_deletion_disabled.repository_default_branch_deletion_disabled import ( + repository_default_branch_deletion_disabled, + ) + + check = repository_default_branch_deletion_disabled() + 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 allow default branch deletion." + ) + + def test_allow_branch_deletion_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", + private=False, + default_branch=default_branch, + default_branch_deletion=False, + 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_deletion_disabled.repository_default_branch_deletion_disabled.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_deletion_disabled.repository_default_branch_deletion_disabled import ( + repository_default_branch_deletion_disabled, + ) + + check = repository_default_branch_deletion_disabled() + 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 deny default branch deletion." + ) diff --git a/tests/providers/github/services/repository/repository_service_test.py b/tests/providers/github/services/repository/repository_service_test.py index 0bdd615497..da28e1269f 100644 --- a/tests/providers/github/services/repository/repository_service_test.py +++ b/tests/providers/github/services/repository/repository_service_test.py @@ -20,6 +20,7 @@ def mock_list_repositories(_): require_pull_request=True, required_linear_history=True, allow_force_pushes=True, + default_branch_deletion=True, approval_count=2, ), } @@ -49,4 +50,5 @@ class Test_Repository_Service: assert repository_service.repositories[1].required_linear_history assert repository_service.repositories[1].require_pull_request assert repository_service.repositories[1].allow_force_pushes + assert repository_service.repositories[1].default_branch_deletion assert repository_service.repositories[1].approval_count == 2