187 lines
7.3 KiB
Python
187 lines
7.3 KiB
Python
"""Tests for keeping secret values out of the output of a run whose logs are kept."""
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
import alakazam
|
|
from alakazam import generate_all_secrets, insert_secret, is_secret_command
|
|
|
|
VALUE = "hunter2-do-not-log-me"
|
|
|
|
|
|
@pytest.fixture(params=[False, True], ids=["visible", "hidden"])
|
|
def hide(request, monkeypatch):
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", request.param)
|
|
return request.param
|
|
|
|
|
|
class TestIsSecretCommand:
|
|
@pytest.mark.parametrize("args", [
|
|
("app", "secret", "insert", "login.a.org", "db_password", "v1", VALUE),
|
|
("app", "secret", "generate", "-a", "login.a.org"),
|
|
])
|
|
def test_commands_carrying_a_value(self, args):
|
|
assert is_secret_command(args)
|
|
|
|
@pytest.mark.parametrize("args", [
|
|
("app", "secret", "ls", "login.a.org"),
|
|
("app", "secret", "rm", "login.a.org", "db_password"),
|
|
("app", "ls"),
|
|
("app", "secret"),
|
|
])
|
|
def test_commands_without_a_value(self, args):
|
|
assert not is_secret_command(args)
|
|
|
|
|
|
class TestGeneratedValues:
|
|
def install(self, monkeypatch, created=False):
|
|
def abra(*args, **kwargs):
|
|
if args[:3] == ("app", "secret", "ls"):
|
|
return [{"name": "db_password", "created on server": str(created).lower()}]
|
|
if args[:3] == ("app", "secret", "generate"):
|
|
return [{"name": "db_password", "value": VALUE}]
|
|
raise AssertionError(f"unexpected: {args}")
|
|
monkeypatch.setattr(alakazam, "abra", abra)
|
|
|
|
def test_the_name_is_always_reported(self, hide, monkeypatch, capsys):
|
|
self.install(monkeypatch)
|
|
generate_all_secrets("login.a.org")
|
|
assert "db_password" in capsys.readouterr().out
|
|
|
|
def test_the_value_follows_the_switch(self, hide, monkeypatch, capsys):
|
|
self.install(monkeypatch)
|
|
generate_all_secrets("login.a.org")
|
|
out = capsys.readouterr().out
|
|
assert (VALUE in out) is not hide
|
|
assert ("[hidden]" in out) is hide
|
|
|
|
|
|
class TestDebugLog:
|
|
"""-l debug is the level a pipeline reaches for when something breaks."""
|
|
|
|
def run_abra(self, monkeypatch, args):
|
|
class Process:
|
|
returncode = 0
|
|
stdout = f'[{{"name":"db_password","value":"{VALUE}"}}]'.encode()
|
|
stderr = b""
|
|
monkeypatch.setattr(alakazam.subprocess, "run", lambda cmd, capture_output: Process())
|
|
return alakazam.abra(*args)
|
|
|
|
def test_the_command_line_is_not_logged_when_hidden(self, monkeypatch, caplog):
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", True)
|
|
with caplog.at_level(logging.DEBUG):
|
|
self.run_abra(monkeypatch, ("app", "secret", "insert", "login.a.org", "db_password", "v1", VALUE))
|
|
assert VALUE not in caplog.text
|
|
|
|
def test_the_generated_output_is_not_logged_when_hidden(self, monkeypatch, caplog):
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", True)
|
|
with caplog.at_level(logging.DEBUG):
|
|
self.run_abra(monkeypatch, ("app", "secret", "generate", "-a", "login.a.org"))
|
|
assert VALUE not in caplog.text
|
|
|
|
def test_other_commands_are_still_logged(self, monkeypatch, caplog):
|
|
"""Hiding secrets must not turn the debug log off altogether."""
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", True)
|
|
with caplog.at_level(logging.DEBUG):
|
|
self.run_abra(monkeypatch, ("app", "ls"))
|
|
assert "run command" in caplog.text
|
|
|
|
def test_insert_logs_the_name_but_not_the_value(self, monkeypatch, caplog):
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", True)
|
|
monkeypatch.setattr(alakazam, "abra", lambda *a, **k: "")
|
|
with caplog.at_level(logging.DEBUG):
|
|
insert_secret("login.a.org", "db_password", VALUE)
|
|
assert "db_password" in caplog.text
|
|
assert VALUE not in caplog.text
|
|
|
|
|
|
class TestLocalHookOutput:
|
|
"""A recipe may print the secret it created, vaultwarden's admin token does exactly that."""
|
|
|
|
TOKEN = "vaultwarden-admin-token-in-plain"
|
|
|
|
def install(self, monkeypatch, fail=False):
|
|
seen = {}
|
|
|
|
class Process:
|
|
returncode = 1 if fail else 0
|
|
stdout = TestLocalHookOutput.TOKEN.encode()
|
|
stderr = b""
|
|
|
|
def run(cmd, capture_output=False, **kwargs):
|
|
seen["capture_output"] = capture_output
|
|
return Process()
|
|
|
|
monkeypatch.setattr(alakazam.subprocess, "run", run)
|
|
def streamed(cmd):
|
|
seen["streamed"] = True
|
|
return Process()
|
|
|
|
monkeypatch.setattr(alakazam, "run_streamed", streamed)
|
|
return seen
|
|
|
|
def hook(self, strict=False):
|
|
alakazam.run_secret_hooks("login.a.org", {"secret_hooks": ["insert_admin_token"], "server": "a.org"}, strict=strict)
|
|
|
|
def test_the_hook_is_streamed_when_not_hiding(self, monkeypatch):
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", False)
|
|
seen = self.install(monkeypatch)
|
|
self.hook()
|
|
assert seen.get("streamed")
|
|
|
|
def test_the_hook_is_not_streamed_when_hiding(self, monkeypatch, capsys):
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", True)
|
|
seen = self.install(monkeypatch)
|
|
self.hook()
|
|
assert not seen.get("streamed")
|
|
assert self.TOKEN not in capsys.readouterr().out
|
|
|
|
def test_a_failing_hook_does_not_leak_through_the_error(self, monkeypatch):
|
|
"""The captured output lands in the exception, which is where it would escape."""
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", True)
|
|
self.install(monkeypatch, fail=True)
|
|
with pytest.raises(alakazam.click.ClickException) as excinfo:
|
|
self.hook(strict=True)
|
|
assert self.TOKEN not in excinfo.value.message
|
|
assert "withheld by --hide-secrets" in excinfo.value.message
|
|
|
|
def test_a_failing_hook_still_reports_its_output_when_not_hiding(self, monkeypatch):
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", False)
|
|
self.install(monkeypatch, fail=True)
|
|
with pytest.raises(alakazam.click.ClickException) as excinfo:
|
|
self.hook(strict=True)
|
|
assert self.TOKEN in excinfo.value.message
|
|
|
|
|
|
class TestLocalScriptOutput:
|
|
def install(self, monkeypatch, tmp_path):
|
|
script = tmp_path / "hook.sh"
|
|
script.write_text("#!/bin/sh\necho secret\n")
|
|
script.chmod(0o755)
|
|
seen = {}
|
|
|
|
class Result:
|
|
returncode = 0
|
|
|
|
monkeypatch.setattr(alakazam, "resolve_path", lambda p, base=None: script)
|
|
monkeypatch.setattr(alakazam.subprocess, "run",
|
|
lambda cmd, **kw: seen.update(kw) or Result())
|
|
return seen
|
|
|
|
def test_the_script_output_is_captured_when_hiding(self, monkeypatch, tmp_path):
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", True)
|
|
seen = self.install(monkeypatch, tmp_path)
|
|
alakazam.run_local_script(["script", "hook.sh"], "login.a.org", "a.org", "a.org")
|
|
assert seen["capture_output"] is True
|
|
|
|
def test_the_script_output_is_passed_through_when_not_hiding(self, monkeypatch, tmp_path):
|
|
monkeypatch.setattr(alakazam, "HIDE_SECRETS", False)
|
|
seen = self.install(monkeypatch, tmp_path)
|
|
alakazam.run_local_script(["script", "hook.sh"], "login.a.org", "a.org", "a.org")
|
|
assert seen["capture_output"] is False
|