diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 77f665303a..017b73c0d1 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add Prowler ThreatScore for M365 provider. [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692) - Add GitHub provider. [(#5787)](https://github.com/prowler-cloud/prowler/pull/5787) - Add `repository_code_changes_multi_approval_requirement` check for GitHub provider. [(#6160)](https://github.com/prowler-cloud/prowler/pull/6160) +- Add `repository_default_branch_protection_enabled` check for GitHub provider. [(#6161)](https://github.com/prowler-cloud/prowler/pull/6161) - 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_enabled/__init__.py b/prowler/providers/github/services/repository/repository_default_branch_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/repository/repository_default_branch_protection_enabled/repository_default_branch_protection_enabled.metadata.json b/prowler/providers/github/services/repository/repository_default_branch_protection_enabled/repository_default_branch_protection_enabled.metadata.json new file mode 100644 index 0000000000..8ba7caa87a --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_protection_enabled/repository_default_branch_protection_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "github", + "CheckID": "repository_default_branch_protection_enabled", + "CheckTitle": "Check if branch protection is enforced on the default branch ", + "CheckType": [], + "ServiceName": "repository", + "SubServiceName": "", + "ResourceIdTemplate": "github:user-id:repository/repository-name", + "Severity": "critical", + "ResourceType": "GitHubRepository", + "Description": "Ensure branch protection is enforced on the default branch", + "Risk": "The absence of branch protection on the default branch increases the risk of unauthorized, unreviewed, or untested changes being merged. This can compromise the stability, security, and reliability of the codebase, which is especially critical for production deployments.", + "RelatedUrl": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Apply branch protection rules to the default branch to ensure it is safeguarded against unauthorized or improper modifications. This helps maintain code quality, enforces proper review and testing procedures, and reduces the risk of accidental or malicious changes.", + "Url": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule#creating-a-branch-protection-rule" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/github/services/repository/repository_default_branch_protection_enabled/repository_default_branch_protection_enabled.py b/prowler/providers/github/services/repository/repository_default_branch_protection_enabled/repository_default_branch_protection_enabled.py new file mode 100644 index 0000000000..a6c9be5a37 --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_protection_enabled/repository_default_branch_protection_enabled.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_enabled(Check): + """Check if a repository enforces default branch protection + + This class verifies whether each repository enforces default branch protection. + """ + + def execute(self) -> List[CheckReportGithub]: + """Execute the Github Repository Enforces Default Branch Protection check + + Iterates over all repositories and checks if they enforce default branch protection. + + Returns: + List[CheckReportGithub]: A list of reports for each repository + """ + findings = [] + for repo in repository_client.repositories.values(): + if repo.default_branch_protection 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 branch protection on default branch ({repo.default_branch})." + + if repo.default_branch_protection: + report.status = "PASS" + report.status_extended = f"Repository {repo.name} does enforce branch protection on default branch ({repo.default_branch})." + + 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 d2ab5d3658..6070e4cb22 100644 --- a/prowler/providers/github/services/repository/repository_service.py +++ b/prowler/providers/github/services/repository/repository_service.py @@ -32,6 +32,7 @@ class Repository(GithubService): require_pr = False approval_cnt = 0 + branch_protection = False try: branch = repo.get_branch(default_branch) if branch.protected: @@ -45,13 +46,16 @@ class Repository(GithubService): if require_pr else 0 ) + branch_protection = True except Exception as error: if "404" in str(error): require_pr = False approval_cnt = 0 + branch_protection = False else: require_pr = None approval_cnt = None + branch_protection = None logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) @@ -65,6 +69,7 @@ class Repository(GithubService): securitymd=securitymd_exists, require_pull_request=require_pr, approval_count=approval_cnt, + default_branch_protection=branch_protection, ) except Exception as error: @@ -80,6 +85,7 @@ class Repo(BaseModel): id: int name: str full_name: str + default_branch_protection: Optional[bool] default_branch: str private: bool securitymd: Optional[bool] diff --git a/tests/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement_test.py b/tests/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement_test.py index ba0b28e673..4378ccae95 100644 --- a/tests/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement_test.py +++ b/tests/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement_test.py @@ -35,6 +35,7 @@ class Test_repository_code_changes_multi_approval_requirement: id=1, name=repo_name, full_name="account-name/repo1", + default_branch_protection=False, default_branch="main", private=False, securitymd=False, @@ -76,6 +77,7 @@ class Test_repository_code_changes_multi_approval_requirement: id=1, name=repo_name, full_name="account-name/repo1", + default_branch_protection=False, default_branch="master", private=False, securitymd=False, @@ -117,6 +119,7 @@ class Test_repository_code_changes_multi_approval_requirement: id=1, name=repo_name, full_name="account-name/repo1", + default_branch_protection=True, default_branch="master", private=False, securitymd=True, diff --git a/tests/providers/github/services/repository/repository_default_branch_protection_enabled/repository_default_branch_protection_enabled_test.py b/tests/providers/github/services/repository/repository_default_branch_protection_enabled/repository_default_branch_protection_enabled_test.py new file mode 100644 index 0000000000..3723863298 --- /dev/null +++ b/tests/providers/github/services/repository/repository_default_branch_protection_enabled/repository_default_branch_protection_enabled_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_enabled_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_enabled.repository_default_branch_protection_enabled.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_protection_enabled.repository_default_branch_protection_enabled import ( + repository_default_branch_protection_enabled, + ) + + check = repository_default_branch_protection_enabled() + result = check.execute() + assert len(result) == 0 + + def test_without_default_branch_protection(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, + default_branch_protection=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_protection_enabled.repository_default_branch_protection_enabled.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_protection_enabled.repository_default_branch_protection_enabled import ( + repository_default_branch_protection_enabled, + ) + + check = repository_default_branch_protection_enabled() + 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 branch protection on default branch ({default_branch})." + ) + + def test_default_branch_protection(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_protection=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_enabled.repository_default_branch_protection_enabled.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_protection_enabled.repository_default_branch_protection_enabled import ( + repository_default_branch_protection_enabled, + ) + + check = repository_default_branch_protection_enabled() + 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 branch protection on default branch ({default_branch})." + ) diff --git a/tests/providers/github/services/repository/repository_service_test.py b/tests/providers/github/services/repository/repository_service_test.py index bc1f709ead..d03853423e 100644 --- a/tests/providers/github/services/repository/repository_service_test.py +++ b/tests/providers/github/services/repository/repository_service_test.py @@ -13,6 +13,7 @@ def mock_list_repositories(_): id=1, name="repo1", full_name="account-name/repo1", + default_branch_protection=True, default_branch="main", private=False, securitymd=True, @@ -41,6 +42,7 @@ class Test_Repository_Service: assert repository_service.repositories[1].name == "repo1" assert repository_service.repositories[1].full_name == "account-name/repo1" assert repository_service.repositories[1].private is False + assert repository_service.repositories[1].default_branch == "main" assert repository_service.repositories[1].securitymd assert repository_service.repositories[1].require_pull_request assert repository_service.repositories[1].approval_count == 2