From 3c599a75cc3be74056d55f1076fc783de73fa622 Mon Sep 17 00:00:00 2001 From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:23:30 +0200 Subject: [PATCH 01/33] feat(iam): add ECS privilege escalation patterns to IAM checks (#8541) --- prowler/CHANGELOG.md | 2 ++ .../aws/services/iam/lib/privilege_escalation.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index dec6f062ae..64db6f51f3 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to the **Prowler SDK** are documented in this file. - Bedrock AgentCore privilege escalation combination for AWS provider [(#8526)](https://github.com/prowler-cloud/prowler/pull/8526) - Remove standalone iam:PassRole from privesc detection and add missing patterns [(#8530)](https://github.com/prowler-cloud/prowler/pull/8530) - `eks_cluster_deletion_protection_enabled` check for AWS provider [(#8536)](https://github.com/prowler-cloud/prowler/pull/8536) +- ECS privilege escalation patterns (StartTask and RunTask) for AWS provider [(#8541)](https://github.com/prowler-cloud/prowler/pull/8541) + ### Changed - Refine kisa isms-p compliance mapping [(#8479)](https://github.com/prowler-cloud/prowler/pull/8479) diff --git a/prowler/providers/aws/services/iam/lib/privilege_escalation.py b/prowler/providers/aws/services/iam/lib/privilege_escalation.py index 9a4a67d3ab..b2545dfce0 100644 --- a/prowler/providers/aws/services/iam/lib/privilege_escalation.py +++ b/prowler/providers/aws/services/iam/lib/privilege_escalation.py @@ -106,6 +106,18 @@ privilege_escalation_policies_combination = { "bedrock-agentcore:CreateCodeInterpreter", "bedrock-agentcore:InvokeCodeInterpreter", }, + # ECS-based privilege escalation patterns + # Reference: https://labs.reversec.com/posts/2025/08/another-ecs-privilege-escalation-path + "PassRole+ECS+StartTask": { + "iam:PassRole", + "ecs:StartTask", + "ecs:RegisterContainerInstance", + "ecs:DeregisterContainerInstance", + }, + "PassRole+ECS+RunTask": { + "iam:PassRole", + "ecs:RunTask", + }, # TO-DO: We have to handle AssumeRole just if the resource is * and without conditions # "sts:AssumeRole": {"sts:AssumeRole"}, } From 55099abc8689aadb942fe8d27bfcbc03f41d0136 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:13:01 +0200 Subject: [PATCH 02/33] fix(organization): list all accessible organizations (#8535) Co-authored-by: MrCloudSec --- prowler/CHANGELOG.md | 1 + .../organization/organization_service.py | 12 ++- .../organization/organization_service_test.py | 96 ++++++++++++++++++- 3 files changed, 106 insertions(+), 3 deletions(-) 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() From 89e657561ce792f6e195675e8edb812f61611ae1 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:26:38 +0200 Subject: [PATCH 03/33] feat(github): add User Email and APP name/installations information (#8501) Co-authored-by: MrCloudSec --- prowler/CHANGELOG.md | 1 + prowler/lib/outputs/finding.py | 10 ++- prowler/lib/outputs/html/html.py | 75 +++++++++++++++---- prowler/providers/github/github_provider.py | 14 +++- prowler/providers/github/models.py | 4 + tests/lib/outputs/finding_test.py | 6 +- tests/lib/outputs/html/html_test.py | 44 ++++++++--- tests/providers/github/github_fixtures.py | 1 + .../providers/github/github_provider_test.py | 6 +- 9 files changed, 123 insertions(+), 38 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index d9bba504f0..6981aed941 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `vm_sufficient_daily_backup_retention_period` check for Azure provider [(#8200)](https://github.com/prowler-cloud/prowler/pull/8200) - `vm_jit_access_enabled` check for Azure provider [(#8202)](https://github.com/prowler-cloud/prowler/pull/8202) - Bedrock AgentCore privilege escalation combination for AWS provider [(#8526)](https://github.com/prowler-cloud/prowler/pull/8526) +- Add User Email and APP name/installations information in GitHub provider [(#8501)](https://github.com/prowler-cloud/prowler/pull/8501) - Remove standalone iam:PassRole from privesc detection and add missing patterns [(#8530)](https://github.com/prowler-cloud/prowler/pull/8530) - `eks_cluster_deletion_protection_enabled` check for AWS provider [(#8536)](https://github.com/prowler-cloud/prowler/pull/8536) - ECS privilege escalation patterns (StartTask and RunTask) for AWS provider [(#8541)](https://github.com/prowler-cloud/prowler/pull/8541) diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index a3ba6271dd..c7e1e1f05b 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -19,6 +19,7 @@ from prowler.lib.outputs.compliance.compliance import get_check_compliance from prowler.lib.outputs.utils import unroll_tags from prowler.lib.utils.utils import dict_to_lowercase, get_nested_attribute from prowler.providers.common.provider import Provider +from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo class Finding(BaseModel): @@ -250,15 +251,16 @@ class Finding(BaseModel): output_data["resource_name"] = check_output.resource_name output_data["resource_uid"] = check_output.resource_id - if hasattr(provider.identity, "account_name"): + if isinstance(provider.identity, GithubIdentityInfo): # GithubIdentityInfo (Personal Access Token, OAuth) output_data["account_name"] = provider.identity.account_name output_data["account_uid"] = provider.identity.account_id - elif hasattr(provider.identity, "app_id"): + output_data["account_email"] = provider.identity.account_email + elif isinstance(provider.identity, GithubAppIdentityInfo): # GithubAppIdentityInfo (GitHub App) - # TODO: Get Github App name - output_data["account_name"] = f"app-{provider.identity.app_id}" + output_data["account_name"] = provider.identity.app_name output_data["account_uid"] = provider.identity.app_id + output_data["installations"] = provider.identity.installations output_data["region"] = check_output.owner diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 7b4fb87601..d7006b8cad 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -41,7 +41,7 @@ class HTML(Output): {finding_status} {finding.metadata.Severity.value} {finding.metadata.ServiceName} - {":".join([finding.resource_metadata['file_path'], "-".join(map(str, finding.resource_metadata['file_line_range']))]) if finding.metadata.Provider == "iac" else finding.region.lower()} + {":".join([finding.resource_metadata["file_path"], "-".join(map(str, finding.resource_metadata["file_line_range"]))]) if finding.metadata.Provider == "iac" else finding.region.lower()} {finding.metadata.CheckID.replace("_", "_")} {finding.metadata.CheckTitle} {finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "_")} @@ -558,10 +558,65 @@ class HTML(Output): try: if hasattr(provider.identity, "account_name"): # GithubIdentityInfo (Personal Access Token, OAuth) - account_display = provider.identity.account_name + account_info_items = f""" +
  • + GitHub account: {provider.identity.account_name} +
  • + """ + # Add email if available + if ( + hasattr(provider.identity, "account_email") + and provider.identity.account_email + ): + account_info_items += f""" +
  • + GitHub account email: {provider.identity.account_email} +
  • """ elif hasattr(provider.identity, "app_id"): # GithubAppIdentityInfo (GitHub App) - account_display = f"app-{provider.identity.app_id}" + # Assessment items: App Name and Installations + account_info_items = f""" +
  • + GitHub App Name: {provider.identity.app_name} +
  • """ + # Add installations if available + if ( + hasattr(provider.identity, "installations") + and provider.identity.installations + ): + installations_display = ", ".join(provider.identity.installations) + account_info_items += f""" +
  • + Installations: {installations_display} +
  • """ + else: + account_info_items += """ +
  • + Installations: No installations found +
  • """ + + # Credentials items: Authentication method and App ID + credentials_items = f""" +
  • + GitHub authentication method: {provider.auth_method} +
  • +
  • + GitHub App ID: {provider.identity.app_id} +
  • """ + else: + # Fallback for other identity types + account_info_items = "" + credentials_items = f""" +
  • + GitHub authentication method: {provider.auth_method} +
  • """ + + # For PAT/OAuth, use default credentials structure + if hasattr(provider.identity, "account_name"): + credentials_items = f""" +
  • + GitHub authentication method: {provider.auth_method} +
  • """ return f"""
    @@ -569,11 +624,8 @@ class HTML(Output):
    GitHub Assessment Summary
    -
      -
    • - GitHub account: {account_display} -
    • +
        + {account_info_items}
    @@ -582,11 +634,8 @@ class HTML(Output):
    GitHub Credentials
    -