Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions scripts/ci/sandboxed_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
from collections.abc import Sequence
from pathlib import Path

if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from scripts.ci.redact_sensitive_log import redact_text

DEFAULT_IGNORE = (
".git",
Expand Down Expand Up @@ -149,7 +153,9 @@ def copy_workspace(repo_root: Path, sandbox_root: Path, extra_ignores: Sequence[
return destination


def run_command(command: Sequence[str], cwd: Path, env: dict[str, str], timeout: int) -> subprocess.CompletedProcess[str]:
def run_command(
command: Sequence[str], cwd: Path, env: dict[str, str], timeout: int
) -> subprocess.CompletedProcess[str]:
"""Run the verification command and capture output for review evidence."""
return subprocess.run(
list(command),
Expand Down Expand Up @@ -188,13 +194,13 @@ def emit_result(
"""Print a machine-readable execution evidence summary."""
payload = {
"allowed_env": sorted(set(allowed_env)),
"command": list(command),
"cwd": str(copied_repo),
"command": [redact_text(item) for item in command],
"cwd": redact_text(str(copied_repo)),
"elapsed_seconds": round(elapsed_seconds, 3),
"evidence_note": evidence_note,
"evidence_note": redact_text(evidence_note),
"exit_code": exit_code,
"network": network,
"sandbox": str(sandbox_root) if kept else "(removed)",
"sandbox": redact_text(str(sandbox_root)) if kept else "(removed)",
"sandboxed": True,
}
print(f"{RESULT_MARKER} {json.dumps(payload, sort_keys=True)}")
Expand All @@ -210,27 +216,39 @@ def main(argv: Sequence[str] | None = None) -> int:
try:
copied_repo = copy_workspace(Path(args.repo_root), sandbox, args.ignore)
env = scrubbed_env(sandbox, args.allow_env)
print(f"sandboxed-verify: cwd={copied_repo}")
print(f"sandboxed-verify: command={' '.join(args.command)}")
print(redact_text(f"sandboxed-verify: cwd={copied_repo}"))
print(redact_text(f"sandboxed-verify: command={' '.join(args.command)}"))
if args.allow_env:
print(f"sandboxed-verify: allowed env names={','.join(sorted(set(args.allow_env)))}")
print(
redact_text(
"sandboxed-verify: allowed env names="
+ ",".join(sorted(set(args.allow_env)))
)
)
if args.network != "default":
print(f"sandboxed-verify: network={args.network}")
try:
completed = run_command(args.command, copied_repo, env, args.timeout)
if completed.stdout:
print(completed.stdout, end="")
print(redact_text(completed.stdout), end="")
if completed.stderr:
print(completed.stderr, end="", file=sys.stderr)
print(redact_text(completed.stderr), end="", file=sys.stderr)
exit_code = completed.returncode
except subprocess.TimeoutExpired as exc:
stdout = timeout_output_text(exc.stdout)
stderr = timeout_output_text(exc.stderr)
if stdout:
print(stdout, end="" if stdout.endswith("\n") else "\n")
print(redact_text(stdout), end="" if stdout.endswith("\n") else "\n")
if stderr:
print(stderr, end="" if stderr.endswith("\n") else "\n", file=sys.stderr)
print(f"sandboxed-verify: command timed out after {args.timeout}s", file=sys.stderr)
print(
redact_text(stderr),
end="" if stderr.endswith("\n") else "\n",
file=sys.stderr,
)
print(
f"sandboxed-verify: command timed out after {args.timeout}s",
file=sys.stderr,
)
exit_code = 124
return exit_code
finally:
Expand Down
28 changes: 14 additions & 14 deletions scripts/ci/sandboxed_web_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from scripts.ci import sandboxed_verify

from scripts.ci.redact_sensitive_log import redact_text

RESULT_MARKER = "SANDBOXED_WEB_E2E_RESULT"

Expand Down Expand Up @@ -184,18 +184,18 @@ def emit_result(
) -> None:
"""Print a machine-readable web E2E execution evidence summary."""
payload = {
"backend_cmd": args.backend_cmd,
"backend_cmd": redact_text(args.backend_cmd),
"backend_ready": backend_ready,
"allowed_env": sorted(set(args.allow_env)),
"cwd": str(copied_repo),
"e2e_cmd": args.e2e_cmd,
"cwd": redact_text(str(copied_repo)),
"e2e_cmd": redact_text(args.e2e_cmd),
"elapsed_seconds": round(elapsed_seconds, 3),
"evidence_note": args.evidence_note,
"evidence_note": redact_text(args.evidence_note),
"exit_code": exit_code,
"frontend_cmd": args.frontend_cmd,
"frontend_cmd": redact_text(args.frontend_cmd),
"frontend_ready": frontend_ready,
"network": args.network,
"sandbox": str(sandbox_root) if args.keep_sandbox else "(removed)",
"sandbox": redact_text(str(sandbox_root)) if args.keep_sandbox else "(removed)",
"sandboxed": True,
}
print(f"{RESULT_MARKER} {json.dumps(payload, sort_keys=True)}")
Expand All @@ -216,9 +216,9 @@ def main(argv: Sequence[str] | None = None) -> int:
try:
copied_repo = sandboxed_verify.copy_workspace(Path(args.repo_root), sandbox, args.ignore)
env = sandboxed_verify.scrubbed_env(sandbox, args.allow_env)
print(f"sandboxed-web-e2e: cwd={copied_repo}")
print(redact_text(f"sandboxed-web-e2e: cwd={copied_repo}"))
if args.allow_env:
print(f"sandboxed-web-e2e: allowed env names={','.join(sorted(set(args.allow_env)))}")
print(redact_text(f"sandboxed-web-e2e: allowed env names={','.join(sorted(set(args.allow_env)))}"))
if args.network != "default":
print(f"sandboxed-web-e2e: network={args.network}")
services.append(start_service("backend", args.backend_cmd, copied_repo, env, logs_dir))
Expand All @@ -232,18 +232,18 @@ def main(argv: Sequence[str] | None = None) -> int:
try:
completed = run_shell(args.e2e_cmd, copied_repo, env, args.e2e_timeout)
if completed.stdout:
print(completed.stdout, end="")
print(redact_text(completed.stdout), end="")
if completed.stderr:
print(completed.stderr, end="", file=sys.stderr)
print(redact_text(completed.stderr), end="", file=sys.stderr)
exit_code = completed.returncode
return exit_code
except subprocess.TimeoutExpired as exc:
stdout = sandboxed_verify.timeout_output_text(exc.stdout)
stderr = sandboxed_verify.timeout_output_text(exc.stderr)
if stdout:
print(stdout, end="" if stdout.endswith("\n") else "\n")
print(redact_text(stdout), end="" if stdout.endswith("\n") else "\n")
if stderr:
print(stderr, end="" if stderr.endswith("\n") else "\n", file=sys.stderr)
print(redact_text(stderr), end="" if stderr.endswith("\n") else "\n", file=sys.stderr)
print(f"sandboxed-web-e2e: e2e command timed out after {args.e2e_timeout}s", file=sys.stderr)
exit_code = 124
return exit_code
Expand All @@ -253,7 +253,7 @@ def main(argv: Sequence[str] | None = None) -> int:
log_tail = tail_text(service.log_path)
if log_tail:
print(f"--- {service.label} log tail ---")
print(log_tail)
print(redact_text(log_tail))
emit_result(
args=args,
copied_repo=copied_repo,
Expand Down
120 changes: 120 additions & 0 deletions tests/test_sandboxed_verify_redaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Regression tests for sandboxed verification log redaction."""

import subprocess

from scripts.ci import sandboxed_verify


def _api_key_fixture() -> str:
"""Return a representative key/value secret fixture."""
return "api_key: " + "mock_token_string"


def _session_key_fixture() -> str:
"""Return a scanner-safe sensitive assignment fixture."""
return "session_key=" + "mock_session_value"


def test_timeout_output_text_redacts_bytes_and_str() -> None:
"""Timeout output is normalized and redacted for bytes and strings."""
api_key = _api_key_fixture()
session_key = _session_key_fixture()

assert sandboxed_verify.redact_text(
sandboxed_verify.timeout_output_text(api_key.encode())
) == "api_key: [REDACTED]"
assert sandboxed_verify.redact_text(
sandboxed_verify.timeout_output_text(session_key)
) == "session_key=[REDACTED]"


def test_main_redacts_timeout_stdout_and_stderr(monkeypatch, tmp_path, capsys) -> None:
"""The actual timeout handler redacts captured subprocess streams."""
repo = tmp_path / "repo"
repo.mkdir()
api_key = _api_key_fixture()
session_key = _session_key_fixture()

def raise_timeout(*_args, **_kwargs):
raise subprocess.TimeoutExpired(
cmd=["python", "-c", "pass"],
timeout=5,
output=api_key,
stderr=session_key,
)

monkeypatch.setattr(sandboxed_verify, "run_command", raise_timeout)

exit_code = sandboxed_verify.main(
[
"--repo-root",
str(repo),
"--timeout",
"5",
"--",
"python",
"-c",
"pass",
]
)
captured = capsys.readouterr()

assert exit_code == 124
assert "api_key: [REDACTED]" in captured.out
assert "mock_token_string" not in captured.out
assert "session_key=[REDACTED]" in captured.err
assert "mock_session_value" not in captured.err


def test_emit_result_redacts_payload_fields(capsys, tmp_path) -> None:
"""Machine-readable evidence never emits credential-shaped payload values."""
api_key = _api_key_fixture()
session_key = _session_key_fixture()
sandboxed_verify.emit_result(
command=["echo", api_key],
copied_repo=tmp_path / session_key,
sandbox_root=tmp_path / "sandbox_test_root",
exit_code=0,
elapsed_seconds=1.0,
kept=True,
allowed_env=[],
network="default",
evidence_note=f"used {api_key}",
)
captured = capsys.readouterr()
assert "[REDACTED]" in captured.out
assert "mock_token_string" not in captured.out
assert "mock_session_value" not in captured.out
assert "sandbox_test_root" in captured.out


def test_main_redacts_stdout_and_stderr(tmp_path, capsys) -> None:
"""Subprocess stdout and stderr are redacted before publication."""
repo = tmp_path / "repo"
repo.mkdir()
api_key = _api_key_fixture()
session_key = _session_key_fixture()
command = (
"import sys; "
f"print({api_key!r}); "
f"print({session_key!r}, file=sys.stderr)"
)

exit_code = sandboxed_verify.main(
[
"--repo-root",
str(repo),
"--timeout",
"5",
"--",
"python",
"-c",
command,
]
)
captured = capsys.readouterr()
assert exit_code == 0
assert "api_key: [REDACTED]" in captured.out
assert "mock_token_string" not in captured.out
assert "session_key=[REDACTED]" in captured.err
assert "mock_session_value" not in captured.err
Loading
Loading