diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 73373c7414..804a4086e9 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -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) diff --git a/prowler/providers/github/services/repository/repository_default_branch_requires_signed_commits/__init__.py b/prowler/providers/github/services/repository/repository_default_branch_requires_signed_commits/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/repository/repository_default_branch_requires_signed_commits/repository_default_branch_requires_signed_commits.metadata.json b/prowler/providers/github/services/repository/repository_default_branch_requires_signed_commits/repository_default_branch_requires_signed_commits.metadata.json new file mode 100644 index 0000000000..2b0ddb8c8a --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_requires_signed_commits/repository_default_branch_requires_signed_commits.metadata.json @@ -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": "" +} diff --git a/prowler/providers/github/services/repository/repository_default_branch_requires_signed_commits/repository_default_branch_requires_signed_commits.py b/prowler/providers/github/services/repository/repository_default_branch_requires_signed_commits/repository_default_branch_requires_signed_commits.py new file mode 100644 index 0000000000..1eb5b41be6 --- /dev/null +++ b/prowler/providers/github/services/repository/repository_default_branch_requires_signed_commits/repository_default_branch_requires_signed_commits.py @@ -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 diff --git a/prowler/providers/github/services/repository/repository_service.py b/prowler/providers/github/services/repository/repository_service.py index 01e7713b56..49cc11177e 100644 --- a/prowler/providers/github/services/repository/repository_service.py +++ b/prowler/providers/github/services/repository/repository_service.py @@ -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] diff --git a/tests/providers/github/services/repository/repository_default_branch_requires_signed_commits/repository_default_branch_requires_signed_commits_test.py b/tests/providers/github/services/repository/repository_default_branch_requires_signed_commits/repository_default_branch_requires_signed_commits_test.py new file mode 100644 index 0000000000..dd177f4288 --- /dev/null +++ b/tests/providers/github/services/repository/repository_default_branch_requires_signed_commits/repository_default_branch_requires_signed_commits_test.py @@ -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})." + )