feat(github): support repository rulesets in default branch protection checks (#11723)

This commit is contained in:
Daniel Barranquero
2026-06-30 13:53:32 +01:00
committed by GitHub
parent aba43440ca
commit 2abcb05e22
32 changed files with 1821 additions and 47 deletions
@@ -155,3 +155,123 @@ class Test_repository_default_branch_deletion_disabled_test:
result[0].status_extended
== f"Repository {repo_name} does deny default branch deletion."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = Branch(
name="main",
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=False,
branch_deletion_source="ruleset",
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
)
now = datetime.now(timezone.utc)
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=default_branch,
private=False,
archived=False,
pushed_at=now,
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_deletion_disabled.repository_default_branch_deletion_disabled.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_deletion_disabled.repository_default_branch_deletion_disabled import (
repository_default_branch_deletion_disabled,
)
check = repository_default_branch_deletion_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = Branch(
name="main",
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
branch_deletion_source="ruleset_not_active",
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
)
now = datetime.now(timezone.utc)
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=default_branch,
private=False,
archived=False,
pushed_at=now,
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_deletion_disabled.repository_default_branch_deletion_disabled.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_deletion_disabled.repository_default_branch_deletion_disabled import (
repository_default_branch_deletion_disabled,
)
check = repository_default_branch_deletion_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has default branch deletion disabled in a ruleset, but the ruleset is not active."
)
@@ -149,3 +149,119 @@ class Test_repository_default_branch_disallows_force_push_test:
result[0].status_extended
== f"Repository {repo_name} does deny force pushes on default branch ({default_branch})."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=True,
default_branch=True,
require_pull_request=True,
approval_count=1,
required_linear_history=True,
allow_force_pushes=False,
allow_force_pushes_source="ruleset",
branch_deletion=False,
status_checks=True,
enforce_admins=True,
require_code_owner_reviews=True,
require_signed_commits=True,
conversation_resolution=True,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=True,
secret_scanning_enabled=True,
dependabot_alerts_enabled=True,
delete_branch_on_merge=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_disallows_force_push.repository_default_branch_disallows_force_push.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_disallows_force_push.repository_default_branch_disallows_force_push import (
repository_default_branch_disallows_force_push,
)
check = repository_default_branch_disallows_force_push()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
allow_force_pushes_source="ruleset_not_active",
branch_deletion=True,
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_disallows_force_push.repository_default_branch_disallows_force_push.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_disallows_force_push.repository_default_branch_disallows_force_push import (
repository_default_branch_disallows_force_push,
)
check = repository_default_branch_disallows_force_push()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has force pushes disallowed in a ruleset on default branch ({default_branch}), but the ruleset is not active."
)
@@ -149,3 +149,119 @@ class Test_repository_default_branch_protection_applies_to_admins_test:
result[0].status_extended
== f"Repository {repo_name} does enforce administrators to be subject to the same branch protection rules as other users."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=True,
default_branch=True,
require_pull_request=True,
approval_count=1,
required_linear_history=True,
allow_force_pushes=False,
branch_deletion=False,
status_checks=True,
enforce_admins=True,
enforce_admins_source="ruleset",
require_code_owner_reviews=True,
require_signed_commits=True,
conversation_resolution=True,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=True,
secret_scanning_enabled=True,
dependabot_alerts_enabled=True,
delete_branch_on_merge=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_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins import (
repository_default_branch_protection_applies_to_admins,
)
check = repository_default_branch_protection_applies_to_admins()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
enforce_admins=False,
enforce_admins_source="ruleset_not_active",
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_protection_applies_to_admins.repository_default_branch_protection_applies_to_admins import (
repository_default_branch_protection_applies_to_admins,
)
check = repository_default_branch_protection_applies_to_admins()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has a ruleset that would apply to administrators, but the ruleset is not active."
)
@@ -149,3 +149,119 @@ class Test_repository_default_branch_protection_enabled_test:
result[0].status_extended
== f"Repository {repo_name} does enforce branch protection on default branch ({default_branch})."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=True,
protected_source="ruleset",
default_branch=True,
require_pull_request=True,
approval_count=1,
required_linear_history=True,
allow_force_pushes=False,
branch_deletion=False,
status_checks=True,
enforce_admins=True,
require_code_owner_reviews=True,
require_signed_commits=True,
conversation_resolution=True,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=True,
secret_scanning_enabled=True,
dependabot_alerts_enabled=True,
delete_branch_on_merge=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_protection_enabled.repository_default_branch_protection_enabled.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_protection_enabled.repository_default_branch_protection_enabled import (
repository_default_branch_protection_enabled,
)
check = repository_default_branch_protection_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=False,
protected_source="ruleset_not_active",
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_protection_enabled.repository_default_branch_protection_enabled.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_protection_enabled.repository_default_branch_protection_enabled import (
repository_default_branch_protection_enabled,
)
check = repository_default_branch_protection_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has a ruleset configured on default branch ({default_branch}), but the ruleset is not active."
)
@@ -147,3 +147,117 @@ class Test_repository_default_branch_requires_codeowners_review:
result[0].status_extended
== f"Repository {repo_name} requires code owner approval for changes to owned code."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name="main",
protected=True,
default_branch=True,
require_pull_request=True,
approval_count=1,
required_linear_history=True,
allow_force_pushes=False,
branch_deletion=False,
status_checks=True,
enforce_admins=True,
require_code_owner_reviews=True,
require_code_owner_reviews_source="ruleset",
require_signed_commits=True,
conversation_resolution=True,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=True,
secret_scanning_enabled=True,
dependabot_alerts_enabled=True,
delete_branch_on_merge=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].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name="main",
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_code_owner_reviews_source="ruleset_not_active",
require_signed_commits=False,
conversation_resolution=False,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has code owner approval configured in a ruleset, but the ruleset is not active."
)
@@ -149,3 +149,119 @@ class Test_repository_default_branch_requires_conversation_resolution_test:
result[0].status_extended
== f"Repository {repo_name} does require conversation resolution on default branch ({default_branch})."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=True,
default_branch=True,
require_pull_request=True,
approval_count=1,
required_linear_history=True,
allow_force_pushes=False,
branch_deletion=False,
status_checks=True,
enforce_admins=True,
require_code_owner_reviews=True,
require_signed_commits=True,
conversation_resolution=True,
conversation_resolution_source="ruleset",
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=True,
secret_scanning_enabled=True,
dependabot_alerts_enabled=True,
delete_branch_on_merge=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_conversation_resolution.repository_default_branch_requires_conversation_resolution.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_conversation_resolution.repository_default_branch_requires_conversation_resolution import (
repository_default_branch_requires_conversation_resolution,
)
check = repository_default_branch_requires_conversation_resolution()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
conversation_resolution_source="ruleset_not_active",
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_conversation_resolution.repository_default_branch_requires_conversation_resolution.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_conversation_resolution.repository_default_branch_requires_conversation_resolution import (
repository_default_branch_requires_conversation_resolution,
)
check = repository_default_branch_requires_conversation_resolution()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has conversation resolution configured in a ruleset on default branch ({default_branch}), but the ruleset is not active."
)
@@ -149,3 +149,119 @@ class Test_repository_default_branch_requires_linear_history_test:
result[0].status_extended
== f"Repository {repo_name} does require linear history on default branch ({default_branch})."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=True,
default_branch=True,
require_pull_request=True,
approval_count=1,
required_linear_history=True,
required_linear_history_source="ruleset",
allow_force_pushes=False,
branch_deletion=False,
status_checks=True,
enforce_admins=True,
require_code_owner_reviews=True,
require_signed_commits=True,
conversation_resolution=True,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=True,
secret_scanning_enabled=True,
dependabot_alerts_enabled=True,
delete_branch_on_merge=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].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
required_linear_history_source="ruleset_not_active",
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has linear history configured in a ruleset on default branch ({default_branch}), but the ruleset is not active."
)
@@ -207,3 +207,117 @@ class Test_repository_default_branch_requires_multiple_approvals:
result[0].status_extended
== f"Repository {repo_name} does enforce at least 2 approvals for code changes."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name="main",
protected=True,
default_branch=True,
require_pull_request=True,
approval_count=2,
approval_count_source="ruleset",
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_multiple_approvals.repository_default_branch_requires_multiple_approvals.repository_client",
new=repository_client,
),
):
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_default_branch_requires_multiple_approvals()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name="main",
protected=False,
default_branch=True,
require_pull_request=True,
approval_count=0,
approval_count_source="ruleset_not_active",
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_multiple_approvals.repository_default_branch_requires_multiple_approvals.repository_client",
new=repository_client,
),
):
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_default_branch_requires_multiple_approvals()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has at least 2 approvals configured in a ruleset, but the ruleset is not active."
)
@@ -149,3 +149,119 @@ class Test_repository_default_branch_requires_signed_commits:
result[0].status_extended
== f"Repository {repo_name} does require signed commits on default branch ({default_branch})."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=True,
default_branch=True,
require_pull_request=True,
approval_count=1,
required_linear_history=True,
allow_force_pushes=False,
branch_deletion=False,
status_checks=True,
enforce_admins=True,
require_code_owner_reviews=True,
require_signed_commits=True,
require_signed_commits_source="ruleset",
conversation_resolution=True,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=True,
secret_scanning_enabled=True,
dependabot_alerts_enabled=True,
delete_branch_on_merge=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_signed_commits.repository_default_branch_requires_signed_commits.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_signed_commits.repository_default_branch_requires_signed_commits import (
repository_default_branch_requires_signed_commits,
)
check = repository_default_branch_requires_signed_commits()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
require_signed_commits_source="ruleset_not_active",
conversation_resolution=False,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_signed_commits.repository_default_branch_requires_signed_commits.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_requires_signed_commits.repository_default_branch_requires_signed_commits import (
repository_default_branch_requires_signed_commits,
)
check = repository_default_branch_requires_signed_commits()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has signed commits configured in a ruleset on default branch ({default_branch}), but the ruleset is not active."
)
@@ -151,3 +151,121 @@ class Test_repository_default_branch_status_checks_required_test:
result[0].status_extended
== f"Repository {repo_name} does enforce status checks."
)
def test_active_ruleset_passes(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
private=False,
default_branch=Branch(
name=default_branch,
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=True,
status_checks_source="ruleset",
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
),
status_checks=True,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=True,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_status_checks_required.repository_default_branch_status_checks_required.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_status_checks_required.repository_default_branch_status_checks_required import (
repository_default_branch_status_checks_required,
)
check = repository_default_branch_status_checks_required()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
def test_inactive_ruleset_fails(self):
repository_client = mock.MagicMock
repo_name = "repo1"
default_branch = "main"
repository_client.repositories = {
1: Repo(
id=1,
name=repo_name,
owner="account-name",
full_name="account-name/repo1",
default_branch=Branch(
name=default_branch,
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
status_checks_source="ruleset_not_active",
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
),
status_checks=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
private=False,
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=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_status_checks_required.repository_default_branch_status_checks_required.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_default_branch_status_checks_required.repository_default_branch_status_checks_required import (
repository_default_branch_status_checks_required,
)
check = repository_default_branch_status_checks_required()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} has status checks configured in a ruleset, but the ruleset is not active."
)
@@ -464,7 +464,7 @@ class Test_Repository_ErrorHandling:
assert "Rate limit exceeded" in str(mock_logger.error.call_args)
class Test_Repository_DismissStaleReviewsRulesets:
class Test_Repository_BranchProtectionRulesets:
def setup_method(self):
self.repository_service = Repository.__new__(Repository)
self.repository_service.provider = set_mocked_github_provider()
@@ -550,6 +550,27 @@ class Test_Repository_DismissStaleReviewsRulesets:
],
}
def _build_ruleset(
self,
*,
enforcement,
include,
rules,
bypass_actors=None,
ruleset_id=201,
):
return {
"id": ruleset_id,
"name": "Branch protection ruleset",
"target": "branch",
"source_type": "Repository",
"source": "owner1/repo1",
"enforcement": enforcement,
"bypass_actors": bypass_actors or [],
"conditions": {"ref_name": {"include": include, "exclude": []}},
"rules": rules,
}
def test_process_repository_uses_classic_branch_protection(self):
repo = self._build_repo(branch_protected=True, dismiss_stale_reviews=True)
repos = {}
@@ -606,3 +627,320 @@ class Test_Repository_DismissStaleReviewsRulesets:
assert (
repos[1].default_branch.dismiss_stale_reviews_source == "ruleset_not_active"
)
def test_ruleset_non_fast_forward_disallows_force_push(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "non_fast_forward"}],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.allow_force_pushes is False
assert repos[1].default_branch.allow_force_pushes_source == "ruleset"
def test_ruleset_non_fast_forward_inactive_keeps_force_push_allowed(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="disabled",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "non_fast_forward"}],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.allow_force_pushes is True
assert repos[1].default_branch.allow_force_pushes_source == "ruleset_not_active"
def test_classic_protection_takes_precedence_over_inactive_ruleset(self):
# Classic protection already disallows force pushes, so an inactive ruleset
# must not downgrade the result.
repo = self._build_repo(
branch_protected=True,
ruleset_details=[
self._build_ruleset(
enforcement="disabled",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "non_fast_forward"}],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.allow_force_pushes is False
assert repos[1].default_branch.allow_force_pushes_source == "classic"
def test_ruleset_required_signatures_requires_signed_commits(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "required_signatures"}],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.require_signed_commits is True
assert repos[1].default_branch.require_signed_commits_source == "ruleset"
def test_ruleset_required_linear_history(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "required_linear_history"}],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.required_linear_history is True
assert repos[1].default_branch.required_linear_history_source == "ruleset"
def test_ruleset_required_status_checks(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[
{
"type": "required_status_checks",
"parameters": {
"required_status_checks": [{"context": "ci/build"}],
"strict_required_status_checks_policy": True,
},
}
],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.status_checks is True
assert repos[1].default_branch.status_checks_source == "ruleset"
def test_ruleset_required_status_checks_without_configured_checks(self):
# A required_status_checks rule with an empty list enforces nothing, so it
# must not be treated as a passing status-checks requirement.
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[
{
"type": "required_status_checks",
"parameters": {
"required_status_checks": [],
"strict_required_status_checks_policy": True,
},
}
],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.status_checks is False
assert repos[1].default_branch.status_checks_source is None
def test_ruleset_deletion_disables_branch_deletion(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "deletion"}],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.branch_deletion is False
assert repos[1].default_branch.branch_deletion_source == "ruleset"
def test_active_ruleset_marks_default_branch_protected(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "non_fast_forward"}],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.protected is True
assert repos[1].default_branch.protected_source == "ruleset"
def test_ruleset_pull_request_parameters_map_to_attributes(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[
{
"type": "pull_request",
"parameters": {
"dismiss_stale_reviews_on_push": False,
"require_code_owner_review": True,
"require_last_push_approval": False,
"required_approving_review_count": 2,
"required_review_thread_resolution": True,
},
}
],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
branch = repos[1].default_branch
assert branch.require_pull_request is True
assert branch.require_pull_request_source == "ruleset"
assert branch.require_code_owner_reviews is True
assert branch.require_code_owner_reviews_source == "ruleset"
assert branch.conversation_resolution is True
assert branch.conversation_resolution_source == "ruleset"
assert branch.approval_count == 2
assert branch.approval_count_source == "ruleset"
def test_inactive_ruleset_approval_count_is_fail_signal(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="disabled",
include=["~DEFAULT_BRANCH"],
rules=[
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 2,
},
}
],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.approval_count == 0
assert repos[1].default_branch.approval_count_source == "ruleset_not_active"
def test_active_ruleset_without_bypass_actors_applies_to_admins(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "non_fast_forward"}],
bypass_actors=[],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.enforce_admins is True
assert repos[1].default_branch.enforce_admins_source == "ruleset"
def test_active_ruleset_with_bypass_actors_does_not_apply_to_admins(self):
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="active",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "non_fast_forward"}],
bypass_actors=[
{
"actor_id": 1,
"actor_type": "RepositoryRole",
"bypass_mode": "always",
}
],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
# Admins can bypass, so the rulesets do not enforce protection for them.
assert repos[1].default_branch.enforce_admins is False
assert repos[1].default_branch.enforce_admins_source is None
def test_inactive_ruleset_with_bypass_actors_is_not_admin_fail_signal(self):
# A disabled ruleset that has bypass actors would not apply to admins even if
# activated, so it must not raise the enforce-admins ruleset_not_active signal.
repo = self._build_repo(
branch_protected=False,
ruleset_details=[
self._build_ruleset(
enforcement="disabled",
include=["~DEFAULT_BRANCH"],
rules=[{"type": "non_fast_forward"}],
bypass_actors=[
{
"actor_id": 1,
"actor_type": "RepositoryRole",
"bypass_mode": "always",
}
],
)
],
)
repos = {}
self.repository_service._process_repository(repo, repos)
assert repos[1].default_branch.enforce_admins is False
assert repos[1].default_branch.enforce_admins_source is None
# The branch is still reported as protected-but-inactive regardless of bypass.
assert repos[1].default_branch.protected_source == "ruleset_not_active"