mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
fix(sdk): redact sensitive CLI flags in HTML output (#10518)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -19,6 +19,8 @@ from prowler.providers.common.arguments import (
|
||||
validate_provider_arguments,
|
||||
)
|
||||
|
||||
SENSITIVE_ARGUMENTS = frozenset({"--shodan"})
|
||||
|
||||
|
||||
class ProwlerArgumentParser:
|
||||
# Set the default parser
|
||||
|
||||
@@ -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)
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
@@ -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()}
|
||||
|
||||
Reference in New Issue
Block a user