chore(v3): show prowler cloud + version upgrade

This commit is contained in:
pedrooot committed 2026-08-13 11:13:36 +02:00
1 parent a952d1d08e
commit d53d06d012
5 files changed
+175 -15

No files matched your search

+28 -1
View File
@@ -3,10 +3,16 @@
import os
import sys
import threading
from colorama import Fore, Style
from prowler.lib.banner import print_banner
from prowler.config.config import get_available_update
from prowler.lib.banner import (
print_banner,
print_prowler_cloud_banner,
print_update_notice,
)
from prowler.lib.check.check import (
bulk_load_checks_metadata,
bulk_load_compliance_frameworks,
@@ -73,6 +79,18 @@ def prowler():
compliance_framework = args.compliance
custom_checks_metadata_file = args.custom_checks_metadata_file
# Check in the background whether a newer Prowler release is available;
# the result is printed at the end of the scan so the check never adds
# latency. Skipped with --no-banner/--only-logs and via
# PROWLER_NO_VERSION_CHECK/DO_NOT_TRACK (handled in get_available_update).
available_update = {}
update_check_thread = threading.Thread(
target=lambda: available_update.update(latest=get_available_update()),
daemon=True,
)
if not args.no_banner and not args.only_logs:
update_check_thread.start()
if not args.no_banner:
print_banner(args)
@@ -325,6 +343,15 @@ def prowler():
audit_output_options.output_directory,
)
# Promote Prowler Cloud as the last thing the user sees after the results,
# preceded by an update notice when a newer release is available
if not args.no_banner and not args.only_logs:
if update_check_thread.is_alive():
update_check_thread.join(timeout=2)
if available_update.get("latest"):
print_update_notice(available_update["latest"])
print_prowler_cloud_banner()
# If custom checks were passed, remove the modules
if checks_folder:
remove_custom_checks_module(checks_folder, provider)
+40 -13
View File
@@ -11,7 +11,7 @@ from prowler.lib.logger import logger
timestamp = datetime.today()
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
prowler_version = "3.16.17"
prowler_version = "3.16.18"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
html_logo_img = "https://user-images.githubusercontent.com/3985464/113734260-7ba06900-96fb-11eb-82bc-d4f68a1e2710.png"
square_logo_img = "https://user-images.githubusercontent.com/38561120/235905862-9ece5bd7-9aa3-4e48-807a-3a9035eb8bfb.png"
@@ -64,23 +64,50 @@ default_config_file_path = (
encoding_format_utf_8 = "utf-8"
def check_current_version():
def get_latest_release_version():
"""Return the latest Prowler release tag name from GitHub, or None if it cannot be retrieved."""
try:
prowler_version_string = f"Prowler {prowler_version}"
release_response = requests.get(
"https://api.github.com/repos/prowler-cloud/prowler/tags", timeout=1
)
latest_version = release_response.json()[0]["name"]
if latest_version != prowler_version:
return f"{prowler_version_string} (latest is {latest_version}, upgrade for the latest features)"
else:
return (
f"{prowler_version_string} (You are running the latest version, yay!)"
)
except requests.RequestException:
return f"{prowler_version_string}"
return release_response.json()[0]["name"]
except Exception:
return f"{prowler_version_string}"
return None
def _version_tuple(version_string):
"""Return a comparable tuple from a dotted version string."""
return tuple(int(part) for part in version_string.split("."))
def get_available_update():
"""Return the latest Prowler version if it is newer than the running one, None otherwise.
Honors the PROWLER_NO_VERSION_CHECK and DO_NOT_TRACK environment variables:
when either is set, no network call is made and None is returned.
"""
if os.environ.get("PROWLER_NO_VERSION_CHECK") or os.environ.get("DO_NOT_TRACK"):
return None
latest_version = get_latest_release_version()
try:
if latest_version and _version_tuple(latest_version) > _version_tuple(
prowler_version
):
return latest_version
except Exception:
return None
return None
def check_current_version():
prowler_version_string = f"Prowler {prowler_version}"
latest_version = get_latest_release_version()
if not latest_version:
return prowler_version_string
if latest_version != prowler_version:
return f"{prowler_version_string} (latest is {latest_version}, upgrade for the latest features)"
else:
return f"{prowler_version_string} (You are running the latest version, yay!)"
def change_config_var(variable: str, value: str, audit_info):
+67
View File
@@ -2,6 +2,73 @@ from colorama import Fore, Style
from prowler.config.config import banner_color, orange_color, prowler_version, timestamp
# Prowler Cloud landing URL used by the CLI banner. The visible text stays
# "cloud.prowler.com" while the clickable target carries the UTM parameters so
# terminals that support OSC 8 hyperlinks attribute the visit to the v3 CLI.
CLOUD_DISPLAY_TEXT = "cloud.prowler.com"
CLOUD_BANNER_URL = (
"https://cloud.prowler.com/sign-up?utm_source=prowler-cli&utm_content=v3"
)
def _hyperlink(url, text):
"""Wrap ``text`` in an OSC 8 terminal hyperlink pointing to ``url``.
Terminals that support OSC 8 render ``text`` as a clickable link to ``url``;
those that do not simply display ``text`` unchanged.
"""
return f"\033]8;;{url}\033\\{text}\033]8;;\033\\"
def print_update_notice(latest_version):
"""
Prints a notice that a newer Prowler version is available.
Parameters:
- latest_version (str): The latest released Prowler version.
Returns:
- None
"""
print(
f"\n{Fore.YELLOW}A new version of Prowler is available: {prowler_version} → {latest_version}{Style.RESET_ALL}\n"
f"Upgrading from Prowler v3 is a major version upgrade — see the release notes at\n"
f"https://github.com/prowler-cloud/prowler/releases before upgrading.\n"
f"Upgrade with: {Style.BRIGHT}pipx upgrade prowler{Style.RESET_ALL} "
f"(disable this check with PROWLER_NO_VERSION_CHECK=1)"
)
def print_prowler_cloud_banner():
"""
Prints a promotional banner highlighting what Prowler Cloud adds on top of
the open-source CLI.
Shown at the end of a scan to let users know about the managed platform
capabilities they are missing.
Returns:
- None
"""
check = f"{Fore.GREEN}✓{Style.RESET_ALL}"
bar = f"{banner_color}│{Style.RESET_ALL}"
print(f"""
{bar} {Style.BRIGHT}You're getting a snapshot 📸. Prowler Cloud gives you the full picture:{Style.RESET_ALL}
{bar}
{bar} {check} {Style.BRIGHT}Send your findings{Style.RESET_ALL} - directly from the Prowler CLI to Prowler Cloud.
{bar} {check} {Style.BRIGHT}Continuous Security Monitoring{Style.RESET_ALL} - custom scheduling and scan configuration with history, trends and alerts.
{bar} {check} {Style.BRIGHT}Triage{Style.RESET_ALL} - review findings, flag false positives and track accepted risk with your team.
{bar} {check} {Style.BRIGHT}Lighthouse AI + MCP{Style.RESET_ALL} - autonomous triage, custom dashboards, prioritization with prevention and remediation.
{bar} {check} {Style.BRIGHT}Alerts{Style.RESET_ALL} - get notified when anything you want is happening.
{bar} {check} {Style.BRIGHT}Live Compliance{Style.RESET_ALL} - dashboards for 50+ frameworks, always up to date.
{bar} {check} {Style.BRIGHT}Remediation{Style.RESET_ALL} - complete guided remediation including Autonomous remediation with Lighthouse AI.
{bar} {check} {Style.BRIGHT}Attack Path Visualization{Style.RESET_ALL} - see how attackers chain risks to reach your crown jewels.
{bar} {check} {Style.BRIGHT}Bulk Provisioning{Style.RESET_ALL} - add your entire AWS Organization in seconds.
{bar} {check} {Style.BRIGHT}Integrations{Style.RESET_ALL} - Anything with our MCP + Jira, Slack, AWS Security Hub, Amazon S3, SSO and RBAC.
{bar}
{bar} {banner_color}Start free at 👉 {_hyperlink(CLOUD_BANNER_URL, CLOUD_DISPLAY_TEXT)}{Style.RESET_ALL}
""")
def print_banner(args):
banner = rf"""{banner_color} _
+1 -1
View File
@@ -22,7 +22,7 @@ packages = [
{include = "prowler"}
]
readme = "README.md"
version = "3.16.17"
version = "3.16.18"
[tool.poetry.dependencies]
alive-progress = "3.1.5"
+39
View File
@@ -8,6 +8,7 @@ from prowler.config.config import (
change_config_var,
check_current_version,
get_available_compliance_frameworks,
get_available_update,
load_and_validate_config_file,
)
from prowler.providers.aws.aws_provider import get_aws_available_regions
@@ -134,6 +135,44 @@ class Test_Config:
== f"Prowler {MOCK_OLD_PROWLER_VERSION} (latest is {MOCK_PROWLER_VERSION}, upgrade for the latest features)"
)
@mock.patch(
"prowler.config.config.requests.get", new=mock_prowler_get_latest_release
)
@mock.patch("prowler.config.config.prowler_version", new=MOCK_OLD_PROWLER_VERSION)
def test_get_available_update_with_old_version(self):
assert get_available_update() == MOCK_PROWLER_VERSION
@mock.patch(
"prowler.config.config.requests.get", new=mock_prowler_get_latest_release
)
@mock.patch("prowler.config.config.prowler_version", new=MOCK_PROWLER_VERSION)
def test_get_available_update_with_latest_version(self):
assert get_available_update() is None
@mock.patch(
"prowler.config.config.requests.get", new=mock_prowler_get_latest_release
)
@mock.patch("prowler.config.config.prowler_version", new=MOCK_OLD_PROWLER_VERSION)
@mock.patch.dict(os.environ, {"PROWLER_NO_VERSION_CHECK": "1"})
def test_get_available_update_opt_out_env_var(self):
assert get_available_update() is None
@mock.patch(
"prowler.config.config.requests.get", new=mock_prowler_get_latest_release
)
@mock.patch("prowler.config.config.prowler_version", new=MOCK_OLD_PROWLER_VERSION)
@mock.patch.dict(os.environ, {"DO_NOT_TRACK": "1"})
def test_get_available_update_do_not_track(self):
assert get_available_update() is None
@mock.patch(
"prowler.config.config.requests.get",
new=mock.MagicMock(side_effect=Exception("network error")),
)
@mock.patch("prowler.config.config.prowler_version", new=MOCK_OLD_PROWLER_VERSION)
def test_get_available_update_network_failure(self):
assert get_available_update() is None
def test_change_config_var_aws(self):
audit_info = AWS_Audit_Info(
session_config=None,