fix(sdk): redact sensitive CLI flags in HTML output (#10518)

This commit is contained in:
Andoni Alonso
2026-03-31 15:01:09 +02:00
committed by GitHub
parent 99b80ebbd9
commit 4bb1e5cff7
11 changed files with 199 additions and 2 deletions
+4
View File
@@ -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)
+2
View File
@@ -19,6 +19,8 @@ from prowler.providers.common.arguments import (
validate_provider_arguments,
)
SENSITIVE_ARGUMENTS = frozenset({"--shodan"})
class ProwlerArgumentParser:
# Set the default parser
+68
View File
@@ -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)
+2 -1
View File
@@ -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):
</div>
</li>
<li class="list-group-item">
<b>Parameters used:</b> {" ".join(sys.argv[1:]) if from_cli else ""}
<b>Parameters used:</b> {redact_argv(sys.argv[1:]) if from_cli else ""}
</li>
<li class="list-group-item">
<b>Date:</b> {timestamp.isoformat()}
@@ -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(
@@ -1,5 +1,7 @@
import re
SENSITIVE_ARGUMENTS = frozenset({"--personal-access-token", "--oauth-app-token"})
SCANNERS_CHOICES = [
"vuln",
"misconfig",
@@ -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(
@@ -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(
@@ -1,5 +1,7 @@
from argparse import Namespace
SENSITIVE_ARGUMENTS = frozenset({"--os-password"})
def init_parser(self):
"""Initialize the OpenStack provider CLI parser."""
+108
View File
@@ -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
+2 -1
View File
@@ -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:
</div>
</li>
<li class="list-group-item">
<b>Parameters used:</b> {" ".join(args)}
<b>Parameters used:</b> {redact_argv(args)}
</li>
<li class="list-group-item">
<b>Date:</b> {timestamp.isoformat()}