diff --git a/README.md b/README.md index 221a29d35e..7fef33c6ae 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ prowler dashboard | GCP | 79 | 13 | 7 | 3 | | Azure | 140 | 18 | 8 | 3 | | Kubernetes | 83 | 7 | 4 | 7 | -| GitHub | 2 | 1 | 1 | 0 | +| GitHub | 3 | 2 | 1 | 0 | | M365 | 44 | 2 | 2 | 0 | | NHN (Unofficial) | 6 | 2 | 1 | 0 | diff --git a/poetry.lock b/poetry.lock index 2734a604dd..01c3b39bd3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3969,6 +3969,32 @@ cffi = ">=1.4.1" docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + [[package]] name = "pyparsing" version = "3.2.3" diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index fdd357b1c2..3baeb7ceb8 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - 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 `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) ### Fixed diff --git a/prowler/__main__.py b/prowler/__main__.py index 6aa74c98ab..e22d7b7fa6 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -799,7 +799,6 @@ def prowler(): cis = GithubCIS( findings=finding_outputs, compliance=bulk_compliance_frameworks[compliance_name], - create_file_descriptor=True, file_path=filename, ) generated_outputs["compliance"].append(cis) diff --git a/prowler/providers/github/services/organization/organization_members_mfa_required/__init__.py b/prowler/providers/github/services/organization/organization_members_mfa_required/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.metadata.json b/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.metadata.json new file mode 100644 index 0000000000..cb1d67e96e --- /dev/null +++ b/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "github", + "CheckID": "organization_members_mfa_required", + "CheckTitle": "Check if organization members are required to have MFA enabled.", + "CheckType": [], + "ServiceName": "organization", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "GitHubOrganization", + "Description": "Ensure that all organization members are required to have multi-factor authentication (MFA) enabled. Enforcing MFA for all organization members helps protect the organization's resources and data from unauthorized access and security breaches.", + "Risk": "Without Multi-Factor Authentication (MFA), user accounts are vulnerable to unauthorized access if their passwords are compromised. This can lead to unauthorized actions such as data theft, malicious code commits, and repository manipulation, potentially compromising the organization's source code and intellectual property.", + "RelatedUrl": "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-two-factor-authentication-for-your-organization/requiring-two-factor-authentication-in-your-organization", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Mandate the use of MFA for all organization members. This significantly enhances account security by adding an additional layer of protection beyond a username and password. MFA ensures that even if a password is compromised, unauthorized access to user accounts and repositories is prevented, safeguarding sensitive data and critical assets.", + "Url": "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-two-factor-authentication-for-your-organization/preparing-to-require-two-factor-authentication-in-your-organization" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.py b/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.py new file mode 100644 index 0000000000..e18c127c0d --- /dev/null +++ b/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.py @@ -0,0 +1,36 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGithub +from prowler.providers.github.services.organization.organization_client import ( + organization_client, +) + + +class organization_members_mfa_required(Check): + """Check if organization members are required to have two-factor authentication enabled. + + This class verifies whether each organization requires its members to have two-factor authentication enabled. + """ + + def execute(self) -> List[CheckReportGithub]: + """Execute the Github Organization Members MFA Required check. + + Iterates over all organizations and checks if members are required to have two-factor authentication enabled. + + Returns: + List[CheckReportGithub]: A list of reports for each repository + """ + findings = [] + for org in organization_client.organizations.values(): + if org.mfa_required is not None: + report = CheckReportGithub(metadata=self.metadata(), resource=org) + report.status = "FAIL" + report.status_extended = f"Organization {org.name} does not require members to have two-factor authentication enabled." + + if org.mfa_required: + report.status = "PASS" + report.status_extended = f"Organization {org.name} does require members to have two-factor authentication enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/github/services/organization/organization_service.py b/prowler/providers/github/services/organization/organization_service.py index 64c2e7c0d8..3f8f8dca95 100644 --- a/prowler/providers/github/services/organization/organization_service.py +++ b/prowler/providers/github/services/organization/organization_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic import BaseModel from prowler.lib.logger import logger @@ -15,9 +17,17 @@ class Organization(GithubService): try: for client in self.clients: for org in client.get_user().get_orgs(): + try: + require_mfa = org.two_factor_requirement_enabled + except Exception as error: + require_mfa = None + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) organizations[org.id] = Org( id=org.id, name=org.login, + mfa_required=require_mfa, ) except Exception as error: logger.error( @@ -31,3 +41,4 @@ class Org(BaseModel): id: int name: str + mfa_required: Optional[bool] = False diff --git a/tests/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required_test.py b/tests/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required_test.py new file mode 100644 index 0000000000..823c0f294f --- /dev/null +++ b/tests/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required_test.py @@ -0,0 +1,100 @@ +from unittest import mock + +from prowler.providers.github.services.organization.organization_service import Org +from tests.providers.github.github_fixtures import set_mocked_github_provider + + +class Test_organization_members_mfa_required: + def test_no_organizations(self): + organization_client = mock.MagicMock + organization_client.organizations = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_members_mfa_required.organization_members_mfa_required.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_members_mfa_required.organization_members_mfa_required import ( + organization_members_mfa_required, + ) + + check = organization_members_mfa_required() + result = check.execute() + assert len(result) == 0 + + def test_organization_mfa_disabled(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + mfa_required=False, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_members_mfa_required.organization_members_mfa_required.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_members_mfa_required.organization_members_mfa_required import ( + organization_members_mfa_required, + ) + + check = organization_members_mfa_required() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == "test-organization" + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Organization {org_name} does not require members to have two-factor authentication enabled." + ) + + def test_one_organization_securitymd(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + mfa_required=True, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_members_mfa_required.organization_members_mfa_required.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_members_mfa_required.organization_members_mfa_required import ( + organization_members_mfa_required, + ) + + check = organization_members_mfa_required() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == "test-organization" + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Organization {org_name} does require members to have two-factor authentication enabled." + ) diff --git a/tests/providers/github/services/organization/organization_service_test.py b/tests/providers/github/services/organization/organization_service_test.py index 02dabf80ff..7720841698 100644 --- a/tests/providers/github/services/organization/organization_service_test.py +++ b/tests/providers/github/services/organization/organization_service_test.py @@ -12,6 +12,7 @@ def mock_list_organizations(_): 1: Org( id=1, name="test-organization", + mfa_required=True, ), } @@ -33,3 +34,4 @@ class Test_Repository_Service: repository_service = Organization(set_mocked_github_provider()) assert len(repository_service.organizations) == 1 assert repository_service.organizations[1].name == "test-organization" + assert repository_service.organizations[1].mfa_required