feat(repository): add new check repository_default_branch_requires_linear_history (#6162)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Hugo Pereira Brito
2025-05-14 14:37:27 +02:00
committed by GitHub
parent beb7a53efe
commit 5a9ccd60a0
11 changed files with 210 additions and 25 deletions
+2 -1
View File
@@ -9,8 +9,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Allow setting cluster name in in-cluster mode in Kubernetes. [(#7695)](https://github.com/prowler-cloud/prowler/pull/7695)
- Add Prowler ThreatScore for M365 provider. [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
- Add GitHub provider. [(#5787)](https://github.com/prowler-cloud/prowler/pull/5787)
- Add `repository_code_changes_multi_approval_requirement` check for GitHub provider. [(#6160)](https://github.com/prowler-cloud/prowler/pull/6160)
- Add `repository_default_branch_requires_multiple_approvals` check for GitHub provider. [(#6160)](https://github.com/prowler-cloud/prowler/pull/6160)
- 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 `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_requires_linear_history",
"CheckTitle": "Check if repository default branch requires linear history",
"CheckType": [],
"ServiceName": "repository",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "GithubRepository",
"Description": "Ensure that the repository default branch requires linear history.",
"Risk": "Allowing non-linear history can result in a cluttered and difficult-to-trace Git history, making it harder to identify specific changes, debug issues, and understand the sequence of development. This increases the risk of errors, inconsistencies, and bugs, especially in production environments.",
"RelatedUrl": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-linear-history",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Enforce a linear history by requiring rebase or squash merges for pull requests. This will create a clean, chronological commit history, making it easier to track changes, revert modifications, and troubleshoot any issues that arise.",
"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,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_linear_history(Check):
"""Check if a repository requires linear history on default branch
This class verifies whether each repository requires linear history on the default branch.
"""
def execute(self) -> List[CheckReportGithub]:
"""Execute the Github Repository Merging Requires Linear History check
Iterates over all repositories and checks if they require linear history on the default branch.
Returns:
List[CheckReportGithub]: A list of reports for each repository
"""
findings = []
for repo in repository_client.repositories.values():
if repo.required_linear_history is not None:
report = CheckReportGithub(
metadata=self.metadata(), resource=repo, repository=repo.name
)
report.status = "FAIL"
report.status_extended = f"Repository {repo.name} does not require linear history on default branch ({repo.default_branch})."
if repo.required_linear_history:
report.status = "PASS"
report.status_extended = f"Repository {repo.name} does require linear history on default branch ({repo.default_branch})."
findings.append(report)
return findings
@@ -1,6 +1,6 @@
{
"Provider": "github",
"CheckID": "repository_code_changes_multi_approval_requirement",
"CheckID": "repository_default_branch_requires_multiple_approvals",
"CheckTitle": "Check if repositories require at least 2 code changes approvals",
"CheckType": [],
"ServiceName": "repository",
@@ -6,7 +6,7 @@ from prowler.providers.github.services.repository.repository_client import (
)
class repository_code_changes_multi_approval_requirement(Check):
class repository_default_branch_requires_multiple_approvals(Check):
"""Check if a repository enforces at least 2 approvals for code changes
This class verifies whether each repository enforces at least 2 approvals for code changes.
@@ -33,6 +33,7 @@ class Repository(GithubService):
require_pr = False
approval_cnt = 0
branch_protection = False
required_linear_history = False
try:
branch = repo.get_branch(default_branch)
if branch.protected:
@@ -46,16 +47,17 @@ class Repository(GithubService):
if require_pr
else 0
)
required_linear_history = (
protection.required_linear_history
)
branch_protection = True
except Exception as error:
if "404" in str(error):
require_pr = False
approval_cnt = 0
branch_protection = False
else:
# If the branch is not found, it is not protected
if "404" not in str(error):
require_pr = None
approval_cnt = None
branch_protection = None
required_linear_history = None
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
@@ -69,6 +71,7 @@ class Repository(GithubService):
securitymd=securitymd_exists,
require_pull_request=require_pr,
approval_count=approval_cnt,
required_linear_history=required_linear_history,
default_branch_protection=branch_protection,
)
@@ -90,4 +93,5 @@ class Repo(BaseModel):
private: bool
securitymd: Optional[bool]
require_pull_request: Optional[bool]
required_linear_history: 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_requires_linear_history_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_requires_linear_history.repository_default_branch_requires_linear_history.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_linear_history.repository_default_branch_requires_linear_history import (
repository_default_branch_requires_linear_history,
)
check = repository_default_branch_requires_linear_history()
result = check.execute()
assert len(result) == 0
def test_linear_history_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",
default_branch=default_branch,
required_linear_history=False,
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_requires_linear_history.repository_default_branch_requires_linear_history.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_linear_history.repository_default_branch_requires_linear_history import (
repository_default_branch_requires_linear_history,
)
check = repository_default_branch_requires_linear_history()
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 require linear history on default branch ({default_branch})."
)
def test_linear_history_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",
private=False,
default_branch=default_branch,
required_linear_history=True,
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_requires_linear_history.repository_default_branch_requires_linear_history.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_linear_history.repository_default_branch_requires_linear_history import (
repository_default_branch_requires_linear_history,
)
check = repository_default_branch_requires_linear_history()
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 require linear history on default branch ({default_branch})."
)
@@ -4,7 +4,7 @@ from prowler.providers.github.services.repository.repository_service import Repo
from tests.providers.github.github_fixtures import set_mocked_github_provider
class Test_repository_code_changes_multi_approval_requirement:
class Test_repository_default_branch_requires_multiple_approvals:
def test_no_repositories(self):
repository_client = mock.MagicMock
repository_client.repositories = {}
@@ -15,15 +15,15 @@ class Test_repository_code_changes_multi_approval_requirement:
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",
"prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.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,
from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import (
repository_default_branch_requires_multiple_approvals,
)
check = repository_code_changes_multi_approval_requirement()
check = repository_default_branch_requires_multiple_approvals()
result = check.execute()
assert len(result) == 0
@@ -50,15 +50,15 @@ class Test_repository_code_changes_multi_approval_requirement:
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",
"prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.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,
from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import (
repository_default_branch_requires_multiple_approvals,
)
check = repository_code_changes_multi_approval_requirement()
check = repository_default_branch_requires_multiple_approvals()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
@@ -92,15 +92,15 @@ class Test_repository_code_changes_multi_approval_requirement:
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",
"prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.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,
from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import (
repository_default_branch_requires_multiple_approvals,
)
check = repository_code_changes_multi_approval_requirement()
check = repository_default_branch_requires_multiple_approvals()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
@@ -134,15 +134,15 @@ class Test_repository_code_changes_multi_approval_requirement:
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",
"prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals.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,
from prowler.providers.github.services.repository.repository_default_branch_requires_multiple_approvals.repository_default_branch_requires_multiple_approvals import (
repository_default_branch_requires_multiple_approvals,
)
check = repository_code_changes_multi_approval_requirement()
check = repository_default_branch_requires_multiple_approvals()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
@@ -18,6 +18,7 @@ def mock_list_repositories(_):
private=False,
securitymd=True,
require_pull_request=True,
required_linear_history=True,
approval_count=2,
),
}
@@ -44,5 +45,6 @@ class Test_Repository_Service:
assert repository_service.repositories[1].private is False
assert repository_service.repositories[1].default_branch == "main"
assert repository_service.repositories[1].securitymd
assert repository_service.repositories[1].required_linear_history
assert repository_service.repositories[1].require_pull_request
assert repository_service.repositories[1].approval_count == 2