import sys from io import StringIO from mock import MagicMock, patch from prowler.config.config import prowler_version, timestamp from prowler.lib.cli.redact import redact_argv from prowler.lib.logger import logger 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.googleworkspace.googleworkspace_fixtures import ( set_mocked_googleworkspace_provider, ) from tests.providers.kubernetes.kubernetes_fixtures import ( set_mocked_kubernetes_provider, ) from tests.providers.m365.m365_fixtures import set_mocked_m365_provider from tests.providers.mongodbatlas.mongodbatlas_fixtures import ( set_mocked_mongodbatlas_provider, ) html_stats = { "total_pass": 25, "total_muted_pass": 20, "total_fail": 5, "total_muted_fail": 5, "resources_count": 1, "findings_count": 30, } pass_html_finding = """ PASS high service eu-west-1 service_test_check_id service_test_check_id

test-risk

•test-compliance: test-compliance

""" fail_html_finding = """ FAIL high service eu-west-1 service_test_check_id service_test_check_id test-resource-uid •key1=value1 •key2=value2 test-status-extended

test-risk

test-remediation-recommendation-text

•test-compliance: test-compliance

""" muted_html_finding = """ MUTED (PASS) high service eu-west-1 service_test_check_id service_test_check_id

test-risk

•test-compliance: test-compliance

""" manual_html_finding = """ MANUAL high service eu-west-1 service_test_check_id service_test_check_id

test-risk

•test-compliance: test-compliance

""" aws_html_assessment_summary = """
AWS Assessment Summary
AWS Credentials
""" azure_html_assessment_summary = """
Azure Assessment Summary
Azure Credentials
""" gcp_html_assessment_summary = """
GCP Assessment Summary
GCP Credentials
""" kubernetes_html_assessment_summary = """
Kubernetes Assessment Summary
Kubernetes Credentials
""" 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 = """
M365 Assessment Summary
M365 Credentials
""" mongodbatlas_html_assessment_summary = """
MongoDB Atlas Assessment Summary
MongoDB Atlas Credentials
""" image_registry_html_assessment_summary = """
Image Assessment Summary
Image Credentials
""" image_list_html_assessment_summary = """
Image Assessment Summary
Image Credentials
""" def get_aws_html_header(args: list) -> str: """ Generate the HTML header for AWS Args: args (list): List of arguments passed to the script Returns: str: HTML header for AWS """ aws_html_header = f""" Prowler - The Handy Cloud Security Tool
prowler-logo
Report Information
  • Version: {prowler_version}
  • Parameters used: {redact_argv(args)}
  • Date: {timestamp.isoformat()}
{aws_html_assessment_summary}
Assessment Overview
  • Total Findings: 30
  • Passed: 25
  • Passed (Muted): 20
  • Failed: 5
  • Failed (Muted): 5
  • Total Resources: 1
""" return aws_html_header html_footer = """
Status Severity Service Name Region Check ID Check Title Resource ID Resource Tags Status Extended Risk Recommendation Compliance
""" class TestHTML: def test_transform_fail_finding(self): findings = [ generate_finding_output( status="FAIL", resource_tags={"key1": "value1", "key2": "value2"}, severity="high", service_name="service", region=AWS_REGION_EU_WEST_1, check_id="service_test_check_id", check_title="service_test_check_id", resource_uid="test-resource-uid", status_extended="test-status-extended", risk="test-risk", remediation_recommendation_text="test-remediation-recommendation-text", remediation_recommendation_url="https://hub.prowler.com/check/check-id", compliance={"test-compliance": "test-compliance"}, ) ] html = HTML(findings) output_data = html.data[0] assert isinstance(output_data, str) assert output_data == fail_html_finding def test_transform_pass_finding(self): findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] html = HTML(findings) output_data = html.data[0] assert isinstance(output_data, str) assert output_data == pass_html_finding def test_transform_muted_finding(self): findings = [ generate_finding_output( muted=True, remediation_recommendation_url="https://hub.prowler.com/check/check-id", ) ] html = HTML(findings) output_data = html.data[0] assert isinstance(output_data, str) assert output_data == muted_html_finding def test_transform_manual_finding(self): findings = [ generate_finding_output( status="MANUAL", remediation_recommendation_url="https://hub.prowler.com/check/check-id", ) ] html = HTML(findings) output_data = html.data[0] assert isinstance(output_data, str) assert output_data == manual_html_finding def test_batch_write_data_to_file(self): mock_file = StringIO() findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) output._file_descriptor = mock_file provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1]) with patch.object(mock_file, "close", return_value=None): output.batch_write_data_to_file(provider, html_stats) mock_file.seek(0) content = mock_file.read() args = sys.argv[1:] assert content == get_aws_html_header(args) + pass_html_finding + html_footer def test_batch_write_data_to_file_without_findings(self): assert not HTML([])._file_descriptor def test_write_header(self): mock_file = StringIO() findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) output._file_descriptor = mock_file provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1]) output.write_header(mock_file, provider, html_stats) mock_file.seek(0) content = mock_file.read() args = sys.argv[1:] assert content == get_aws_html_header(args) def test_write_footer(self): mock_file = StringIO() findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) output._file_descriptor = mock_file output.write_footer(mock_file) mock_file.seek(0) content = mock_file.read() assert content == html_footer def test_aws_get_assessment_summary(self): findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1]) summary = output.get_assessment_summary(provider) assert summary == aws_html_assessment_summary def test_azure_get_assessment_summary(self): findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) provider = set_mocked_azure_provider() summary = output.get_assessment_summary(provider) assert summary == summary def test_gcp_get_assessment_summary(self): findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) provider = set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]) summary = output.get_assessment_summary(provider) assert summary == gcp_html_assessment_summary def test_kubernetes_get_assessment_summary(self): findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) provider = set_mocked_kubernetes_provider() summary = output.get_assessment_summary(provider) assert summary == kubernetes_html_assessment_summary def test_m365_get_assessment_summary(self): findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) provider = set_mocked_m365_provider() summary = output.get_assessment_summary(provider) 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( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) provider = set_mocked_github_provider(auth_method="Personal Access Token") summary = output.get_assessment_summary(provider) # Check for expected content in the summary assert "GitHub Assessment Summary" in summary assert "GitHub Credentials" in summary assert "GitHub account: account-name" in summary assert "GitHub authentication method: Personal Access Token" in summary # Note: account_email is None in the default fixture, so it shouldn't appear def test_github_app_get_assessment_summary(self): """Test GitHub HTML assessment summary generation with GitHub App authentication.""" findings = [ generate_finding_output( remediation_recommendation_url="https://hub.prowler.com/check/check-id" ) ] output = HTML(findings) provider = set_mocked_github_provider( auth_method="GitHub App Token", identity=GithubAppIdentityInfo( app_id=APP_ID, app_name="test-app", installations=["test-org"] ), ) summary = output.get_assessment_summary(provider) logger.error(summary) # Check for expected content in the summary assert "GitHub Assessment Summary" in summary assert "GitHub Credentials" in summary assert "GitHub App Name: test-app" in summary assert "Installations: test-org" in summary assert "GitHub authentication method: GitHub App Token" in summary assert f"GitHub App ID: {APP_ID}" in summary def test_mongodbatlas_get_assessment_summary(self): """Test MongoDB Atlas HTML assessment summary generation.""" findings = [generate_finding_output()] output = HTML(findings) provider = set_mocked_mongodbatlas_provider() summary = output.get_assessment_summary(provider) assert summary == mongodbatlas_html_assessment_summary def test_googleworkspace_get_assessment_summary(self): """Test Google Workspace HTML assessment summary generation.""" findings = [generate_finding_output()] output = HTML(findings) provider = set_mocked_googleworkspace_provider() summary = output.get_assessment_summary(provider) assert "Google Workspace Assessment Summary" in summary assert "Google Workspace Credentials" in summary assert "Domain: test-company.com" in summary assert "Customer ID: C1234567" in summary assert "Delegated User: prowler-reader@test-company.com" in summary assert ( "Authentication Method: Service Account with Domain-Wide Delegation" in summary ) def test_image_get_assessment_summary_with_registry(self): """Test Image HTML assessment summary with registry URL.""" findings = [generate_finding_output()] output = HTML(findings) provider = MagicMock() provider.type = "image" provider.registry = "myregistry.io" provider.images = ["nginx:latest", "alpine:3.18"] provider.auth_method = "Docker login" summary = output.get_assessment_summary(provider) assert summary == image_registry_html_assessment_summary def test_image_get_assessment_summary_with_images(self): """Test Image HTML assessment summary with image list.""" findings = [generate_finding_output()] output = HTML(findings) provider = MagicMock() provider.type = "image" provider.registry = None provider.images = ["nginx:latest", "alpine:3.18"] provider.auth_method = "No auth" summary = output.get_assessment_summary(provider) assert summary == image_list_html_assessment_summary def test_stackit_get_assessment_summary(self): """Test StackIT HTML assessment summary shows the project ID.""" findings = [generate_finding_output()] output = HTML(findings) provider = MagicMock() provider.type = "stackit" provider.identity.project_id = "f033ea6d-8697-40eb-a60e-acfa9128480d" provider.identity.project_name = "ProwlerDev" provider.identity.audited_regions = {"eu01", "eu02"} summary = output.get_assessment_summary(provider) assert "StackIT Assessment Summary" in summary assert "StackIT Credentials" in summary assert "Project ID: f033ea6d-8697-40eb-a60e-acfa9128480d" in summary assert "Project Name: ProwlerDev" in summary assert "Regions: eu01, eu02" in summary assert "Authentication Type: Service Account Key" in summary def test_stackit_get_assessment_summary_without_project_name(self): """Project ID is always shown; the Project Name line is omitted when the service account cannot read it from Resource Manager.""" findings = [generate_finding_output()] output = HTML(findings) provider = MagicMock() provider.type = "stackit" provider.identity.project_id = "f033ea6d-8697-40eb-a60e-acfa9128480d" provider.identity.project_name = "" provider.identity.audited_regions = {"eu01"} summary = output.get_assessment_summary(provider) assert "Project ID: f033ea6d-8697-40eb-a60e-acfa9128480d" in summary assert "Project Name:" not in summary def test_process_markdown_bold_text(self): """Test that **text** is converted to text""" test_text = "This is **bold text** and this is **also bold**" result = HTML.process_markdown(test_text) expected = ( "This is bold text and this is also bold" ) assert result == expected def test_process_markdown_italic_text(self): """Test that *text* is converted to text""" test_text = "This is *italic text* and this is *also italic*" result = HTML.process_markdown(test_text) expected = "This is italic text and this is also italic" assert result == expected def test_process_markdown_code_text(self): """Test that `text` is converted to text""" test_text = "Use the `ls` command to list files and `cd` to change directories" result = HTML.process_markdown(test_text) expected = "Use the ls command to list files and cd to change directories" assert result == expected def test_process_markdown_line_breaks(self): """Test that line breaks are converted to
tags""" test_text = "Line 1\nLine 2\nLine 3" result = HTML.process_markdown(test_text) expected = "Line 1
\nLine 2
\nLine 3" assert result == expected def test_process_markdown_mixed_formatting(self): """Test mixed markdown formatting""" test_text = "**Bold text** with *italic* and `code` elements.\n\nNew paragraph with **more bold**." result = HTML.process_markdown(test_text) expected = "Bold text with italic and code elements.
\n
\nNew paragraph with more bold." assert result == expected def test_process_markdown_empty_string(self): """Test that empty string returns empty string""" result = HTML.process_markdown("") assert result == "" def test_process_markdown_none_input(self): """Test that None input returns None""" result = HTML.process_markdown(None) assert result is None def test_process_markdown_no_markdown(self): """Test that plain text without markdown is returned unchanged""" test_text = "This is plain text without any markdown formatting" result = HTML.process_markdown(test_text) assert result == test_text def test_transform_with_markdown_risk(self): """Test that Risk field with markdown is properly converted""" findings = [ generate_finding_output( risk="Outdated contacts delay **security notifications** and slow **incident response**", remediation_recommendation_url="https://hub.prowler.com/check/check-id", ) ] html = HTML(findings) output_data = html.data[0] # Check that markdown is converted to HTML assert "security notifications" in output_data assert "incident response" in output_data def test_transform_with_markdown_recommendation(self): """Test that Recommendation field with markdown is properly converted""" findings = [ generate_finding_output( risk="test-risk", remediation_recommendation_text="Adopt:\n- **Primary** and **alternate contacts**\n- Use `monitored aliases`", remediation_recommendation_url="https://hub.prowler.com/check/check-id", ) ] html = HTML(findings) output_data = html.data[0] # Check that markdown is converted to HTML assert "Primary" in output_data assert "alternate contacts" in output_data assert "monitored aliases" in output_data assert "
" in output_data # Line breaks converted