From 4bb1e5cff7b3ed4c9b60cc52ee32bd451ed5a158 Mon Sep 17 00:00:00 2001
From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
Date: Tue, 31 Mar 2026 15:01:09 +0200
Subject: [PATCH] fix(sdk): redact sensitive CLI flags in HTML output (#10518)
---
prowler/CHANGELOG.md | 4 +
prowler/lib/cli/parser.py | 2 +
prowler/lib/cli/redact.py | 68 +++++++++++
prowler/lib/outputs/html/html.py | 3 +-
.../github/lib/arguments/arguments.py | 3 +
.../providers/iac/lib/arguments/arguments.py | 2 +
.../mongodbatlas/lib/arguments/arguments.py | 3 +
.../providers/nhn/lib/arguments/arguments.py | 3 +
.../openstack/lib/arguments/arguments.py | 2 +
tests/lib/cli/redact_test.py | 108 ++++++++++++++++++
tests/lib/outputs/html/html_test.py | 3 +-
11 files changed, 199 insertions(+), 2 deletions(-)
create mode 100644 prowler/lib/cli/redact.py
create mode 100644 tests/lib/cli/redact_test.py
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index b2db4b7914..fd6dd121fe 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -30,6 +30,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `return` statements in `finally` blocks replaced across IAM, Organizations, GCP provider, and custom checks metadata to stop silently swallowing exceptions [(#10102)](https://github.com/prowler-cloud/prowler/pull/10102)
+### 🔐 Security
+
+- Sensitive CLI flag values (tokens, keys, passwords) in HTML output "Parameters used" field now redacted to prevent credential leaks [(#10518)](https://github.com/prowler-cloud/prowler/pull/10518)
+
---
## [5.22.1] (Prowler UNRELEASED)
diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py
index 651e9db477..e82ead1db9 100644
--- a/prowler/lib/cli/parser.py
+++ b/prowler/lib/cli/parser.py
@@ -19,6 +19,8 @@ from prowler.providers.common.arguments import (
validate_provider_arguments,
)
+SENSITIVE_ARGUMENTS = frozenset({"--shodan"})
+
class ProwlerArgumentParser:
# Set the default parser
diff --git a/prowler/lib/cli/redact.py b/prowler/lib/cli/redact.py
new file mode 100644
index 0000000000..2dfd9e2bfa
--- /dev/null
+++ b/prowler/lib/cli/redact.py
@@ -0,0 +1,68 @@
+from functools import lru_cache
+from importlib import import_module
+
+from prowler.lib.logger import logger
+from prowler.providers.common.provider import Provider, providers_path
+
+REDACTED_VALUE = "REDACTED"
+
+
+@lru_cache(maxsize=None)
+def get_sensitive_arguments() -> frozenset:
+ """Collect SENSITIVE_ARGUMENTS from all provider argument modules and the common parser."""
+ sensitive: set[str] = set()
+
+ # Common parser sensitive arguments (e.g., --shodan)
+ try:
+ parser_module = import_module("prowler.lib.cli.parser")
+ sensitive.update(getattr(parser_module, "SENSITIVE_ARGUMENTS", frozenset()))
+ except Exception as error:
+ logger.debug(f"Could not load SENSITIVE_ARGUMENTS from parser: {error}")
+
+ # Provider-specific sensitive arguments
+ for provider in Provider.get_available_providers():
+ try:
+ module = import_module(
+ f"{providers_path}.{provider}.lib.arguments.arguments"
+ )
+ sensitive.update(getattr(module, "SENSITIVE_ARGUMENTS", frozenset()))
+ except Exception as error:
+ logger.debug(f"Could not load SENSITIVE_ARGUMENTS from {provider}: {error}")
+
+ return frozenset(sensitive)
+
+
+def redact_argv(argv: list[str]) -> str:
+ """Redact values of sensitive CLI flags from an argument list.
+
+ Handles both ``--flag value`` and ``--flag=value`` syntax.
+ Returns a single joined string suitable for display.
+ """
+ sensitive = get_sensitive_arguments()
+ result: list[str] = []
+ skip_next = False
+
+ for i, arg in enumerate(argv):
+ if skip_next:
+ result.append(REDACTED_VALUE)
+ skip_next = False
+ continue
+
+ # Handle --flag=value syntax
+ if "=" in arg:
+ flag = arg.split("=", 1)[0]
+ if flag in sensitive:
+ result.append(f"{flag}={REDACTED_VALUE}")
+ continue
+
+ # Handle --flag value syntax
+ if arg in sensitive:
+ result.append(arg)
+ # Only redact the next token if it exists and is not another flag
+ if i + 1 < len(argv) and not argv[i + 1].startswith("-"):
+ skip_next = True
+ continue
+
+ result.append(arg)
+
+ return " ".join(result)
diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py
index d17320453b..4ffbf1f402 100644
--- a/prowler/lib/outputs/html/html.py
+++ b/prowler/lib/outputs/html/html.py
@@ -9,6 +9,7 @@ from prowler.config.config import (
square_logo_img,
timestamp,
)
+from prowler.lib.cli.redact import redact_argv
from prowler.lib.logger import logger
from prowler.lib.outputs.output import Finding, Output
from prowler.lib.outputs.utils import parse_html_string, unroll_dict
@@ -196,7 +197,7 @@ class HTML(Output):
- Parameters used: {" ".join(sys.argv[1:]) if from_cli else ""}
+ Parameters used: {redact_argv(sys.argv[1:]) if from_cli else ""}
Date: {timestamp.isoformat()}
diff --git a/prowler/providers/github/lib/arguments/arguments.py b/prowler/providers/github/lib/arguments/arguments.py
index 57aed863ab..748d77a927 100644
--- a/prowler/providers/github/lib/arguments/arguments.py
+++ b/prowler/providers/github/lib/arguments/arguments.py
@@ -1,3 +1,6 @@
+SENSITIVE_ARGUMENTS = frozenset({"--personal-access-token", "--oauth-app-token"})
+
+
def init_parser(self):
"""Init the Github Provider CLI parser"""
github_parser = self.subparsers.add_parser(
diff --git a/prowler/providers/iac/lib/arguments/arguments.py b/prowler/providers/iac/lib/arguments/arguments.py
index af4bf476e8..a489eb3063 100644
--- a/prowler/providers/iac/lib/arguments/arguments.py
+++ b/prowler/providers/iac/lib/arguments/arguments.py
@@ -1,5 +1,7 @@
import re
+SENSITIVE_ARGUMENTS = frozenset({"--personal-access-token", "--oauth-app-token"})
+
SCANNERS_CHOICES = [
"vuln",
"misconfig",
diff --git a/prowler/providers/mongodbatlas/lib/arguments/arguments.py b/prowler/providers/mongodbatlas/lib/arguments/arguments.py
index 13bdb2a975..64aff68343 100644
--- a/prowler/providers/mongodbatlas/lib/arguments/arguments.py
+++ b/prowler/providers/mongodbatlas/lib/arguments/arguments.py
@@ -1,3 +1,6 @@
+SENSITIVE_ARGUMENTS = frozenset({"--atlas-private-key"})
+
+
def init_parser(self):
"""Initialize the MongoDB Atlas Provider CLI parser"""
mongodbatlas_parser = self.subparsers.add_parser(
diff --git a/prowler/providers/nhn/lib/arguments/arguments.py b/prowler/providers/nhn/lib/arguments/arguments.py
index d4925090e6..8f7c71fd7c 100644
--- a/prowler/providers/nhn/lib/arguments/arguments.py
+++ b/prowler/providers/nhn/lib/arguments/arguments.py
@@ -1,3 +1,6 @@
+SENSITIVE_ARGUMENTS = frozenset({"--nhn-password"})
+
+
def init_parser(self):
"""Init the NHN Provider CLI parser"""
nhn_parser = self.subparsers.add_parser(
diff --git a/prowler/providers/openstack/lib/arguments/arguments.py b/prowler/providers/openstack/lib/arguments/arguments.py
index e77e129d1d..459012c4ec 100644
--- a/prowler/providers/openstack/lib/arguments/arguments.py
+++ b/prowler/providers/openstack/lib/arguments/arguments.py
@@ -1,5 +1,7 @@
from argparse import Namespace
+SENSITIVE_ARGUMENTS = frozenset({"--os-password"})
+
def init_parser(self):
"""Initialize the OpenStack provider CLI parser."""
diff --git a/tests/lib/cli/redact_test.py b/tests/lib/cli/redact_test.py
new file mode 100644
index 0000000000..1f33998356
--- /dev/null
+++ b/tests/lib/cli/redact_test.py
@@ -0,0 +1,108 @@
+from unittest.mock import patch
+
+import pytest
+
+from prowler.lib.cli.redact import REDACTED_VALUE, get_sensitive_arguments, redact_argv
+
+
+@pytest.fixture
+def mock_sensitive_args():
+ """Mock get_sensitive_arguments to return a known set."""
+ sensitive = frozenset(
+ {"--shodan", "--personal-access-token", "--atlas-private-key"}
+ )
+ with patch(
+ "prowler.lib.cli.redact.get_sensitive_arguments", return_value=sensitive
+ ):
+ yield sensitive
+
+
+class TestRedactArgv:
+ def test_empty_argv(self, mock_sensitive_args):
+ assert redact_argv([]) == ""
+
+ def test_no_sensitive_flags(self, mock_sensitive_args):
+ argv = ["aws", "--region", "eu-west-1", "--output-formats", "html"]
+ assert redact_argv(argv) == "aws --region eu-west-1 --output-formats html"
+
+ def test_sensitive_flag_with_value(self, mock_sensitive_args):
+ argv = ["aws", "--shodan", "abc123"]
+ assert redact_argv(argv) == f"aws --shodan {REDACTED_VALUE}"
+
+ def test_sensitive_flag_with_equals_syntax(self, mock_sensitive_args):
+ argv = ["aws", "--shodan=abc123"]
+ assert redact_argv(argv) == f"aws --shodan={REDACTED_VALUE}"
+
+ def test_sensitive_flag_at_end_without_value(self, mock_sensitive_args):
+ argv = ["aws", "--shodan"]
+ assert redact_argv(argv) == "aws --shodan"
+
+ def test_sensitive_flag_followed_by_another_flag(self, mock_sensitive_args):
+ argv = ["aws", "--shodan", "--region", "eu-west-1"]
+ # --region starts with '-', so --shodan value is not redacted (it has no value)
+ assert redact_argv(argv) == "aws --shodan --region eu-west-1"
+
+ def test_multiple_sensitive_flags(self, mock_sensitive_args):
+ argv = [
+ "github",
+ "--personal-access-token",
+ "ghp_secret123",
+ "--shodan",
+ "shodan_key",
+ ]
+ assert (
+ redact_argv(argv)
+ == f"github --personal-access-token {REDACTED_VALUE} --shodan {REDACTED_VALUE}"
+ )
+
+ def test_mixed_sensitive_and_non_sensitive(self, mock_sensitive_args):
+ argv = [
+ "mongodbatlas",
+ "--atlas-private-key",
+ "my_secret",
+ "--atlas-project-id",
+ "proj123",
+ ]
+ assert (
+ redact_argv(argv)
+ == f"mongodbatlas --atlas-private-key {REDACTED_VALUE} --atlas-project-id proj123"
+ )
+
+ def test_sensitive_flag_equals_with_other_args(self, mock_sensitive_args):
+ argv = [
+ "aws",
+ "--region",
+ "us-east-1",
+ "--shodan=key123",
+ "--output-formats",
+ "html",
+ ]
+ assert (
+ redact_argv(argv)
+ == f"aws --region us-east-1 --shodan={REDACTED_VALUE} --output-formats html"
+ )
+
+ def test_non_sensitive_flag_with_equals(self, mock_sensitive_args):
+ argv = ["aws", "--region=us-east-1"]
+ assert redact_argv(argv) == "aws --region=us-east-1"
+
+
+class TestGetSensitiveArguments:
+ def test_discovers_known_sensitive_arguments(self):
+ """Integration test: verify the discovery mechanism finds flags from provider modules."""
+ get_sensitive_arguments.cache_clear()
+ result = get_sensitive_arguments()
+ assert "--shodan" in result
+ assert "--personal-access-token" in result
+ assert "--oauth-app-token" in result
+ assert "--atlas-private-key" in result
+ assert "--nhn-password" in result
+ assert "--os-password" in result
+
+ def test_does_not_include_non_sensitive_flags(self):
+ """Verify non-sensitive flags are not in the set."""
+ get_sensitive_arguments.cache_clear()
+ result = get_sensitive_arguments()
+ assert "--region" not in result
+ assert "--profile" not in result
+ assert "--output-formats" not in result
diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py
index 7bb5256d81..c8d4ac199d 100644
--- a/tests/lib/outputs/html/html_test.py
+++ b/tests/lib/outputs/html/html_test.py
@@ -4,6 +4,7 @@ from io import StringIO
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.providers.github.models import GithubAppIdentityInfo
@@ -473,7 +474,7 @@ def get_aws_html_header(args: list) -> str:
- Parameters used: {" ".join(args)}
+ Parameters used: {redact_argv(args)}
Date: {timestamp.isoformat()}