Skip to content

Commit ca01304

Browse files
committed
Improve logging of environment variables
Fixes: #3542
1 parent e243a3a commit ca01304

File tree

4 files changed

+74
-2
lines changed

4 files changed

+74
-2
lines changed

docs/changelog/3542.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improves logging of environment variables by sorting them by key and redacting
2+
the values for the ones that are likely to contain secrets.

docs/user_guide.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,17 @@ CLI
441441
have a stale Python environment; e.g. ``tox run -e py310 -r`` would clean the run environment and recreate it from
442442
scratch.
443443

444+
Logging
445+
~~~~~~~
446+
447+
Tox logs its activity inside ``.tox/<env_name>/log`` which can prove to be a good
448+
source of information when debugging its behavior. It should be noted that
449+
some of the environment variables with names containing one of the words
450+
``access``, ``api``, ``auth``, ``client``, ``cred``, ``key``, ``passwd``,
451+
``password``, ``private``, ``pwd``, ``secret`` and ``token`` will be logged
452+
with their values redacted ``*`` to prevent accidental secret leaking when tox
453+
is used in CI/CD environments (as log collection is common).
454+
444455
Config files
445456
~~~~~~~~~~~~
446457

src/tox/tox_env/api.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,30 @@
3131
from tox.tox_env.installer import Installer
3232

3333
LOGGER = logging.getLogger(__name__)
34+
# Based on original gitleaks rule named generic-api-key
35+
# See: https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#L587
36+
secret_keywords = [
37+
"access",
38+
"api",
39+
"auth",
40+
"client",
41+
"cred",
42+
"key",
43+
"passwd",
44+
"password",
45+
"private",
46+
"pwd",
47+
"secret",
48+
"token",
49+
]
50+
SECRET_ENV_VAR_REGEX = re.compile(".*(" + "|".join(secret_keywords) + ").*", re.IGNORECASE)
51+
52+
53+
def redact_value(name: str, value: str) -> str:
54+
"""Returns a redacted text if the key name looks like a secret."""
55+
if SECRET_ENV_VAR_REGEX.match(name):
56+
return "*" * len(value)
57+
return value
3458

3559

3660
class ToxEnvCreateArgs(NamedTuple):
@@ -461,8 +485,11 @@ def _write_execute_log(env_name: str, log_file: Path, request: ExecuteRequest, s
461485
with log_file.open("wt", encoding="utf-8") as file:
462486
file.write(f"name: {env_name}\n")
463487
file.write(f"run_id: {request.run_id}\n")
464-
for env_key, env_value in request.env.items():
465-
file.write(f"env {env_key}: {env_value}\n")
488+
msg = ""
489+
for env_key, env_value in sorted(request.env.items()):
490+
redacted_value = redact_value(name=env_key, value=env_value)
491+
msg += f"env {env_key}: {redacted_value}\n"
492+
file.write(msg)
466493
for meta_key, meta_value in status.metadata.items():
467494
file.write(f"metadata {meta_key}: {meta_value}\n")
468495
file.write(f"cwd: {request.cwd}\n")

tests/tox_env/test_api.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
from typing import TYPE_CHECKING
44

5+
import pytest
6+
7+
from tox.tox_env.api import redact_value
8+
59
if TYPE_CHECKING:
610
from pathlib import Path
711

@@ -32,3 +36,31 @@ def test_setenv_section_substitution(tox_project: ToxProjectCreator) -> None:
3236
project = tox_project({"tox.ini": ini})
3337
result = project.run()
3438
result.assert_success()
39+
40+
41+
@pytest.mark.parametrize(
42+
("key", "do_redact"),
43+
[
44+
pytest.param("SOME_KEY", True, id="key"),
45+
pytest.param("API_FOO", True, id="api"),
46+
pytest.param("AUTH", True, id="auth"),
47+
pytest.param("CLIENT", True, id="client"),
48+
pytest.param("DB_PASSWORD", True, id="password"),
49+
pytest.param("FOO", False, id="foo"),
50+
pytest.param("GITHUB_TOKEN", True, id="token"),
51+
pytest.param("NORMAL_VAR", False, id="other"),
52+
pytest.param("S_PASSWD", True, id="passwd"),
53+
pytest.param("SECRET", True, id="secret"),
54+
pytest.param("SOME_ACCESS", True, id="access"),
55+
pytest.param("MY_CRED", True, id="cred"),
56+
pytest.param("MY_PRIVATE", True, id="private"),
57+
pytest.param("MY_PWD", True, id="pwd"),
58+
],
59+
)
60+
def test_redact(key: str, do_redact: bool) -> None:
61+
"""Ensures that redact_value works as expected."""
62+
result = redact_value(key, "foo")
63+
if do_redact:
64+
assert result == "***"
65+
else:
66+
assert result == "foo"

0 commit comments

Comments
 (0)