"""
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
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
Status
Severity
Service Name
Region
Check ID
Check Title
Resource ID
Resource Tags
Status Extended
Risk
Recommendation
Compliance
"""
return aws_html_header
html_footer = """
"""
def _setup_aws_xss(provider, payload):
provider.identity.account = payload
provider.identity.profile = payload
provider.identity.audited_regions = [payload]
provider.identity.user_id = payload
provider.identity.identity_arn = payload
def _setup_azure_xss(provider, payload):
provider.identity.tenant_ids = [payload]
provider.identity.tenant_domain = payload
provider.identity.subscriptions = {payload: payload}
provider.identity.identity_type = payload
provider.identity.identity_id = payload
def _setup_gcp_xss(provider, payload):
provider.project_ids = [payload]
provider.session._service_account_email = payload
def _setup_kubernetes_xss(provider, payload):
provider.identity.cluster = payload
provider.identity.context = payload
def _setup_github_xss(provider, payload):
provider.identity = MagicMock(spec=["account_name", "account_email"])
provider.identity.account_name = payload
provider.identity.account_email = payload
provider.auth_method = payload
def _setup_m365_xss(provider, payload):
provider.identity.tenant_domain = payload
provider.identity.identity_type = payload
provider.identity.identity_id = payload
provider.identity.user = payload
def _setup_nhn_xss(provider, payload):
provider.identity.tenant_domain = payload
provider.identity.identity_type = payload
provider.identity.identity_id = payload
def _setup_mongodbatlas_xss(provider, payload):
provider.identity.organization_name = payload
def _setup_iac_xss(provider, payload):
provider.scan_repository_url = payload
provider.scan_path = None
provider.auth_method = payload
def _setup_image_xss(provider, payload):
provider.registry = payload
provider.images = [payload]
provider.auth_method = payload
def _setup_llm_xss(provider, payload):
provider.model = payload
provider.plugins = [payload]
provider.max_concurrency = payload
provider.config_path = payload
def _setup_oraclecloud_xss(provider, payload):
provider.session.profile = payload
provider.identity.tenancy_name = payload
provider.identity.tenancy_id = payload
def _setup_stackit_xss(provider, payload):
provider.identity.project_id = payload
provider.identity.project_name = payload
provider.identity.audited_regions = {payload}
def _setup_cloudflare_xss(provider, payload):
account = MagicMock()
account.id = payload
provider.accounts = [account]
provider.session.api_token = "token"
provider.session.api_key = None
provider.session.api_email = None
provider.identity.email = payload
def _setup_alibabacloud_xss(provider, payload):
provider.identity.account_id = payload
provider.identity.account_name = payload
provider.identity.audited_regions = payload
provider.identity.identity_arn = payload
provider.identity.user_name = payload
def _setup_openstack_xss(provider, payload):
provider.identity.project_id = payload
provider.identity.project_name = payload
provider.identity.region_name = payload
provider.identity.username = payload
provider.identity.user_id = payload
def _setup_googleworkspace_xss(provider, payload):
provider.identity.domain = payload
provider.identity.customer_id = payload
provider.identity.delegated_user = payload
def _setup_e2enetworks_xss(provider, payload):
provider.identity.project_id = payload
provider.identity.locations = [payload]
def _setup_vercel_xss(provider, payload):
team = MagicMock()
team.name = payload
team.id = payload
provider.identity.team = team
provider.identity.email = payload
provider.identity.username = payload
def _setup_okta_xss(provider, payload):
provider.identity.org_domain = payload
provider.auth_method = payload
provider.identity.client_id = payload
def _setup_scaleway_xss(provider, payload):
provider.identity.organization_id = payload
provider.session.access_key = payload
provider.identity.bearer_type = payload
provider.identity.bearer_email = payload
provider.identity.bearer_id = payload
provider.session.default_region = payload
def _setup_linode_xss(provider, payload):
provider.identity.username = payload
provider.identity.email = payload
provider.identity.account_id = payload
def _setup_huaweicloud_xss(provider, payload):
provider.identity.account_id = payload
provider.identity.account_name = payload
provider.identity.profile = payload
provider.identity.regions = {payload}
provider.identity.domain_id = payload
provider.identity.user_id = payload
provider.identity.user_name = payload
provider.identity.identity_type = payload
PROVIDER_XSS_SETUPS = [
("aws", _setup_aws_xss),
("azure", _setup_azure_xss),
("gcp", _setup_gcp_xss),
("kubernetes", _setup_kubernetes_xss),
("github", _setup_github_xss),
("m365", _setup_m365_xss),
("nhn", _setup_nhn_xss),
("mongodbatlas", _setup_mongodbatlas_xss),
("iac", _setup_iac_xss),
("image", _setup_image_xss),
("llm", _setup_llm_xss),
("oraclecloud", _setup_oraclecloud_xss),
("stackit", _setup_stackit_xss),
("cloudflare", _setup_cloudflare_xss),
("alibabacloud", _setup_alibabacloud_xss),
("openstack", _setup_openstack_xss),
("googleworkspace", _setup_googleworkspace_xss),
("e2enetworks", _setup_e2enetworks_xss),
("vercel", _setup_vercel_xss),
("okta", _setup_okta_xss),
("scaleway", _setup_scaleway_xss),
("linode", _setup_linode_xss),
("huaweicloud", _setup_huaweicloud_xss),
]
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_escapes_provider_originated_fields(self):
xss_payload = ''
findings = [
generate_finding_output(
region="REGION&<>'\"",
resource_uid="resource&<>'\"_uid",
resource_tags={f"key&<>'\"{xss_payload}": f"value&<>'\"{xss_payload}"},
status_extended=f"status&<>'\"_{xss_payload}",
remediation_recommendation_url="https://hub.prowler.com/check/check-id",
)
]
output_data = HTML(findings).data[0]
assert xss_payload not in output_data
assert "region&<>'"" in output_data
assert "resource&<>'"_uid" in output_data
assert "status&<>'"_<img" in output_data
assert "•key&<>'"<img" in output_data
assert "=value&<>'"<img" in output_data
def test_transform_escapes_metadata_fields(self):
finding = generate_finding_output()
finding.metadata.Severity = MagicMock(value='')
finding.metadata.ServiceName = ''
finding.metadata.CheckID = '_suffix'
finding.metadata.CheckTitle = ''
finding.metadata.Risk = '**Risk** '
finding.metadata.Remediation.Recommendation.Text = (
'**Recommendation** '
)
finding.metadata.Remediation.Recommendation.Url = (
'https://example.com">'
)
output_data = HTML([finding]).data[0]
raw_payloads = (
'',
'',
'_suffix',
'',
'',
'',
'href="https://example.com">"',
)
for payload in raw_payloads:
assert payload not in output_data
assert "<img data-field="severity" src=x>" in output_data
assert "<img data-field="service" src=x>" in output_data
assert (
"<img data-field="check_id" src=x>_suffix"
in output_data
)
assert "<img data-field="check_title" src=x>" in output_data
assert (
"Risk <img data-field="risk" src=x>"
in output_data
)
assert (
"Recommendation <img "
"data-field="recommendation" src=x>" in output_data
)
assert (
'href="https://example.com"><img '
'data-field="url" src=x>"' in output_data
)
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_e2enetworks_get_assessment_summary(self):
"""Test E2E Networks HTML assessment summary shows project and locations."""
findings = [generate_finding_output()]
output = HTML(findings)
provider = MagicMock()
provider.type = "e2enetworks"
provider.identity.project_id = 12345
provider.identity.locations = ["Delhi", "Chennai"]
summary = output.get_assessment_summary(provider)
assert "E2E Networks Assessment Summary" in summary
assert "Project ID: 12345" in summary
assert "Locations: Delhi, Chennai" in summary
assert "API Key + Bearer Token" in summary
def test_e2enetworks_get_assessment_summary_escapes_locations(self):
"""Test E2E Networks HTML assessment summary escapes user-controlled locations."""
findings = [generate_finding_output()]
output = HTML(findings)
provider = MagicMock()
provider.type = "e2enetworks"
provider.identity.project_id = 12345
provider.identity.locations = ['Delhi">']
summary = output.get_assessment_summary(provider)
assert "" not in summary
assert "Delhi"><script>alert(1)</script>" in summary
@pytest.mark.parametrize(
"provider_type,setup_fn",
PROVIDER_XSS_SETUPS,
ids=[t for t, _ in PROVIDER_XSS_SETUPS],
)
def test_get_assessment_summary_escapes_provider_identity(
self, provider_type, setup_fn
):
"""Every provider header must HTML-escape tenant-controlled identity fields."""
payload = ""
findings = [generate_finding_output()]
output = HTML(findings)
provider = MagicMock()
provider.type = provider_type
setup_fn(provider, payload)
summary = output.get_assessment_summary(provider)
assert payload not in summary
assert "<script>alert(1)</script>" in summary
def test_provider_xss_setups_covers_every_assessment_summary_method(self):
"""Adding a new get__assessment_summary without a PROVIDER_XSS_SETUPS
entry must fail this test, so the escape guarantee cannot silently regress."""
pattern = re.compile(r"^get_(.+)_assessment_summary$")
discovered = set()
for name in dir(HTML):
match = pattern.match(name)
if match:
discovered.add(match.group(1))
covered = {ptype for ptype, _ in PROVIDER_XSS_SETUPS}
missing = discovered - covered
extra = covered - discovered
assert (
not missing
), f"providers without XSS coverage in PROVIDER_XSS_SETUPS: {sorted(missing)}"
assert (
not extra
), f"PROVIDER_XSS_SETUPS entries with no matching HTML method: {sorted(extra)}"
def test_github_app_get_assessment_summary_escapes_app_identity(self):
"""The GitHub App branch (elif hasattr app_id) must escape app_name/app_id/installations,
which the PAT setup does not exercise."""
payload = ""
findings = [generate_finding_output()]
output = HTML(findings)
provider = MagicMock()
provider.type = "github"
provider.identity = MagicMock(spec=["app_id", "app_name", "installations"])
provider.identity.app_id = payload
provider.identity.app_name = payload
provider.identity.installations = [payload]
provider.auth_method = payload
summary = output.get_assessment_summary(provider)
assert payload not in summary
assert "<script>alert(1)</script>" in summary
def test_iac_get_assessment_summary_escapes_scan_path(self):
"""The IAC scan_path branch (else of `if scan_repository_url`) must escape it."""
payload = ""
findings = [generate_finding_output()]
output = HTML(findings)
provider = MagicMock()
provider.type = "iac"
provider.scan_repository_url = None
provider.scan_path = payload
provider.auth_method = payload
summary = output.get_assessment_summary(provider)
assert payload not in summary
assert "<script>alert(1)</script>" in summary
def test_image_get_assessment_summary_escapes_images_list(self):
"""The Image `else` branch (no registry, images list) must escape each image."""
payload = ""
findings = [generate_finding_output()]
output = HTML(findings)
provider = MagicMock()
provider.type = "image"
provider.registry = None
provider.images = [payload]
provider.auth_method = payload
summary = output.get_assessment_summary(provider)
assert payload not in summary
assert "<script>alert(1)</script>" 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
def test_process_markdown_strips_javascript_links(self):
"""Markdown links with javascript: scheme must not produce clickable hrefs."""
result = HTML.process_markdown(
"Click [here](javascript:alert("xss")) to continue"
)
assert 'href="javascript:' not in result
assert "here" in result
def test_process_markdown_keeps_https_links(self):
"""Markdown links with https: scheme must be preserved."""
result = HTML.process_markdown("[docs](https://docs.prowler.com)")
assert 'href="https://docs.prowler.com"' in result
assert "