feat(repository): add new check repository_default_branch_deletion_disabled (#6200)

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-15 08:33:36 +02:00
committed by GitHub
parent 8f9bdae2b7
commit 1c874d1283
7 changed files with 190 additions and 0 deletions
+1
View File
@@ -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)
@@ -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": ""
}
@@ -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
@@ -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]
@@ -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."
)
@@ -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