feat(repository): add new check repository_default_branch_requires_codeowners_review (#7753)

Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
This commit is contained in:
Andoni Alonso
2025-05-21 16:18:55 +02:00
committed by GitHub
parent f72eb7e212
commit d036e0054b
7 changed files with 192 additions and 0 deletions
+1
View File
@@ -14,6 +14,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add a level for Prowler ThreatScore in the accordion in Dashboard. [(#7739)](https://github.com/prowler-cloud/prowler/pull/7739)
- Add CIS 4.0 compliance framework for GCP. [(7785)](https://github.com/prowler-cloud/prowler/pull/7785)
- Add `repository_has_codeowners_file` check for GitHub provider. [(#7752)](https://github.com/prowler-cloud/prowler/pull/7752)
- Add `repository_default_branch_requires_codeowners_review` check for GitHub provider. [(#7753)](https://github.com/prowler-cloud/prowler/pull/7753)
### Fixed
- Fix `m365_powershell test_credentials` to use sanitized credentials. [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
@@ -0,0 +1,30 @@
{
"Provider": "github",
"CheckID": "repository_default_branch_requires_codeowners_review",
"CheckTitle": "Check if code owner approval is required for changes to owned code",
"CheckType": [],
"ServiceName": "repository",
"SubServiceName": "",
"ResourceIdTemplate": "github:user-id:repository/repository-name",
"Severity": "high",
"ResourceType": "GitHubRepository",
"Description": "Ensure that code owners are required to review and approve any proposed changes that affect their respective areas of ownership in the code base.",
"Risk": "If code owner approval is not required, unauthorized or unqualified individuals may merge changes to sensitive or critical areas of the codebase, increasing the risk of security vulnerabilities, bugs, or malicious modifications.",
"RelatedUrl": "https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#requiring-code-owner-review",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "To require code owner review, navigate to the repository settings, click on 'Branches', add or edit a branch protection rule, and enable 'Require review from Code Owners'.",
"Url": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-review-from-code-owners"
}
},
"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_requires_codeowners_review(Check):
"""Check if code owner approval is required for changes to owned code
This class verifies whether each repository requires code owner review for changes to code they own.
"""
def execute(self) -> List[CheckReportGithub]:
"""Execute the Github Repository Code Owner Approval Requirement check
Iterates over all repositories and checks if they require code owner review for changes.
Returns:
List[CheckReportGithub]: A list of reports for each repository
"""
findings = []
for repo in repository_client.repositories.values():
if repo.require_code_owner_reviews is not None:
report = CheckReportGithub(
metadata=self.metadata(), resource=repo, repository=repo.name
)
if repo.require_code_owner_reviews:
report.status = "PASS"
report.status_extended = f"Repository {repo.name} requires code owner approval for changes to owned code."
else:
report.status = "FAIL"
report.status_extended = f"Repository {repo.name} does not require code owner approval for changes to owned code."
findings.append(report)
return findings
@@ -60,6 +60,7 @@ class Repository(GithubService):
required_linear_history = False
allow_force_pushes = True
branch_deletion = True
require_code_owner_reviews = False
status_checks = False
enforce_admins = False
conversation_resolution = False
@@ -89,6 +90,11 @@ class Repository(GithubService):
protection.required_conversation_resolution
)
branch_protection = True
require_code_owner_reviews = (
protection.required_pull_request_reviews.require_code_owner_reviews
if require_pr
else False
)
except Exception as error:
# If the branch is not found, it is not protected
if "404" in str(error):
@@ -103,6 +109,7 @@ class Repository(GithubService):
required_linear_history = None
allow_force_pushes = None
branch_deletion = None
require_code_owner_reviews = None
status_checks = None
enforce_admins = None
conversation_resolution = None
@@ -127,6 +134,7 @@ class Repository(GithubService):
conversation_resolution=conversation_resolution,
default_branch_protection=branch_protection,
codeowners_exists=codeowners_exists,
require_code_owner_reviews=require_code_owner_reviews,
delete_branch_on_merge=delete_branch_on_merge,
)
@@ -155,5 +163,6 @@ class Repo(BaseModel):
enforce_admins: Optional[bool]
approval_count: Optional[int]
codeowners_exists: Optional[bool]
require_code_owner_reviews: Optional[bool]
delete_branch_on_merge: Optional[bool]
conversation_resolution: Optional[bool]
@@ -0,0 +1,112 @@
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_codeowners_review:
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_codeowners_review.repository_default_branch_requires_codeowners_review.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_codeowners_review.repository_default_branch_requires_codeowners_review import (
repository_default_branch_requires_codeowners_review,
)
check = repository_default_branch_requires_codeowners_review()
result = check.execute()
assert len(result) == 0
def test_one_repository_no_codeowner_approval(self):
repository_client = mock.MagicMock
repo_name = "repo1"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
full_name="account-name/repo1",
default_branch="main",
private=False,
securitymd=True,
require_pull_request=False,
approval_count=0,
require_code_owner_reviews=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_codeowners_review.repository_default_branch_requires_codeowners_review.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_codeowners_review.repository_default_branch_requires_codeowners_review import (
repository_default_branch_requires_codeowners_review,
)
check = repository_default_branch_requires_codeowners_review()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == repo_name
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} does not require code owner approval for changes to owned code."
)
def test_one_repository_with_codeowner_approval(self):
repository_client = mock.MagicMock
repo_name = "repo2"
repository_client.repositories = {
2: Repo(
id=2,
name=repo_name,
full_name="account-name/repo2",
default_branch="main",
private=False,
securitymd=True,
require_pull_request=False,
approval_count=0,
require_code_owner_reviews=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_codeowners_review.repository_default_branch_requires_codeowners_review.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_codeowners_review.repository_default_branch_requires_codeowners_review import (
repository_default_branch_requires_codeowners_review,
)
check = repository_default_branch_requires_codeowners_review()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 2
assert result[0].resource_name == repo_name
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Repository {repo_name} requires code owner approval for changes to owned code."
)
@@ -24,6 +24,7 @@ def mock_list_repositories(_):
status_checks=True,
approval_count=2,
codeowners_exists=True,
require_code_owner_reviews=True,
enforce_admins=True,
delete_branch_on_merge=True,
conversation_resolution=True,
@@ -62,6 +63,7 @@ class Test_Repository_Service:
assert repository_service.repositories[1].conversation_resolution
assert repository_service.repositories[1].approval_count == 2
assert repository_service.repositories[1].codeowners_exists is True
assert repository_service.repositories[1].require_code_owner_reviews is True
class Test_Repository_FileExists: