diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index acb6287be6..33b0d7982d 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. - Use the correct @staticmethod decorator for `set_identity` and `set_session_config` methods in AwsProvider [(#8056)](https://github.com/prowler-cloud/prowler/pull/8056) - Use the correct default value for `role_session_name` and `session_duration` in AwsSetUpSession [(#8056)](https://github.com/prowler-cloud/prowler/pull/8056) - Use the correct default value for `role_session_name` and `session_duration` in S3 [(#8417)](https://github.com/prowler-cloud/prowler/pull/8417) +- GitHub App authentication fails to generate output files and HTML header sections [(#8423)](https://github.com/prowler-cloud/prowler/pull/8423) --- diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 19ad271218..a3ba6271dd 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -249,8 +249,17 @@ class Finding(BaseModel): output_data["auth_method"] = provider.auth_method output_data["resource_name"] = check_output.resource_name output_data["resource_uid"] = check_output.resource_id - output_data["account_name"] = provider.identity.account_name - output_data["account_uid"] = provider.identity.account_id + + if hasattr(provider.identity, "account_name"): + # 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"): + # GithubAppIdentityInfo (GitHub App) + # TODO: Get Github App name + output_data["account_name"] = f"app-{provider.identity.app_id}" + output_data["account_uid"] = provider.identity.app_id + output_data["region"] = check_output.owner elif provider.type == "m365": diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 6d43e9aab8..7b4fb87601 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -556,6 +556,13 @@ class HTML(Output): str: the HTML assessment summary """ try: + if hasattr(provider.identity, "account_name"): + # GithubIdentityInfo (Personal Access Token, OAuth) + account_display = provider.identity.account_name + elif hasattr(provider.identity, "app_id"): + # GithubAppIdentityInfo (GitHub App) + account_display = f"app-{provider.identity.app_id}" + return f"""
@@ -565,7 +572,7 @@ class HTML(Output):
diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index f086c493ee..5b22fa675d 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -14,7 +14,14 @@ from prowler.lib.check.models import ( ) from prowler.lib.outputs.common import Status from prowler.lib.outputs.finding import Finding +from prowler.providers.github.models import GithubAppIdentityInfo from tests.lib.outputs.fixtures.fixtures import generate_finding_output +from tests.providers.github.github_fixtures import ( + ACCOUNT_ID, + ACCOUNT_NAME, + ACCOUNT_URL, + APP_ID, +) def mock_check_metadata(provider): @@ -506,6 +513,140 @@ class TestFinding: assert finding_output.metadata.Notes == "mock_notes" assert finding_output.metadata.Compliance == [] + def test_generate_output_github_personal_access_token(self): + """Test GitHub output generation with Personal Access Token authentication.""" + # Mock provider using Personal Access Token + provider = MagicMock() + provider.type = "github" + # Use the actual GithubIdentityInfo for Personal Access Token + from prowler.providers.github.models import GithubIdentityInfo + + provider.identity = GithubIdentityInfo( + account_name=ACCOUNT_NAME, account_id=ACCOUNT_ID, account_url=ACCOUNT_URL + ) + provider.auth_method = "Personal Access Token" + + # Mock check result + check_output = MagicMock() + check_output.resource_id = "test_repository" + check_output.resource_name = "test_repository" + check_output.resource_details = "GitHub repository test_repository" + check_output.resource_tags = {"topic": "security"} + check_output.owner = "test-owner" # GitHub uses owner for region + check_output.status = Status.PASS + check_output.status_extended = "Repository has security features enabled" + check_output.muted = False + check_output.check_metadata = mock_check_metadata(provider="github") + check_output.resource = {"url": "https://github.com/owner/test_repository"} + check_output.compliance = { + "CIS-2.0": ["1.12"], + "ENS-RD2022": ["op.acc.2.gcp.rbak.1"], + } + + # Mock output options + output_options = MagicMock() + output_options.unix_timestamp = False + + # Generate the finding + finding_output = Finding.generate_output(provider, check_output, output_options) + + # Assert basic finding properties + assert isinstance(finding_output, Finding) + assert finding_output.provider == "github" + assert finding_output.auth_method == "Personal Access Token" + assert finding_output.resource_name == "test_repository" + assert finding_output.resource_uid == "test_repository" + assert finding_output.region == "test-owner" + assert finding_output.status == Status.PASS + assert ( + finding_output.status_extended == "Repository has security features enabled" + ) + assert finding_output.muted is False + assert finding_output.resource_tags == {"topic": "security"} + + # Assert account information for Personal Access Token + assert finding_output.account_name == ACCOUNT_NAME + assert finding_output.account_uid == ACCOUNT_ID + assert finding_output.account_email is None + assert finding_output.account_organization_uid is None + assert finding_output.account_organization_name is None + assert finding_output.account_tags == {} + + # Metadata checks + assert finding_output.metadata.Provider == "github" + assert finding_output.metadata.CheckID == "service_check_id" + assert finding_output.metadata.ServiceName == "service" + assert finding_output.metadata.Severity == Severity.high + assert finding_output.metadata.ResourceType == "mock_resource_type" + + def test_generate_output_github_app_authentication(self): + """Test GitHub output generation with GitHub App authentication.""" + # Mock provider using GitHub App authentication - this is the key test case for the bug fix + provider = MagicMock() + provider.type = "github" + # GitHub App identity only has app_id, not account_name/account_id + provider.identity = GithubAppIdentityInfo(app_id=APP_ID) + provider.auth_method = "GitHub App Token" + + # Mock check result + check_output = MagicMock() + check_output.resource_id = "test_repository" + check_output.resource_name = "test_repository" + check_output.resource_details = "GitHub repository test_repository" + check_output.resource_tags = {"language": "python"} + check_output.owner = "test-owner" # GitHub provider uses owner for region + check_output.status = Status.FAIL + check_output.status_extended = ( + "Repository lacks required security configuration" + ) + check_output.muted = False + check_output.check_metadata = mock_check_metadata(provider="github") + check_output.resource = {"url": "https://github.com/org/test_repository"} + check_output.compliance = { + "CIS-2.0": ["1.12"], + "MITRE-ATTACK": ["T1098"], + } + + # Mock output options + output_options = MagicMock() + output_options.unix_timestamp = True + + # Generate the finding - this was failing before the fix + finding_output = Finding.generate_output(provider, check_output, output_options) + + # Assert basic finding properties + assert isinstance(finding_output, Finding) + assert finding_output.provider == "github" + assert finding_output.auth_method == "GitHub App Token" + assert finding_output.resource_name == "test_repository" + assert finding_output.resource_uid == "test_repository" + assert finding_output.region == "test-owner" + assert finding_output.status == Status.FAIL + assert ( + finding_output.status_extended + == "Repository lacks required security configuration" + ) + assert finding_output.muted is False + assert finding_output.resource_tags == {"language": "python"} + assert isinstance(finding_output.timestamp, int) + + # Assert account information for GitHub App - this is the core of the bug fix + # Before the fix, this would fail because GithubAppIdentityInfo doesn't have account_name + # After the fix, it should use app_id with "app-" prefix + assert finding_output.account_name == f"app-{APP_ID}" + assert finding_output.account_uid == APP_ID + assert finding_output.account_email is None + assert finding_output.account_organization_uid is None + assert finding_output.account_organization_name is None + assert finding_output.account_tags == {} + + # Metadata checks + assert finding_output.metadata.Provider == "github" + assert finding_output.metadata.CheckID == "service_check_id" + assert finding_output.metadata.ServiceName == "service" + assert finding_output.metadata.Severity == Severity.high + assert finding_output.metadata.ResourceType == "mock_resource_type" + def test_generate_output_iac_remote(self): # Mock provider provider = MagicMock() diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py index bce4a82154..e3a834e452 100644 --- a/tests/lib/outputs/html/html_test.py +++ b/tests/lib/outputs/html/html_test.py @@ -5,10 +5,12 @@ from mock import patch from prowler.config.config import prowler_version, timestamp from prowler.lib.outputs.html.html import HTML +from prowler.providers.github.models import GithubAppIdentityInfo from tests.lib.outputs.fixtures.fixtures import generate_finding_output from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider from tests.providers.azure.azure_fixtures import set_mocked_azure_provider from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider +from tests.providers.github.github_fixtures import APP_ID, set_mocked_github_provider from tests.providers.kubernetes.kubernetes_fixtures import ( set_mocked_kubernetes_provider, ) @@ -223,6 +225,62 @@ kubernetes_html_assessment_summary = """
""" +github_personal_access_token_html_assessment_summary = """ +
+
+
+ GitHub Assessment Summary +
+ +
+
+
+
+
+ GitHub Credentials +
+ +
+
""" + +github_app_html_assessment_summary = """ +
+
+
+ GitHub Assessment Summary +
+ +
+
+
+
+
+ GitHub Credentials +
+ +
+
""" + m365_html_assessment_summary = """
@@ -597,3 +655,27 @@ class TestHTML: expected_summary = m365_html_assessment_summary assert summary == expected_summary + + def test_github_personal_access_token_get_assessment_summary(self): + """Test GitHub HTML assessment summary generation with Personal Access Token authentication.""" + findings = [generate_finding_output()] + output = HTML(findings) + provider = set_mocked_github_provider(auth_method="Personal Access Token") + + summary = output.get_assessment_summary(provider) + + assert summary == github_personal_access_token_html_assessment_summary + + def test_github_app_get_assessment_summary(self): + """Test GitHub HTML assessment summary generation with GitHub App authentication.""" + findings = [generate_finding_output()] + output = HTML(findings) + + provider = set_mocked_github_provider( + auth_method="GitHub App Token", + identity=GithubAppIdentityInfo(app_id=APP_ID), + ) + + summary = output.get_assessment_summary(provider) + + assert summary == github_app_html_assessment_summary