feat(repository): add new check repository_default_branch_protection_enabled (#6161)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
This commit is contained in:
Hugo Pereira Brito
2025-05-19 10:46:40 +02:00
committed by César Arroba
co-authored by MrCloudSec Andoni A.
parent f1c165a89d
commit 2b4bb7f805
8 changed files with 190 additions and 0 deletions
+1
View File
@@ -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)
@@ -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": ""
}
@@ -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
@@ -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]
@@ -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,
@@ -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})."
)
@@ -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