feat: add new check

This commit is contained in:
HugoPBrito committed 2024-12-12 13:41:15 +01:00
1 parent ab1239cc93
commit 705f577412
9 files changed
+301 -84

No files matched your search

@@ -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."
@@ -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": ""
}
@@ -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
@@ -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,
@@ -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})."
)
@@ -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