mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-14 15:34:19 +00:00
feat: add new check
This commit is contained in:
9 files changed
+301
-84
No files matched your search
+1
-1
@@ -28,7 +28,7 @@ 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.approval_count >= 2:
|
||||
if 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."
|
||||
|
||||
|
||||
Whitespace-only changes.
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "github",
|
||||
"CheckID": "repository_enforces_default_branch_protection",
|
||||
"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": "Other",
|
||||
"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": ""
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
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_enforces_default_branch_protection(Check):
|
||||
"""Check if a repository enforces default branch protection
|
||||
|
||||
This class verifies whether each repository enforces default branch protection.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_Github]:
|
||||
"""Execute the Github Repository Enforces Default Branch Protection check
|
||||
|
||||
Iterates over all repositories and checks if they enforce default branch protection.
|
||||
|
||||
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 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
|
||||
@@ -17,45 +17,51 @@ class Repository(GithubService):
|
||||
try:
|
||||
for client in self.clients:
|
||||
for repo in client.get_user().get_repos():
|
||||
default_branch = repo.default_branch
|
||||
securitymd_exists = False
|
||||
try:
|
||||
securitymd_exists = repo.get_contents("SECURITY.md") is not None
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not find SECURITY.md for repo {repo.name}: {e}"
|
||||
)
|
||||
if not repo.private: # Only for testing purposes
|
||||
default_branch = repo.default_branch
|
||||
securitymd_exists = False
|
||||
try:
|
||||
securitymd_exists = (
|
||||
repo.get_contents("SECURITY.md") is not None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not find SECURITY.md for repo {repo.name}: {e}"
|
||||
)
|
||||
|
||||
require_pr = False
|
||||
approval_cnt = 0
|
||||
try:
|
||||
branch = repo.get_branch(default_branch)
|
||||
if branch.protected:
|
||||
protection = branch.get_protection()
|
||||
if protection:
|
||||
require_pr = (
|
||||
protection.required_pull_request_reviews is not None
|
||||
)
|
||||
approval_cnt = (
|
||||
protection.required_pull_request_reviews.required_approving_review_count
|
||||
if require_pr
|
||||
else 0
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not get branch protection for repo {repo.name}: {e}"
|
||||
)
|
||||
branch_protection = None
|
||||
try:
|
||||
branch = repo.get_branch(default_branch)
|
||||
if branch.protected:
|
||||
protection = branch.get_protection()
|
||||
if protection:
|
||||
require_pr = (
|
||||
protection.required_pull_request_reviews
|
||||
is not None
|
||||
)
|
||||
approval_cnt = (
|
||||
protection.required_pull_request_reviews.required_approving_review_count
|
||||
if require_pr
|
||||
else 0
|
||||
)
|
||||
branch_protection = Protection(
|
||||
require_pull_request=require_pr,
|
||||
approval_count=approval_cnt,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not get branch protection for repo {repo.name}: {e}"
|
||||
)
|
||||
|
||||
repos[repo.id] = Repo(
|
||||
id=repo.id,
|
||||
name=repo.name,
|
||||
full_name=repo.full_name,
|
||||
default_branch=repo.default_branch,
|
||||
private=repo.private,
|
||||
securitymd=securitymd_exists,
|
||||
require_pull_request=require_pr,
|
||||
approval_count=approval_cnt,
|
||||
)
|
||||
repos[repo.id] = Repo(
|
||||
id=repo.id,
|
||||
name=repo.name,
|
||||
full_name=repo.full_name,
|
||||
default_branch=repo.default_branch,
|
||||
private=repo.private,
|
||||
securitymd=securitymd_exists,
|
||||
default_branch_protection=branch_protection,
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -64,14 +70,20 @@ class Repository(GithubService):
|
||||
return repos
|
||||
|
||||
|
||||
class Protection(BaseModel):
|
||||
"""Model for Github Branch Protection"""
|
||||
|
||||
require_pull_request: Optional[bool] = False
|
||||
approval_count: Optional[int] = 0
|
||||
|
||||
|
||||
class Repo(BaseModel):
|
||||
"""Model for Github Repository"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
full_name: str
|
||||
default_branch: str
|
||||
private: bool
|
||||
default_branch: str
|
||||
default_branch_protection: Optional[Protection]
|
||||
securitymd: Optional[bool] = False
|
||||
require_pull_request: Optional[bool] = False
|
||||
approval_count: Optional[int] = 0
|
||||
+52
-34
@@ -1,6 +1,9 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import Repo
|
||||
from prowler.providers.github.services.repository.repository_service import (
|
||||
Protection,
|
||||
Repo,
|
||||
)
|
||||
from tests.providers.github.github_fixtures import set_mocked_github_provider
|
||||
|
||||
|
||||
@@ -9,12 +12,15 @@ class Test_repository_code_changes_multi_approval_requirement:
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import (
|
||||
repository_code_changes_multi_approval_requirement,
|
||||
@@ -32,20 +38,24 @@ class Test_repository_code_changes_multi_approval_requirement:
|
||||
id=1,
|
||||
name=repo_name,
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
private=False,
|
||||
default_branch="main",
|
||||
default_branch_protection=Protection(
|
||||
require_pull_request=False, approval_count=0
|
||||
),
|
||||
securitymd=False,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
),
|
||||
}
|
||||
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import (
|
||||
repository_code_changes_multi_approval_requirement,
|
||||
@@ -70,20 +80,24 @@ class Test_repository_code_changes_multi_approval_requirement:
|
||||
id=1,
|
||||
name=repo_name,
|
||||
full_name="account-name/repo1",
|
||||
default_branch="master",
|
||||
private=False,
|
||||
default_branch="master",
|
||||
default_branch_protection=Protection(
|
||||
require_pull_request=True, approval_count=0
|
||||
),
|
||||
securitymd=False,
|
||||
require_pull_request=True,
|
||||
approval_count=0,
|
||||
),
|
||||
}
|
||||
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import (
|
||||
repository_code_changes_multi_approval_requirement,
|
||||
@@ -108,20 +122,24 @@ class Test_repository_code_changes_multi_approval_requirement:
|
||||
id=1,
|
||||
name=repo_name,
|
||||
full_name="account-name/repo1",
|
||||
default_branch="master",
|
||||
private=False,
|
||||
default_branch="master",
|
||||
default_branch_protection=Protection(
|
||||
require_pull_request=True, approval_count=2
|
||||
),
|
||||
securitymd=True,
|
||||
require_pull_request=True,
|
||||
approval_count=2,
|
||||
),
|
||||
}
|
||||
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
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_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_code_changes_multi_approval_requirement.repository_code_changes_multi_approval_requirement import (
|
||||
repository_code_changes_multi_approval_requirement,
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
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_enforces_default_branch_protection_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_enforces_default_branch_protection.repository_enforces_default_branch_protection.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_enforces_default_branch_protection.repository_enforces_default_branch_protection import (
|
||||
repository_enforces_default_branch_protection,
|
||||
)
|
||||
|
||||
check = repository_enforces_default_branch_protection()
|
||||
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,
|
||||
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_enforces_default_branch_protection.repository_enforces_default_branch_protection.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_enforces_default_branch_protection.repository_enforces_default_branch_protection import (
|
||||
repository_enforces_default_branch_protection,
|
||||
)
|
||||
|
||||
check = repository_enforces_default_branch_protection()
|
||||
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=Protection(
|
||||
require_pull_request=True, approval_count=2
|
||||
),
|
||||
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_enforces_default_branch_protection.repository_enforces_default_branch_protection.repository_client",
|
||||
new=repository_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.github.services.repository.repository_enforces_default_branch_protection.repository_enforces_default_branch_protection import (
|
||||
repository_enforces_default_branch_protection,
|
||||
)
|
||||
|
||||
check = repository_enforces_default_branch_protection()
|
||||
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})."
|
||||
)
|
||||
-4
@@ -35,8 +35,6 @@ class Test_repository_public_has_securitymd_file_test:
|
||||
default_branch="main",
|
||||
private=False,
|
||||
securitymd=False,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -73,8 +71,6 @@ class Test_repository_public_has_securitymd_file_test:
|
||||
default_branch="main",
|
||||
private=False,
|
||||
securitymd=True,
|
||||
require_pull_request=False,
|
||||
approval_count=0,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from prowler.providers.github.services.repository.repository_service import (
|
||||
Protection,
|
||||
Repo,
|
||||
Repository,
|
||||
)
|
||||
@@ -13,11 +14,12 @@ def mock_list_repositories(_):
|
||||
id=1,
|
||||
name="repo1",
|
||||
full_name="account-name/repo1",
|
||||
default_branch="main",
|
||||
private=False,
|
||||
default_branch="main",
|
||||
default_branch_protection=Protection(
|
||||
require_pull_request=True, approval_count=2
|
||||
),
|
||||
securitymd=True,
|
||||
require_pull_request=True,
|
||||
approval_count=2,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -41,6 +43,14 @@ 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"
|
||||
# Default branch protection
|
||||
assert repository_service.repositories[
|
||||
1
|
||||
].default_branch_protection.require_pull_request
|
||||
assert (
|
||||
repository_service.repositories[1].default_branch_protection.approval_count
|
||||
== 2
|
||||
)
|
||||
# Repo
|
||||
assert repository_service.repositories[1].securitymd
|
||||
assert repository_service.repositories[1].require_pull_request
|
||||
assert repository_service.repositories[1].approval_count == 2
|
||||
Reference in new issue
Block a user