mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-05 02:44:49 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51bf412ff7 | ||
|
|
21e11b46e3 | ||
|
|
ed6de7c8a2 |
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.15.1-slim-trixie@sha256:533b0b2f9b5d46fd097091cf826f27902f1d6c3bca714b6dc74551356aa97a51 AS build
|
||||
FROM python:3.12.16-slim-trixie@sha256:8523d6abeb48ceae0b7240d30c5808fc26cb79c1a635bacdf19cf3baecfbec07 AS build
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/prowler"
|
||||
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.15.1-slim-trixie@sha256:533b0b2f9b5d46fd097091cf826f27902f1d6c3bca714b6dc74551356aa97a51 AS build
|
||||
FROM python:3.12.16-slim-trixie@sha256:8523d6abeb48ceae0b7240d30c5808fc26cb79c1a635bacdf19cf3baecfbec07 AS build
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/api"
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
# =============================================================================
|
||||
# Final stage - Minimal runtime environment
|
||||
# =============================================================================
|
||||
FROM python:3.15.1-alpine3.23@sha256:cb295b554dbbea09b2c0e6e6d9ce55f051347c902fbb611e618c071addfb30a3
|
||||
FROM python:3.13.16-alpine3.23@sha256:07b20c774a247152e74f622034c1482495d299e070eec620ddff13e01264290b
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud"
|
||||
|
||||
|
||||
@@ -698,6 +698,8 @@ def prowler():
|
||||
html_output = HTML(
|
||||
findings=finding_outputs,
|
||||
file_path=f"{filename}{html_file_suffix}",
|
||||
# Only the OSS CLI shows the Prowler Cloud hand-off block
|
||||
show_cloud_cta=True,
|
||||
)
|
||||
generated_outputs["regular"].append(html_output)
|
||||
html_output.batch_write_data_to_file(
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Prowler Cloud hand-off block after the assessment summary in OSS CLI HTML reports, controlled by an opt-in `show_cloud_cta` option that Prowler Cloud and Private Cloud exports leave disabled
|
||||
@@ -20,6 +20,18 @@ from prowler.providers.common.provider import Provider
|
||||
|
||||
_SAFE_URL_SCHEMES = {"http", "https"}
|
||||
|
||||
# Prowler Cloud hand-off block. Only the OSS CLI opts in (see `show_cloud_cta`),
|
||||
# so Prowler Cloud and Private Cloud reports never contain it. These URLs are
|
||||
# static: no assessment data is ever appended to them.
|
||||
PROWLER_CLOUD_CTA_URL = (
|
||||
"https://cloud.prowler.com/sign-up"
|
||||
"?utm_source=prowler-html-report&utm_medium=oss&utm_campaign=report-handoff"
|
||||
)
|
||||
PROWLER_CLOUD_PRICING_URL = "https://prowler.com/pricing"
|
||||
PROWLER_CLOUD_IMPORT_DOCS_URL = (
|
||||
"https://docs.prowler.com/user-guide/tutorials/prowler-import-findings"
|
||||
)
|
||||
|
||||
|
||||
def _safe_url(url: str) -> str:
|
||||
"""Return url if its scheme is http/https, otherwise return empty string."""
|
||||
@@ -47,6 +59,57 @@ def _strip_unsafe_links(html_content: str) -> str:
|
||||
|
||||
|
||||
class HTML(Output):
|
||||
def __init__(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
file_path: str = None,
|
||||
file_extension: str = "",
|
||||
from_cli: bool = True,
|
||||
show_cloud_cta: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Initializes the HTML output.
|
||||
|
||||
Args:
|
||||
findings (list[Finding]): findings to transform
|
||||
file_path (str): path of the output file
|
||||
file_extension (str): extension of the output file
|
||||
from_cli (bool): whether the output is written in one go (CLI) or in batches (API)
|
||||
show_cloud_cta (bool): whether to render the Prowler Cloud hand-off block after the
|
||||
assessment summary. Disabled by default; only the OSS CLI enables it.
|
||||
"""
|
||||
self._show_cloud_cta = show_cloud_cta
|
||||
super().__init__(
|
||||
findings,
|
||||
file_path=file_path,
|
||||
file_extension=file_extension,
|
||||
from_cli=from_cli,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_cloud_cta() -> str:
|
||||
"""
|
||||
Returns the Prowler Cloud hand-off block shown after the assessment summary in OSS CLI
|
||||
reports. It is static markup: no scripts, no requests, and no assessment data in URLs.
|
||||
|
||||
Returns:
|
||||
str: HTML markup of the block
|
||||
"""
|
||||
return f"""
|
||||
<div class="row mt-3 prowler-cloud-cta" role="complementary" aria-labelledby="prowler-cloud-cta-title">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" id="prowler-cloud-cta-title">Keep scanning locally. Bring your findings to Prowler Cloud.</h5>
|
||||
<p class="card-text">Import your accompanying OCSF report to explore managed security workflows. <a href="{PROWLER_CLOUD_IMPORT_DOCS_URL}" target="_blank" rel="noopener noreferrer">How to import findings</a></p>
|
||||
<a class="btn btn-primary" href="{PROWLER_CLOUD_CTA_URL}" target="_blank" rel="noopener noreferrer">Explore Prowler Cloud</a>
|
||||
<a class="btn btn-link" href="{PROWLER_CLOUD_PRICING_URL}" target="_blank" rel="noopener noreferrer">Compare editions</a>
|
||||
<p class="card-text text-muted mt-2 mb-0"><small>Nothing is uploaded automatically.</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
@staticmethod
|
||||
def process_markdown(text: str) -> str:
|
||||
"""
|
||||
@@ -142,7 +205,11 @@ class HTML(Output):
|
||||
):
|
||||
if self._file_descriptor.tell() == 0:
|
||||
HTML.write_header(
|
||||
self._file_descriptor, provider, stats, self._from_cli
|
||||
self._file_descriptor,
|
||||
provider,
|
||||
stats,
|
||||
self._from_cli,
|
||||
show_cloud_cta=getattr(self, "_show_cloud_cta", False),
|
||||
)
|
||||
for finding in self._data:
|
||||
self._file_descriptor.write(finding)
|
||||
@@ -160,6 +227,7 @@ class HTML(Output):
|
||||
provider: Provider,
|
||||
stats: dict,
|
||||
from_cli: bool = True,
|
||||
show_cloud_cta: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Writes the header of the HTML file.
|
||||
@@ -169,8 +237,17 @@ class HTML(Output):
|
||||
provider (Provider): the provider object
|
||||
stats (dict): the statistics of the findings
|
||||
from_cli (bool): whether the request is from the CLI or not
|
||||
show_cloud_cta (bool): whether to include the Prowler Cloud hand-off block. When
|
||||
False the block, its styles and its URLs are omitted entirely.
|
||||
"""
|
||||
try:
|
||||
cloud_cta_style = (
|
||||
"""
|
||||
@media print { .prowler-cloud-cta { display: none !important; } }"""
|
||||
if show_cloud_cta
|
||||
else ""
|
||||
)
|
||||
cloud_cta = HTML.get_cloud_cta() if show_cloud_cta else ""
|
||||
file_descriptor.write(f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -199,7 +276,7 @@ class HTML(Output):
|
||||
|
||||
.container-fluid {{font-size: 14px;}}
|
||||
|
||||
.float-left {{ float: left !important; max-width: 100%; }}
|
||||
.float-left {{ float: left !important; max-width: 100%; }}{cloud_cta_style}
|
||||
</style>
|
||||
<title>Prowler - The Handy Cloud Security Tool</title>
|
||||
</head>
|
||||
@@ -259,7 +336,7 @@ class HTML(Output):
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>{cloud_cta}
|
||||
</div>
|
||||
<div class="row-mt-3">
|
||||
<div class="col-md-12">
|
||||
|
||||
@@ -8,7 +8,12 @@ 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.lib.outputs.html.html import (
|
||||
HTML,
|
||||
PROWLER_CLOUD_CTA_URL,
|
||||
PROWLER_CLOUD_IMPORT_DOCS_URL,
|
||||
PROWLER_CLOUD_PRICING_URL,
|
||||
)
|
||||
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
|
||||
@@ -1521,3 +1526,133 @@ class TestHTML:
|
||||
]
|
||||
output_data = HTML(findings).data[0]
|
||||
assert 'href="https://hub.prowler.com/check/check-id"' in output_data
|
||||
|
||||
def test_write_header_omits_cloud_cta_by_default(self):
|
||||
mock_file = StringIO()
|
||||
provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1])
|
||||
|
||||
HTML.write_header(mock_file, provider, html_stats)
|
||||
|
||||
content = mock_file.getvalue()
|
||||
assert content == get_aws_html_header(sys.argv[1:])
|
||||
assert "prowler-cloud-cta" not in content
|
||||
assert PROWLER_CLOUD_CTA_URL not in content
|
||||
assert "utm_campaign=report-handoff" not in content
|
||||
assert PROWLER_CLOUD_PRICING_URL not in content
|
||||
assert PROWLER_CLOUD_IMPORT_DOCS_URL not in content
|
||||
|
||||
def test_write_header_omits_cloud_cta_when_from_cli_but_not_enabled(self):
|
||||
# `from_cli` alone must never turn the block on: it is a batching flag, not an
|
||||
# OSS marker, and it defaults to True for every library consumer.
|
||||
mock_file = StringIO()
|
||||
provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1])
|
||||
|
||||
HTML.write_header(mock_file, provider, html_stats, from_cli=True)
|
||||
|
||||
content = mock_file.getvalue()
|
||||
assert "prowler-cloud-cta" not in content
|
||||
assert "cloud.prowler.com" not in content
|
||||
|
||||
def test_write_header_includes_cloud_cta_when_enabled(self):
|
||||
mock_file = StringIO()
|
||||
provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1])
|
||||
|
||||
HTML.write_header(mock_file, provider, html_stats, show_cloud_cta=True)
|
||||
|
||||
content = mock_file.getvalue()
|
||||
assert content.count('class="row mt-3 prowler-cloud-cta"') == 1
|
||||
assert "Keep scanning locally. Bring your findings to Prowler Cloud." in content
|
||||
assert (
|
||||
"Import your accompanying OCSF report to explore managed security workflows."
|
||||
in content
|
||||
)
|
||||
assert f'href="{PROWLER_CLOUD_CTA_URL}"' in content
|
||||
assert ">Explore Prowler Cloud</a>" in content
|
||||
assert f'href="{PROWLER_CLOUD_PRICING_URL}"' in content
|
||||
assert ">Compare editions</a>" in content
|
||||
assert f'href="{PROWLER_CLOUD_IMPORT_DOCS_URL}"' in content
|
||||
assert ">How to import findings</a>" in content
|
||||
assert "Nothing is uploaded automatically." in content
|
||||
assert 'rel="noopener noreferrer"' in content
|
||||
assert (
|
||||
"@media print { .prowler-cloud-cta { display: none !important; } }"
|
||||
in content
|
||||
)
|
||||
# Placed after the assessment summary and before the findings table
|
||||
assert (
|
||||
content.index("AWS Assessment Summary")
|
||||
< content.index('class="row mt-3 prowler-cloud-cta"')
|
||||
< content.index('id="findingsTable"')
|
||||
)
|
||||
# No script, form or automatic request is introduced by the block
|
||||
cta_block = HTML.get_cloud_cta()
|
||||
assert "<script" not in cta_block
|
||||
assert "<form" not in cta_block
|
||||
assert "<img" not in cta_block
|
||||
assert "<iframe" not in cta_block
|
||||
|
||||
def test_cloud_cta_does_not_change_assessment_content(self):
|
||||
mock_file_oss = StringIO()
|
||||
mock_file_cloud = StringIO()
|
||||
provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1])
|
||||
|
||||
HTML.write_header(mock_file_oss, provider, html_stats, show_cloud_cta=True)
|
||||
HTML.write_header(mock_file_cloud, provider, html_stats, show_cloud_cta=False)
|
||||
|
||||
oss = mock_file_oss.getvalue()
|
||||
cloud = mock_file_cloud.getvalue()
|
||||
stripped = oss.replace(HTML.get_cloud_cta(), "").replace(
|
||||
"\n @media print { .prowler-cloud-cta { display: none !important; } }",
|
||||
"",
|
||||
)
|
||||
assert stripped == cloud
|
||||
|
||||
def test_batch_write_data_to_file_oss_cli_includes_cloud_cta(self):
|
||||
mock_file = StringIO()
|
||||
findings = [
|
||||
generate_finding_output(
|
||||
remediation_recommendation_url="https://hub.prowler.com/check/check-id"
|
||||
)
|
||||
]
|
||||
output = HTML(findings, show_cloud_cta=True)
|
||||
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)
|
||||
|
||||
content = mock_file.getvalue()
|
||||
assert PROWLER_CLOUD_CTA_URL in content
|
||||
assert content.count('class="row mt-3 prowler-cloud-cta"') == 1
|
||||
# Findings and footer are untouched
|
||||
assert pass_html_finding in content
|
||||
assert content.endswith(html_footer)
|
||||
|
||||
def test_batch_write_data_to_file_api_omits_cloud_cta(self):
|
||||
# Mirrors how Prowler Cloud / Private Cloud construct the output in the API
|
||||
mock_file = StringIO()
|
||||
findings = [
|
||||
generate_finding_output(
|
||||
remediation_recommendation_url="https://hub.prowler.com/check/check-id"
|
||||
)
|
||||
]
|
||||
output = HTML(
|
||||
findings,
|
||||
file_path=None,
|
||||
file_extension=".html",
|
||||
from_cli=False,
|
||||
)
|
||||
output._file_descriptor = mock_file
|
||||
output.close_file = True
|
||||
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)
|
||||
|
||||
content = mock_file.getvalue()
|
||||
assert "prowler-cloud-cta" not in content
|
||||
assert "cloud.prowler.com" not in content
|
||||
assert "utm_campaign=report-handoff" not in content
|
||||
assert PROWLER_CLOUD_PRICING_URL not in content
|
||||
assert PROWLER_CLOUD_IMPORT_DOCS_URL not in content
|
||||
assert pass_html_finding in content
|
||||
Reference in new issue
Block a user