From e907abb92903ae986d8467f54240d8eebc1ecf5c Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Thu, 12 Dec 2024 13:44:13 +0100 Subject: [PATCH] feat: add new check --- ...code_changes_multi_approval_requirement.py | 5 +- .../__init__.py | 0 ...ging_requires_linear_history.metadata.json | 30 +++++ ...ository_merging_requires_linear_history.py | 40 ++++++ .../services/repository/repository_service.py | 11 +- ...ry_merging_requires_linear_history_test.py | 117 ++++++++++++++++++ .../repository/repository_service_test.py | 5 +- 7 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 prowler/providers/github/services/repository/repository_merging_requires_linear_history/__init__.py create mode 100644 prowler/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history.metadata.json create mode 100644 prowler/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history.py create mode 100644 tests/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history_test.py 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_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.py index 9ef2b02ee6..a2da6823fc 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_code_changes_multi_approval_requirement/repository_code_changes_multi_approval_requirement.py @@ -28,7 +28,10 @@ class repository_code_changes_multi_approval_requirement(Check): report.status = "FAIL" report.status_extended = f"Repository {repo.name} does not enforce at least 2 approvals for code changes." - if repo.default_branch_protection.approval_count >= 2: + if ( + repo.default_branch_protection + and repo.default_branch_protection.approval_count >= 2 + ): report.status = "PASS" report.status_extended = f"Repository {repo.name} does enforce at least 2 approvals for code changes." diff --git a/prowler/providers/github/services/repository/repository_merging_requires_linear_history/__init__.py b/prowler/providers/github/services/repository/repository_merging_requires_linear_history/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history.metadata.json b/prowler/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history.metadata.json new file mode 100644 index 0000000000..65609eede6 --- /dev/null +++ b/prowler/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "github", + "CheckID": "repository_merging_requires_linear_history", + "CheckTitle": "Check if repository default branch requires linear history", + "CheckType": [], + "ServiceName": "repository", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "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_merging_requires_linear_history/repository_merging_requires_linear_history.py b/prowler/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history.py new file mode 100644 index 0000000000..ac346e9199 --- /dev/null +++ b/prowler/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history.py @@ -0,0 +1,40 @@ +from typing import List + +from prowler.lib.check.models import Check, Check_Report_Github +from prowler.providers.github.services.repository.repository_client import ( + repository_client, +) + + +class repository_merging_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[Check_Report_Github]: + """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[Check_Report_Github]: A list of reports for each repository + """ + findings = [] + for repo in repository_client.repositories.values(): + report = Check_Report_Github(self.metadata()) + report.resource_id = repo.id + report.resource_name = 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.default_branch_protection + and repo.default_branch_protection.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_service.py b/prowler/providers/github/services/repository/repository_service.py index 8bc1d75257..d20d92aa57 100644 --- a/prowler/providers/github/services/repository/repository_service.py +++ b/prowler/providers/github/services/repository/repository_service.py @@ -44,9 +44,13 @@ class Repository(GithubService): if require_pr else 0 ) + required_linear_history = ( + protection.required_linear_history + ) branch_protection = Protection( require_pull_request=require_pr, approval_count=approval_cnt, + linear_history=required_linear_history, ) except Exception as e: logger.warning( @@ -73,8 +77,9 @@ class Repository(GithubService): class Protection(BaseModel): """Model for Github Branch Protection""" - require_pull_request: Optional[bool] = False - approval_count: Optional[int] = 0 + require_pull_request: bool = False + approval_count: int = 0 + linear_history: bool = False class Repo(BaseModel): @@ -86,4 +91,4 @@ class Repo(BaseModel): private: bool default_branch: str default_branch_protection: Optional[Protection] - securitymd: Optional[bool] = False + securitymd: bool = False diff --git a/tests/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history_test.py b/tests/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history_test.py new file mode 100644 index 0000000000..c829715a12 --- /dev/null +++ b/tests/providers/github/services/repository/repository_merging_requires_linear_history/repository_merging_requires_linear_history_test.py @@ -0,0 +1,117 @@ +from unittest import mock + +from prowler.providers.github.services.repository.repository_service import ( + Protection, + Repo, +) +from tests.providers.github.github_fixtures import set_mocked_github_provider + + +class Test_repository_merging_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_merging_requires_linear_history.repository_merging_requires_linear_history.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_merging_requires_linear_history.repository_merging_requires_linear_history import ( + repository_merging_requires_linear_history, + ) + + check = repository_merging_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, + default_branch_protection=Protection( + require_pull_request=True, approval_count=2, 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_merging_requires_linear_history.repository_merging_requires_linear_history.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_merging_requires_linear_history.repository_merging_requires_linear_history import ( + repository_merging_requires_linear_history, + ) + + check = repository_merging_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, + default_branch_protection=Protection( + require_pull_request=True, approval_count=2, 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_merging_requires_linear_history.repository_merging_requires_linear_history.repository_client", + new=repository_client, + ), + ): + from prowler.providers.github.services.repository.repository_merging_requires_linear_history.repository_merging_requires_linear_history import ( + repository_merging_requires_linear_history, + ) + + check = repository_merging_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_service_test.py b/tests/providers/github/services/repository/repository_service_test.py index 2cafe0ef9a..4346ba595e 100644 --- a/tests/providers/github/services/repository/repository_service_test.py +++ b/tests/providers/github/services/repository/repository_service_test.py @@ -17,7 +17,7 @@ def mock_list_repositories(_): private=False, default_branch="main", default_branch_protection=Protection( - require_pull_request=True, approval_count=2 + require_pull_request=True, approval_count=2, linear_history=True ), securitymd=True, ), @@ -52,5 +52,8 @@ class Test_Repository_Service: repository_service.repositories[1].default_branch_protection.approval_count == 2 ) + assert repository_service.repositories[ + 1 + ].default_branch_protection.linear_history # Repo assert repository_service.repositories[1].securitymd