mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
chore(v4): show prowler cloud + version upgrade
This commit is contained in:
+28
-1
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user