Skip to content

Commit

Permalink
Ensure log does not contain ANSI sequences or control codes
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Jan 14, 2024
1 parent be9d845 commit 9ed70cd
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 8 deletions.
30 changes: 22 additions & 8 deletions src/briefcase/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from pathlib import Path

from rich.console import Console as RichConsole
from rich.control import strip_control_codes
from rich.highlighter import RegexHighlighter
from rich.markup import escape
from rich.progress import (
Expand All @@ -29,6 +30,9 @@
# Regex to identify settings likely to contain sensitive information
SENSITIVE_SETTING_RE = re.compile(r"API|TOKEN|KEY|SECRET|PASS|SIGNATURE", flags=re.I)

# 7-bit C1 ANSI escape sequences
ANSI_ESCAPE_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")


class InputDisabled(Exception):
def __init__(self):
Expand All @@ -37,6 +41,15 @@ def __init__(self):
)


def sanitize_text(text: str) -> str:
"""Remove control codes and ANSI escape sequences from a line of text.
This is useful for extracting the plain text from the output of third party tools
that may be including markup for display in the console.
"""
return ANSI_ESCAPE_RE.sub("", strip_control_codes(text))


class RichConsoleHighlighter(RegexHighlighter):
"""Custom Rich highlighter for printing to the console.
Expand Down Expand Up @@ -102,18 +115,18 @@ def __call__(cls, *messages, stack_offset=5, show=True, **kwargs):
cls.to_log(*messages, stack_offset=stack_offset, **kwargs)

@classmethod
def to_console(cls, *renderables, **kwargs):
"""Specialized print to the console only omitting the log."""
cls.console.print(*renderables, **kwargs)
def to_console(cls, *messages, **kwargs):
"""Write only to the console and skip writing to the log."""
cls.console.print(*messages, **kwargs)

@classmethod
def to_log(cls, *renderables, stack_offset=5, **kwargs):
"""Specialized print to the log only omitting the console."""
cls.log.log(*renderables, _stack_offset=stack_offset, **kwargs)
def to_log(cls, *messages, stack_offset=5, **kwargs):
"""Write only to the log and skip writing to the console."""
cls.log.log(*map(sanitize_text, messages), _stack_offset=stack_offset, **kwargs)

@classmethod
def export_log(cls):
"""Export the text of the entire log."""
"""Export the text of the entire log; the log is also cleared."""
return cls.log.export_text()


Expand Down Expand Up @@ -399,7 +412,8 @@ def _build_log(self, command):
# build log header and export buffered log from Rich
uname = platform.uname()
sanitized_env_vars = "\n".join(
f" {env_var}={value if not SENSITIVE_SETTING_RE.search(env_var) else '********************'}"
f"\t{env_var}="
f"{sanitize_text(value) if not SENSITIVE_SETTING_RE.search(env_var) else '********************'}"
for env_var, value in sorted(command.tools.os.environ.items())
)
return (
Expand Down
4 changes: 4 additions & 0 deletions tests/console/test_Log.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def test_save_log_to_file_no_exception(mock_now, command, tmp_path):
command.tools.os.environ = {
"GITHUB_KEY": "super-secret-key",
"ANDROID_HOME": "/androidsdk",
"ANSI_ENV_VAR": f"{chr(7)}this is sanitized env var: \u001b[31mred",
}

logger = Log(verbosity=LogLevel.DEBUG)
Expand All @@ -177,6 +178,7 @@ def test_save_log_to_file_no_exception(mock_now, command, tmp_path):
logger.error("this is error output")
logger.print("this is print output")
logger.print.to_log("this is log output")
logger.print.to_log(f"{chr(7)}this is sanitized log output: \u001b[31mred")
logger.print.to_console("this is console output")

logger.info("this is [bold]info output with markup[/bold]")
Expand Down Expand Up @@ -206,9 +208,11 @@ def test_save_log_to_file_no_exception(mock_now, command, tmp_path):
assert "this is error output" in log_contents
assert "this is print output" in log_contents
assert "this is log output" in log_contents
assert "this is sanitized log output: red" in log_contents
assert "this is console output" not in log_contents
# Environment variables are in the output
assert "ANDROID_HOME=/androidsdk" in log_contents
assert "ANSI_ENV_VAR=this is sanitized env var: red" in log_contents
assert "GITHUB_KEY=********************" in log_contents
assert "GITHUB_KEY=super-secret-key" not in log_contents
# Environment variables are sorted
Expand Down
33 changes: 33 additions & 0 deletions tests/console/test_sanitize_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import pytest

from briefcase.console import sanitize_text


@pytest.mark.parametrize(
"input_text, sanitized_text",
[
(
"log output",
"log output",
),
(
"ls\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\n\x1b[01;31m",
"ls\nexamplefile.zip\n",
),
(
"log output: \u001b[31mRed\u001B[0m",
"log output: Red",
),
(
"\u001b[1mbold log output:\u001b[0m \u001b[4mUnderline\u001b[0m",
"bold log output: Underline",
),
(
f"{chr(7)}{chr(8)}{chr(11)}{chr(12)}{chr(13)}log{chr(7)} output{chr(7)}",
"log output",
),
],
)
def test_sanitize_text(input_text, sanitized_text):
"""Text is sanitized as expected."""
assert sanitize_text(input_text) == sanitized_text

0 comments on commit 9ed70cd

Please sign in to comment.