feat(repository): add new check repository_default_branch_requires_signed_commits (#7777)

Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
This commit is contained in:
Andoni AlonsoandSergio Garcia authored and GitHub committed 2025-05-22 12:45:13 +02:00
1 parent 7ebc5d3c31
commit 99164ce93e
6 files changed
+186

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add a level for Prowler ThreatScore in the accordion in Dashboard. [(#7739)](https://github.com/prowler-cloud/prowler/pull/7739)
- Add CIS 4.0 compliance framework for GCP. [(7785)](https://github.com/prowler-cloud/prowler/pull/7785)
- Add `repository_has_codeowners_file` check for GitHub provider. [(#7752)](https://github.com/prowler-cloud/prowler/pull/7752)
- Add `repository_default_branch_requires_signed_commits` check for GitHub provider. [(#7777)](https://github.com/prowler-cloud/prowler/pull/7777)
- Add `repository_dependency_scanning_enabled` check for GitHub provider. [(#7771)](https://github.com/prowler-cloud/prowler/pull/7771)
- Add `repository_secret_scanning_enabled` check for GitHub provider. [(#7759)](https://github.com/prowler-cloud/prowler/pull/7759)
- Add `repository_default_branch_requires_codeowners_review` check for GitHub provider. [(#7753)](https://github.com/prowler-cloud/prowler/pull/7753)
@@ -0,0 +1,30 @@
{
"Provider": "github",
"CheckID": "repository_default_branch_requires_signed_commits",
"CheckTitle": "Check if repository requires signed commits",
"CheckType": [],
"ServiceName": "repository",
"SubServiceName": "",
"ResourceIdTemplate": "github:user-id:repository/repository-name",
"Severity": "medium",
"ResourceType": "GitHubRepository",
"Description": "Ensure that every commit in a pull request is signed and verified before merging to the default branch.",
"Risk": "If repositories do not require signed commits, there is no way to verify the authenticity and integrity of code changes. This could allow malicious actors to impersonate legitimate contributors and introduce unauthorized or harmful changes to the codebase.",
"RelatedUrl": "https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable the 'Require signed commits' option in branch protection rules to ensure that all commits are cryptographically signed and verified before they can be merged.",
"Url": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-signed-commits"
}
},
"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_signed_commits(Check):
"""Check if a repository requires signed commits
This class verifies whether each repository requires signed commits for the default branch.
"""
def execute(self) -> List[CheckReportGithub]:
"""Execute the Github Repository Requires Signed Commits check
Iterates over all repositories and checks if they require signed commits.
Returns:
List[CheckReportGithub]: A list of reports for each repository
"""
findings = []
for repo in repository_client.repositories.values():
if repo.require_signed_commits 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 signed commits on default branch ({repo.default_branch})."
if repo.require_signed_commits:
report.status = "PASS"
report.status_extended = f"Repository {repo.name} does require signed commits on default branch ({repo.default_branch})."
findings.append(report)
return findings
@@ -61,6 +61,7 @@ class Repository(GithubService):
allow_force_pushes = True
branch_deletion = True
require_code_owner_reviews = False
require_signed_commits = False
status_checks = False
enforce_admins = False
conversation_resolution = False
@@ -95,6 +96,9 @@ class Repository(GithubService):
if require_pr
else False
)
require_signed_commits = (
branch.get_required_signatures()
)
except Exception as error:
# If the branch is not found, it is not protected
if "404" in str(error):
@@ -110,6 +114,7 @@ class Repository(GithubService):
allow_force_pushes = None
branch_deletion = None
require_code_owner_reviews = None
require_signed_commits = None
status_checks = None
enforce_admins = None
conversation_resolution = None
@@ -171,6 +176,7 @@ class Repository(GithubService):
codeowners_exists=codeowners_exists,
require_code_owner_reviews=require_code_owner_reviews,
secret_scanning_enabled=secret_scanning_enabled,
require_signed_commits=require_signed_commits,
dependabot_alerts_enabled=dependabot_alerts_enabled,
delete_branch_on_merge=delete_branch_on_merge,
)
@@ -202,6 +208,7 @@ class Repo(BaseModel):
codeowners_exists: Optional[bool]
require_code_owner_reviews: Optional[bool]
secret_scanning_enabled: Optional[bool]
require_signed_commits: Optional[bool]
dependabot_alerts_enabled: Optional[bool]
delete_branch_on_merge: Optional[bool]
conversation_resolution: Optional[bool]
@@ -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_signed_commits:
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_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) == 0
def test_signed_commits_not_required(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,
require_signed_commits=False,
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_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].resource_id == 1
assert result[0].resource_name == repo_name
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Repository {repo_name} does not require signed commits on default branch ({default_branch})."
)
def test_signed_commits_required(self):
repository_client = mock.MagicMock
repo_name = "repo2"
default_branch = "main"
repository_client.repositories = {
2: Repo(
id=2,
name=repo_name,
full_name="account-name/repo2",
private=False,
default_branch=default_branch,
require_signed_commits=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_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].resource_id == 2
assert result[0].resource_name == repo_name
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Repository {repo_name} does require signed commits on default branch ({default_branch})."
)