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):
-
- GitHub account: {provider.identity.account_name}
+ GitHub account: {account_display}
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 account: account-name
+
+
+
+
+
+
+
+
+ -
+ GitHub authentication method: Personal Access Token
+
+
+
+
"""
+
+github_app_html_assessment_summary = """
+
+
+
+
+ -
+ GitHub account: app-app-id
+
+
+
+
+
+
+
+
+ -
+ GitHub authentication method: GitHub App Token
+
+
+
+
"""
+
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