diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 64db6f51f3..d9bba504f0 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -26,6 +26,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### Fixed - AWS resource-arn filtering [(#8533)](https://github.com/prowler-cloud/prowler/pull/8533) - GitHub App authentication for GitHub provider [(#8529)](https://github.com/prowler-cloud/prowler/pull/8529) +- List all accessible organizations in GitHub provider [(#8535)](https://github.com/prowler-cloud/prowler/pull/8535) --- diff --git a/prowler/providers/github/services/organization/organization_service.py b/prowler/providers/github/services/organization/organization_service.py index cf57f2c799..9bbc5f2671 100644 --- a/prowler/providers/github/services/organization/organization_service.py +++ b/prowler/providers/github/services/organization/organization_service.py @@ -5,6 +5,7 @@ from pydantic.v1 import BaseModel from prowler.lib.logger import logger from prowler.providers.github.lib.service.service import GithubService +from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo class Organization(GithubService): @@ -113,8 +114,15 @@ class Organization(GithubService): elif not self.provider.repositories: # Default behavior: get all organizations the user is a member of # Only when no repositories are specified - for org in client.get_user().get_orgs(): - self._process_organization(org, organizations) + if isinstance(self.provider.identity, GithubIdentityInfo): + orgs = client.get_user().get_orgs() + for org in orgs: + self._process_organization(org, organizations) + elif isinstance(self.provider.identity, GithubAppIdentityInfo): + orgs = client.get_organizations() + if orgs.totalCount > 0: + for org in orgs: + self._process_organization(org, organizations) except github.RateLimitExceededException as error: logger.error(f"GitHub API rate limit exceeded: {error}") diff --git a/tests/providers/github/services/organization/organization_service_test.py b/tests/providers/github/services/organization/organization_service_test.py index a51373b8c7..45b85198de 100644 --- a/tests/providers/github/services/organization/organization_service_test.py +++ b/tests/providers/github/services/organization/organization_service_test.py @@ -63,7 +63,12 @@ class Test_Organization_Scoping: mock_client = MagicMock() mock_user = MagicMock() - mock_user.get_orgs.return_value = [self.mock_org1, self.mock_org2] + mock_orgs = MagicMock() + mock_orgs.totalCount = 2 + mock_orgs.__iter__ = MagicMock( + return_value=iter([self.mock_org1, self.mock_org2]) + ) + mock_user.get_orgs.return_value = mock_orgs mock_client.get_user.return_value = mock_user with patch( @@ -81,6 +86,95 @@ class Test_Organization_Scoping: assert orgs[1].name == "test-org1" assert orgs[2].name == "test-org2" + def test_no_organizations_found_for_user(self): + """Test that when user has no organizations, empty dict is returned""" + provider = set_mocked_github_provider() + provider.repositories = [] + provider.organizations = [] + + mock_client = MagicMock() + mock_user = MagicMock() + mock_orgs = MagicMock() + mock_orgs.totalCount = 0 + mock_user.get_orgs.return_value = mock_orgs + mock_client.get_user.return_value = mock_user + + with patch( + "prowler.providers.github.services.organization.organization_service.GithubService.__init__" + ): + organization_service = Organization(provider) + organization_service.clients = [mock_client] + organization_service.provider = provider + + orgs = organization_service._list_organizations() + + assert len(orgs) == 0 + + def test_github_app_organization_scoping(self): + """Test GitHub App organization listing""" + from prowler.providers.github.models import GithubAppIdentityInfo + + provider = set_mocked_github_provider() + provider.repositories = [] + provider.organizations = [] + # Set GitHub App identity + provider.identity = GithubAppIdentityInfo( + app_id="test-app-id", + app_name="test-app", + installations=["test-org1", "test-org2"], + ) + + mock_client = MagicMock() + mock_orgs = MagicMock() + mock_orgs.totalCount = 2 + mock_orgs.__iter__ = MagicMock( + return_value=iter([self.mock_org1, self.mock_org2]) + ) + mock_client.get_organizations.return_value = mock_orgs + + with patch( + "prowler.providers.github.services.organization.organization_service.GithubService.__init__" + ): + organization_service = Organization(provider) + organization_service.clients = [mock_client] + organization_service.provider = provider + + orgs = organization_service._list_organizations() + + assert len(orgs) == 2 + assert 1 in orgs + assert 2 in orgs + assert orgs[1].name == "test-org1" + assert orgs[2].name == "test-org2" + + def test_github_app_no_organizations_found(self): + """Test GitHub App when no organizations are accessible""" + from prowler.providers.github.models import GithubAppIdentityInfo + + provider = set_mocked_github_provider() + provider.repositories = [] + provider.organizations = [] + # Set GitHub App identity + provider.identity = GithubAppIdentityInfo( + app_id="test-app-id", app_name="test-app", installations=[] + ) + + mock_client = MagicMock() + mock_orgs = MagicMock() + mock_orgs.totalCount = 0 + mock_client.get_organizations.return_value = mock_orgs + + with patch( + "prowler.providers.github.services.organization.organization_service.GithubService.__init__" + ): + organization_service = Organization(provider) + organization_service.clients = [mock_client] + organization_service.provider = provider + + orgs = organization_service._list_organizations() + + assert len(orgs) == 0 + def test_specific_organization_scoping(self): """Test that only specified organizations are returned""" provider = set_mocked_github_provider()