From 5a9ccd60a01f03544abb68291eb886e2cea3a010 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Wed, 14 May 2025 14:37:27 +0200 Subject: [PATCH] feat(repository): add new check `repository_default_branch_requires_linear_history` (#6162) Co-authored-by: MrCloudSec --- prowler/CHANGELOG.md | 3 +- .../__init__.py | 0 ...anch_requires_linear_history.metadata.json | 30 +++++ ..._default_branch_requires_linear_history.py | 38 ++++++ .../__init__.py | 0 ...requires_multiple_approvals.metadata.json} | 2 +- ...ult_branch_requires_multiple_approvals.py} | 2 +- .../services/repository/repository_service.py | 14 ++- ...ult_branch_requires_linear_history_test.py | 110 ++++++++++++++++++ ...ranch_requires_multiple_approvals_test.py} | 34 +++--- .../repository/repository_service_test.py | 2 + 11 files changed, 210 insertions(+), 25 deletions(-) rename prowler/providers/github/services/repository/{repository_code_changes_multi_approval_requirement => repository_default_branch_requires_linear_history}/__init__.py (100%) create mode 100644 prowler/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history.metadata.json create mode 100644 prowler/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history.py create mode 100644 prowler/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/__init__.py rename prowler/providers/github/services/repository/{repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.metadata.json => repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals.metadata.json} (95%) rename prowler/providers/github/services/repository/{repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.py => repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals.py} (95%) create mode 100644 tests/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history_test.py rename tests/providers/github/services/repository/{repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement_test.py => repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals_test.py} (75%) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 916005d084..130a3a6ba7 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -9,8 +9,9 @@ All notable changes to the **Prowler SDK** are documented in this file. - Allow setting cluster name in in-cluster mode in Kubernetes. [(#7695)](https://github.com/prowler-cloud/prowler/pull/7695) - 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_requires_multiple_approvals` 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 `repository_default_branch_requires_linear_history` check for GitHub provider. [(#6162)](https://github.com/prowler-cloud/prowler/pull/6162) - 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_code_changes_multi_approval_requirement/__init__.py b/prowler/providers/github/services/repository/repository_default_branch_requires_linear_history/__init__.py similarity index 100% rename from prowler/providers/github/services/repository/repository_code_changes_multi_approval_requirement/__init__.py rename to prowler/providers/github/services/repository/repository_default_branch_requires_linear_history/__init__.py diff --git a/prowler/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history.metadata.json b/prowler/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history.metadata.json new file mode 100644 index 0000000000..b33d6de467 --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "github", + "CheckID": "repository_default_branch_requires_linear_history", + "CheckTitle": "Check if repository default branch requires linear history", + "CheckType": [], + "ServiceName": "repository", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "GithubRepository", + "Description": "Ensure that the repository default branch requires linear history.", + "Risk": "Allowing non-linear history can result in a cluttered and difficult-to-trace Git history, making it harder to identify specific changes, debug issues, and understand the sequence of development. This increases the risk of errors, inconsistencies, and bugs, especially in production environments.", + "RelatedUrl": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-linear-history", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enforce a linear history by requiring rebase or squash merges for pull requests. This will create a clean, chronological commit history, making it easier to track changes, revert modifications, and troubleshoot any issues that arise.", + "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_requires_linear_history/repository_default_branch_requires_linear_history.py b/prowler/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history.py new file mode 100644 index 0000000000..a06c8b4a3f --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history.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_requires_linear_history(Check): + """Check if a repository requires linear history on default branch + + This class verifies whether each repository requires linear history on the default branch. + """ + + def execute(self) -> List[CheckReportGithub]: + """Execute the Github Repository Merging Requires Linear History check + + Iterates over all repositories and checks if they require linear history on the default branch. + + Returns: + List[CheckReportGithub]: A list of reports for each repository + """ + findings = [] + for repo in repository_client.repositories.values(): + if repo.required_linear_history 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 require linear history on default branch ({repo.default_branch})." + + if repo.required_linear_history: + report.status = "PASS" + report.status_extended = f"Repository {repo.name} does require linear history on default branch ({repo.default_branch})." + + findings.append(report) + + return findings diff --git a/prowler/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/__init__.py b/prowler/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.metadata.json b/prowler/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals.metadata.json similarity index 95% rename from prowler/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.metadata.json rename to prowler/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals.metadata.json index 8390543480..4de957cb40 100644 --- a/prowler/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.metadata.json +++ b/prowler/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals.metadata.json @@ -1,6 +1,6 @@ { "Provider": "github", - "CheckID": "repository_code_changes_multi_approval_requirement", + "CheckID": "repository_default_branch_requires_multiple_approvals", "CheckTitle": "Check if repositories require at least 2 code changes approvals", "CheckType": [], "ServiceName": "repository", diff --git a/prowler/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.py b/prowler/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals.py similarity index 95% rename from prowler/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.py rename to prowler/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals.py index 599b77a6f5..ac75115010 100644 --- a/prowler/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.py +++ b/prowler/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals.py @@ -6,7 +6,7 @@ from prowler.providers.github.services.repository.repository_client import ( ) -class repository_code_changes_multi_approval_requirement(Check): +class repository_default_branch_requires_multiple_approvals(Check): """Check if a repository enforces at least 2 approvals for code changes This class verifies whether each repository enforces at least 2 approvals for code changes. diff --git a/prowler/providers/github/services/repository/repository_service.py b/prowler/providers/github/services/repository/repository_service.py index 6070e4cb22..28ba63e7ff 100644 --- a/prowler/providers/github/services/repository/repository_service.py +++ b/prowler/providers/github/services/repository/repository_service.py @@ -33,6 +33,7 @@ class Repository(GithubService): require_pr = False approval_cnt = 0 branch_protection = False + required_linear_history = False try: branch = repo.get_branch(default_branch) if branch.protected: @@ -46,16 +47,17 @@ class Repository(GithubService): if require_pr else 0 ) + required_linear_history = ( + protection.required_linear_history + ) branch_protection = True except Exception as error: - if "404" in str(error): - require_pr = False - approval_cnt = 0 - branch_protection = False - else: + # If the branch is not found, it is not protected + if "404" not in str(error): require_pr = None approval_cnt = None branch_protection = None + required_linear_history = None logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) @@ -69,6 +71,7 @@ class Repository(GithubService): securitymd=securitymd_exists, require_pull_request=require_pr, approval_count=approval_cnt, + required_linear_history=required_linear_history, default_branch_protection=branch_protection, ) @@ -90,4 +93,5 @@ class Repo(BaseModel): private: bool securitymd: Optional[bool] require_pull_request: Optional[bool] + required_linear_history: Optional[bool] approval_count: Optional[int] diff --git a/tests/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history_test.py b/tests/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history_test.py new file mode 100644 index 0000000000..5759697999 --- /dev/null +++ b/tests/providers/github/services/repository/repository_default_branch_requires_linear_history/repository_default_branch_requires_linear_history_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_requires_linear_history_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_requires_linear_history.repository_default_branch_requires_linear_history.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_requires_linear_history.repository_default_branch_requires_linear_history import ( + repository_default_branch_requires_linear_history, + ) + + check = repository_default_branch_requires_linear_history() + result = check.execute() + assert len(result) == 0 + + def test_linear_history_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, + required_linear_history=False, + 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_requires_linear_history.repository_default_branch_requires_linear_history.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_requires_linear_history.repository_default_branch_requires_linear_history import ( + repository_default_branch_requires_linear_history, + ) + + check = repository_default_branch_requires_linear_history() + 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 require linear history on default branch ({default_branch})." + ) + + def test_linear_history_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, + required_linear_history=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_requires_linear_history.repository_default_branch_requires_linear_history.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_default_branch_requires_linear_history.repository_default_branch_requires_linear_history import ( + repository_default_branch_requires_linear_history, + ) + + check = repository_default_branch_requires_linear_history() + 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 require linear history on default branch ({default_branch})." + ) 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_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals_test.py similarity index 75% rename from tests/providers/github/services/repository/repository_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement_test.py rename to tests/providers/github/services/repository/repository_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals_test.py index 4378ccae95..be8905fc7d 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_default_branch_requires_multiple_approvals/repository_default_branch_requires_multiple_approvals_test.py @@ -4,7 +4,7 @@ from prowler.providers.github.services.repository.repository_service import Repo from tests.providers.github.github_fixtures import set_mocked_github_provider -class Test_repository_code_changes_multi_approval_requirement: +class Test_repository_default_branch_requires_multiple_approvals: def test_no_repositories(self): repository_client = mock.MagicMock repository_client.repositories = {} @@ -15,15 +15,15 @@ class Test_repository_code_changes_multi_approval_requirement: return_value=set_mocked_github_provider(), ), mock.patch( - "prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client", + "prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.repository_client", new=repository_client, ), ): - from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import ( - repository_code_changes_multi_approval_requirement, + from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import ( + repository_default_branch_requires_multiple_approvals, ) - check = repository_code_changes_multi_approval_requirement() + check = repository_default_branch_requires_multiple_approvals() result = check.execute() assert len(result) == 0 @@ -50,15 +50,15 @@ class Test_repository_code_changes_multi_approval_requirement: return_value=set_mocked_github_provider(), ), mock.patch( - "prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client", + "prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.repository_client", new=repository_client, ), ): - from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import ( - repository_code_changes_multi_approval_requirement, + from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import ( + repository_default_branch_requires_multiple_approvals, ) - check = repository_code_changes_multi_approval_requirement() + check = repository_default_branch_requires_multiple_approvals() result = check.execute() assert len(result) == 1 assert result[0].resource_id == 1 @@ -92,15 +92,15 @@ class Test_repository_code_changes_multi_approval_requirement: return_value=set_mocked_github_provider(), ), mock.patch( - "prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client", + "prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.repository_client", new=repository_client, ), ): - from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import ( - repository_code_changes_multi_approval_requirement, + from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import ( + repository_default_branch_requires_multiple_approvals, ) - check = repository_code_changes_multi_approval_requirement() + check = repository_default_branch_requires_multiple_approvals() result = check.execute() assert len(result) == 1 assert result[0].resource_id == 1 @@ -134,15 +134,15 @@ class Test_repository_code_changes_multi_approval_requirement: return_value=set_mocked_github_provider(), ), mock.patch( - "prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client", + "prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.repository_client", new=repository_client, ), ): - from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import ( - repository_code_changes_multi_approval_requirement, + from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import ( + repository_default_branch_requires_multiple_approvals, ) - check = repository_code_changes_multi_approval_requirement() + check = repository_default_branch_requires_multiple_approvals() result = check.execute() assert len(result) == 1 assert result[0].resource_id == 1 diff --git a/tests/providers/github/services/repository/repository_service_test.py b/tests/providers/github/services/repository/repository_service_test.py index d03853423e..970e93a76b 100644 --- a/tests/providers/github/services/repository/repository_service_test.py +++ b/tests/providers/github/services/repository/repository_service_test.py @@ -18,6 +18,7 @@ def mock_list_repositories(_): private=False, securitymd=True, require_pull_request=True, + required_linear_history=True, approval_count=2, ), } @@ -44,5 +45,6 @@ class Test_Repository_Service: 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].required_linear_history assert repository_service.repositories[1].require_pull_request assert repository_service.repositories[1].approval_count == 2