chore(v4): show prowler cloud + version upgrade

This commit is contained in:
pedrooot
2026-08-13 13:02:57 +02:00
parent 4fb010847f
commit 4a3c9b8dc3
5 changed files with 171 additions and 9 deletions
+28 -1
View File
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
import sys
import threading
from os import environ
from colorama import Fore, Style
@@ -10,11 +11,16 @@ from colorama import init as colorama_init
from prowler.config.config import (
csv_file_suffix,
get_available_compliance_frameworks,
get_available_update,
html_file_suffix,
json_asff_file_suffix,
json_ocsf_file_suffix,
)
from prowler.lib.banner import print_banner
from prowler.lib.banner import (
print_banner,
print_prowler_cloud_banner,
print_update_notice,
)
from prowler.lib.check.check import (
exclude_checks_to_run,
exclude_services_to_run,
@@ -118,6 +124,18 @@ def prowler():
if args.no_color:
colorama_init(strip=True)
# 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:
legend = args.verbose or getattr(args, "fixer", None)
print_banner(legend)
@@ -719,6 +737,15 @@ def prowler():
f"\nDetailed compliance results are in {Fore.YELLOW}{output_options.output_directory}/compliance/{Style.RESET_ALL}\n"
)
# 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)
+34 -7
View File
@@ -12,7 +12,7 @@ from prowler.lib.logger import logger
timestamp = datetime.today()
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
prowler_version = "4.6.2"
prowler_version = "4.6.3"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
@@ -86,23 +86,50 @@ def get_default_mute_file_path(provider: str):
return mutelist_path
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"]
return release_response.json()[0]["name"]
except Exception:
return None
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.parse(latest_version) > version.parse(
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
try:
if version.parse(latest_version) > version.parse(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}"
except Exception:
return f"{prowler_version_string}"
return prowler_version_string
def load_and_validate_config_file(provider: str, config_file_path: str) -> dict:
+69
View File
@@ -2,6 +2,75 @@ 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 v4 CLI.
CLOUD_DISPLAY_TEXT = "cloud.prowler.com"
CLOUD_BANNER_URL = (
"https://cloud.prowler.com/sign-up?utm_source=prowler-cli&utm_content=v4"
)
def _hyperlink(url: str, text: str) -> str:
"""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: str):
"""
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 v4 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(legend: bool = False):
"""
+1 -1
View File
@@ -23,7 +23,7 @@ packages = [
{include = "dashboard"}
]
readme = "README.md"
version = "4.6.2"
version = "4.6.3"
[tool.poetry.dependencies]
alive-progress = "3.2.0"
+39
View File
@@ -8,6 +8,7 @@ from requests import Response
from prowler.config.config import (
check_current_version,
get_available_compliance_frameworks,
get_available_update,
load_and_validate_config_file,
load_and_validate_fixer_config_file,
)
@@ -377,6 +378,44 @@ class Test_Config:
== f"Prowler {MOCK_PROWLER_MASTER_VERSION} (You are running the latest version, yay!)"
)
@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_get_available_compliance_frameworks(self):
compliance_frameworks = [
"cisa_aws",