Skip to content

Commit b431951

Browse files
committed
Increase logging for external commands
The `external_commands.run()` function now logs all environment variables. Before it only logged extra environment variables defined in a plugin. The logs were missing information about global environment variables, e.g. env vars defined in a build container. These additional env vars influence a build, too. The run log file now contains the command, env vars, and current working directory, too. Potentially sensitive env vars are masked. This is an additional precaution. GitHub and GitLab already mask sensitive values in logs. Signed-off-by: Christian Heimes <cheimes@redhat.com>
1 parent f4cb469 commit b431951

File tree

2 files changed

+55
-9
lines changed

2 files changed

+55
-9
lines changed

src/fromager/external_commands.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import os
33
import pathlib
4+
import re
45
import shlex
56
import subprocess
67
import sys
@@ -46,6 +47,21 @@ class NetworkIsolationError(subprocess.CalledProcessError):
4647
pass
4748

4849

50+
# GitLab BOT_PAT
51+
# SECRET, PASSWORD, PASSPHRASE, CRED(entials), TOKEN
52+
SENSITIVE_KEYS = re.compile("^(BOT_PAT|.*SECRET.*|.*PASSWORD.*|.*PASSPHRASE.*|.*CRED.*|.*TOKEN.*)$", re.IGNORECASE)
53+
54+
def mask_envvars(env: dict[str, typing.Any]) -> typing.Iterator[tuple[str, str]]:
55+
"""Mask sensistive env keys, shlex quote others"""
56+
result: dict[str, typing.Any] = {}
57+
for key, value in sorted(env.items()):
58+
if SENSITIVE_KEYS.match(key):
59+
value = "'[MASKED]'"
60+
else:
61+
value = shlex.quote(str(value))
62+
yield key, value
63+
64+
4965
# based on pyproject_hooks/_impl.py: quiet_subprocess_runner
5066
def run(
5167
cmd: typing.Sequence[str],
@@ -70,14 +86,17 @@ def run(
7086
*cmd,
7187
]
7288

73-
logger.debug(
74-
"running: %s %s in %s",
75-
" ".join(f"{k}={shlex.quote(v)}" for k, v in extra_environ.items()),
76-
" ".join(shlex.quote(str(s)) for s in cmd),
77-
cwd or ".",
78-
)
89+
cmd_str = " ".join(shlex.quote(str(s)) for s in cmd)
90+
env_str = " ".join(f"{k}={shlex.quote(v)}" for k, v in mask_envvars(env))
91+
cwd_str = cwd or os.path.abspath(os.getcwd())
92+
93+
logger.debug("running: %s %s in %s", cmd_str, env_str, cwd_str)
7994
if log_filename:
8095
with open(log_filename, "w") as log_file:
96+
print(f"cmd: {cmd_str}", file=log_file)
97+
print(f"env: {env_str}", file=log_file)
98+
print(f"cwd: {cwd_str}", file=log_file)
99+
log_file.flush()
81100
completed = subprocess.run(
82101
cmd,
83102
cwd=cwd,

tests/test_external_commands.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import pathlib
3+
import shlex
34
import subprocess
45
import typing
56
from unittest import mock
@@ -8,6 +9,24 @@
89

910
from fromager import external_commands
1011

12+
MASKED = "'[MASKED]'"
13+
14+
@pytest.mark.parametrize(
15+
"key,value,expected_value",
16+
[
17+
("KEY", "value", "value"),
18+
("KEY", "value;value", "'value;value'"),
19+
("BOT_PAT", "value", MASKED),
20+
("BOT_PATS", "value", "value"),
21+
("MY_SECRET", "value", MASKED),
22+
("MY_CREDENTIALS", "value", MASKED),
23+
("SOME_PASSWORD_VALUE", "value", MASKED),
24+
],
25+
)
26+
def test_mask_envvars(key: str, value: str, expected_value: str):
27+
out = dict(external_commands.mask_envvars({key: value}))
28+
assert out == {key: expected_value}
29+
1130

1231
def test_external_commands_environ():
1332
env = {"BLAH": "test"}
@@ -18,15 +37,23 @@ def test_external_commands_environ():
1837
def test_external_commands_log_file(tmp_path):
1938
log_filename = pathlib.Path(tmp_path) / "test.log"
2039
env = {"BLAH": "test"}
40+
cmd = ["sh", "-c", "echo $BLAH"]
2141
output = external_commands.run(
22-
["sh", "-c", "echo $BLAH"],
42+
cmd,
2343
extra_environ=env,
2444
log_filename=log_filename,
2545
)
26-
assert "test\n" == output
46+
assert output.endswith("test\n")
47+
2748
assert log_filename.exists()
2849
file_contents = log_filename.read_text()
29-
assert "test\n" == file_contents
50+
assert file_contents == output
51+
52+
file_lines = file_contents.split("\n")
53+
assert file_lines[0] == f"cmd: {shlex.join(cmd)}"
54+
assert file_lines[1].startswith("env:")
55+
assert file_lines[2].startswith("cwd:")
56+
assert file_lines[3] == "test"
3057

3158

3259
@mock.patch("subprocess.run", return_value=mock.Mock(returncode=0))

0 commit comments

Comments
 (0)