mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
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:
@@ -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)
|
||||
|
||||
+30
@@ -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": ""
|
||||
}
|
||||
+38
@@ -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]
|
||||
|
||||
+112
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user