Skip to content

Commit 876f694

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 876f694

File tree

2 files changed

+59
-9
lines changed

2 files changed

+59
-9
lines changed

src/fromager/external_commands.py

Lines changed: 28 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,24 @@ class NetworkIsolationError(subprocess.CalledProcessError):
4647
pass
4748

4849

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

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-
)
92+
cmd_str = " ".join(shlex.quote(str(s)) for s in cmd)
93+
env_str = " ".join(f"{k}={shlex.quote(v)}" for k, v in mask_envvars(env))
94+
cwd_str = cwd or os.path.abspath(os.getcwd())
95+
96+
logger.debug("running: %s %s in %s", cmd_str, env_str, cwd_str)
7997
if log_filename:
8098
with open(log_filename, "w") as log_file:
99+
print(f"cmd: {cmd_str}", file=log_file)
100+
print(f"env: {env_str}", file=log_file)
101+
print(f"cwd: {cwd_str}", file=log_file)
102+
log_file.flush()
81103
completed = subprocess.run(
82104
cmd,
83105
cwd=cwd,

tests/test_external_commands.py

Lines changed: 31 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,25 @@
89

910
from fromager import external_commands
1011

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

1232
def test_external_commands_environ():
1333
env = {"BLAH": "test"}
@@ -18,15 +38,23 @@ def test_external_commands_environ():
1838
def test_external_commands_log_file(tmp_path):
1939
log_filename = pathlib.Path(tmp_path) / "test.log"
2040
env = {"BLAH": "test"}
41+
cmd = ["sh", "-c", "echo $BLAH"]
2142
output = external_commands.run(
22-
["sh", "-c", "echo $BLAH"],
43+
cmd,
2344
extra_environ=env,
2445
log_filename=log_filename,
2546
)
26-
assert "test\n" == output
47+
assert output.endswith("test\n")
48+
2749
assert log_filename.exists()
2850
file_contents = log_filename.read_text()
29-
assert "test\n" == file_contents
51+
assert file_contents == output
52+
53+
file_lines = file_contents.split("\n")
54+
assert file_lines[0] == f"cmd: {shlex.join(cmd)}"
55+
assert file_lines[1].startswith("env:")
56+
assert file_lines[2].startswith("cwd:")
57+
assert file_lines[3] == "test"
3058

3159

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

0 commit comments

Comments
 (0)