From aa6dbec89b0ded9b795a2ba3b2e24069e4167bf6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:46:41 +0900 Subject: [PATCH 01/24] fix(ci): redact sandboxed verification output --- scripts/ci/sandboxed_verify.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/ci/sandboxed_verify.py b/scripts/ci/sandboxed_verify.py index aace18d45..3650893c3 100644 --- a/scripts/ci/sandboxed_verify.py +++ b/scripts/ci/sandboxed_verify.py @@ -14,6 +14,9 @@ from collections.abc import Sequence from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from scripts.ci.redact_sensitive_log import redact_text DEFAULT_IGNORE = ( ".git", @@ -169,8 +172,8 @@ def timeout_output_text(value: str | bytes | None) -> str: if value is None: return "" if isinstance(value, bytes): - return value.decode(errors="replace") - return value + return redact_text(value.decode(errors="replace")) + return redact_text(value) def emit_result( @@ -219,9 +222,9 @@ def main(argv: Sequence[str] | None = None) -> int: 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) From 06f1dcdb18015034f0182604fad1fd494584efdd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:47:45 +0900 Subject: [PATCH 02/24] fix(ci): redact sandboxed web E2E output --- scripts/ci/sandboxed_web_e2e.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index ae0c3105a..39e2fed16 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -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" @@ -232,9 +232,9 @@ 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: From df52aa3c58c26a97689cc403a77582e13e854777 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:48:25 +0900 Subject: [PATCH 03/24] test(ci): cover secret-safe sandbox output --- tests/test_sandboxed_output_redaction.py | 131 +++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/test_sandboxed_output_redaction.py diff --git a/tests/test_sandboxed_output_redaction.py b/tests/test_sandboxed_output_redaction.py new file mode 100644 index 000000000..4373a496f --- /dev/null +++ b/tests/test_sandboxed_output_redaction.py @@ -0,0 +1,131 @@ +"""Regression tests for secret-safe sandboxed command output.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +from scripts.ci import sandboxed_verify, sandboxed_web_e2e + + +def _fake_personal_access_token() -> str: + """Build a PAT-shaped fixture without storing a scanner-triggering literal.""" + + return "gh" + "p_" + "123456789012345678901234567890123456" + + +def test_timeout_output_redacts_text_and_bytes() -> None: + """Timeout normalization must redact secrets in both subprocess payload types.""" + + token = _fake_personal_access_token() + + assert sandboxed_verify.timeout_output_text(f"text {token}\n") == "text [REDACTED]\n" + assert sandboxed_verify.timeout_output_text(f"bytes {token}\n".encode()) == ( + "bytes [REDACTED]\n" + ) + + +def test_sandboxed_verify_redacts_completed_output( + monkeypatch, tmp_path: Path, capsys +) -> None: + """Ordinary verification stdout and stderr must not disclose captured secrets.""" + + repo_root = tmp_path / "repo" + repo_root.mkdir() + token = _fake_personal_access_token() + + monkeypatch.setattr( + sandboxed_verify, + "run_command", + lambda command, cwd, env, timeout: subprocess.CompletedProcess( + command, + 0, + stdout=f"verify-out {token}\n", + stderr=f"verify-err {token}\n", + ), + ) + + assert ( + sandboxed_verify.main( + ["--repo-root", str(repo_root), "--timeout", "5", "--", "true"] + ) + == 0 + ) + captured = capsys.readouterr() + + assert token not in captured.out + assert token not in captured.err + assert "verify-out [REDACTED]" in captured.out + assert "verify-err [REDACTED]" in captured.err + + +def test_sandboxed_web_e2e_redacts_completed_output( + monkeypatch, tmp_path: Path, capsys +) -> None: + """Web E2E command output must be redacted before reaching Actions logs.""" + + repo_root = tmp_path / "repo" + repo_root.mkdir() + token = _fake_personal_access_token() + + class FinishedProcess: + """Minimal completed-service process used by the orchestration test.""" + + def poll(self) -> int: + """Report that the synthetic service has already exited cleanly.""" + + return 0 + + def fake_start_service(label, command, cwd, env, logs_dir): + """Return a bounded service record without starting a real subprocess.""" + + return sandboxed_web_e2e.Service( + label=label, + command=command, + process=FinishedProcess(), + log_path=logs_dir / f"{label}.log", + ) + + monkeypatch.setattr(sandboxed_web_e2e, "start_service", fake_start_service) + monkeypatch.setattr( + sandboxed_web_e2e, + "wait_for_url", + lambda url, timeout, service: True, + ) + monkeypatch.setattr( + sandboxed_web_e2e, + "run_shell", + lambda command, cwd, env, timeout: subprocess.CompletedProcess( + command, + 0, + stdout=f"e2e-out {token}\n", + stderr=f"e2e-err {token}\n", + ), + ) + monkeypatch.setattr(sandboxed_web_e2e, "stop_service", lambda service: None) + + assert ( + sandboxed_web_e2e.main( + [ + "--repo-root", + str(repo_root), + "--backend-cmd", + "backend", + "--frontend-cmd", + "frontend", + "--e2e-cmd", + "e2e", + "--startup-timeout", + "5", + "--e2e-timeout", + "5", + ] + ) + == 0 + ) + captured = capsys.readouterr() + + assert token not in captured.out + assert token not in captured.err + assert "e2e-out [REDACTED]" in captured.out + assert "e2e-err [REDACTED]" in captured.err From 014f30ce8ad0276576f1ff914d2b7d64a62049a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 09:49:45 +0900 Subject: [PATCH 04/24] docs(doctoring): record sandbox log redaction boundary --- .../sandboxed-command-log-redaction.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/doctoring/sandboxed-command-log-redaction.md diff --git a/docs/doctoring/sandboxed-command-log-redaction.md b/docs/doctoring/sandboxed-command-log-redaction.md new file mode 100644 index 000000000..1563f4d9c --- /dev/null +++ b/docs/doctoring/sandboxed-command-log-redaction.md @@ -0,0 +1,38 @@ +# Sandboxed command log redaction + +## Decision + +Captured `stdout` and `stderr` from repository verification and web end-to-end commands are untrusted evidence. Before those streams are written to GitHub Actions logs, they pass through the existing central `redact_text` boundary. The same rule applies to ordinary process completion and `TimeoutExpired` payloads represented as either text or bytes. + +This change preserves the command exit status, line boundaries, non-sensitive diagnostics, machine-readable result envelope, isolated workspace, scrubbed environment, bounded timeouts, and service cleanup. It does not synthesize success, discard stderr, widen an environment allowlist, or alter the review gate. + +## Threat model + +A tool, test, package manager, browser runner, or application process can print credentials obtained from an explicitly allowed environment variable, configuration file, exception, dependency-manager diagnostic, or echoed request. CI logs are durable review evidence and can have a broader readership than the secret itself. Redaction therefore occurs at the final central output sink rather than relying on every child process to behave correctly. + +MITRE classifies writing sensitive information to a log as CWE-532 and recommends not writing secrets to log files. OWASP likewise identifies access tokens, passwords, connection strings, encryption keys, and other primary secrets as data that should be removed, masked, sanitized, hashed, or encrypted before logging. The Python subprocess API does not implicitly select a system shell when `shell=False`; structured argument execution is retained independently of the log-redaction control. + +## Verification contract + +Permanent regression tests must prove that: + +1. text and byte timeout payloads redact a PAT-shaped fixture; +2. normal sandbox verification stdout and stderr redact the same fixture; +3. normal sandboxed web E2E stdout and stderr redact the same fixture; +4. the raw fixture never appears in captured output; +5. ordinary non-sensitive text and process exit codes remain visible; +6. the test fixture is assembled from separate fragments so repository secret scanning does not mistake it for a live credential. + +Exact-head repository tests, statement/branch coverage, docstring checks, Secret Scan, Semgrep, CodeQL, Security Scan, OpenCode, Noema, and branch protection remain authoritative. + +## Modular boundary + +Both wrappers remain independently executable scripts and reusable Python modules. They depend only on the central redaction utility and standard-library process primitives, so product repositories can consume the same behavior through the organization workflow without copying a repository-local implementation. + +## References + +MITRE. (2026, April 30). *CWE-532: Insertion of sensitive information into log file*. Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/532.html + +OWASP Foundation. (n.d.). *Logging cheat sheet*. OWASP Cheat Sheet Series. Retrieved August 5, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html + +Python Software Foundation. (2026). *subprocess—Subprocess management* (Python 3.14.6 documentation). https://docs.python.org/3/library/subprocess.html From e1658340e16e1562d92bd96525aa873849692104 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:05:49 +0900 Subject: [PATCH 05/24] test(ci): require complete sandbox output redaction --- tests/test_sandboxed_output_redaction.py | 213 ++++++++++++++--------- 1 file changed, 130 insertions(+), 83 deletions(-) diff --git a/tests/test_sandboxed_output_redaction.py b/tests/test_sandboxed_output_redaction.py index 4373a496f..38611c217 100644 --- a/tests/test_sandboxed_output_redaction.py +++ b/tests/test_sandboxed_output_redaction.py @@ -1,131 +1,178 @@ -"""Regression tests for secret-safe sandboxed command output.""" +"""Regression tests for secret-safe sandbox subprocess evidence.""" from __future__ import annotations import subprocess from pathlib import Path +from typing import cast from scripts.ci import sandboxed_verify, sandboxed_web_e2e +from scripts.ci.redact_sensitive_log import ( + REDACTED, + redact_command_arguments, + redact_shell_command, +) -def _fake_personal_access_token() -> str: - """Build a PAT-shaped fixture without storing a scanner-triggering literal.""" +def _provider_token() -> str: + """Build a credential-shaped fixture without committing a scanner secret.""" + return "gh" + "p_" + ("A" * 36) - return "gh" + "p_" + "123456789012345678901234567890123456" +def test_redact_command_arguments_covers_separate_equals_and_direct_tokens() -> None: + """Redact option values, assignments, and provider-shaped standalone values.""" + token = _provider_token() -def test_timeout_output_redacts_text_and_bytes() -> None: - """Timeout normalization must redact secrets in both subprocess payload types.""" + assert redact_command_arguments( + ["tool", "--api-key", token, f"TOKEN={token}", token, "plain"] + ) == [ + "tool", + "--api-key", + REDACTED, + f"TOKEN={REDACTED}", + REDACTED, + "plain", + ] - token = _fake_personal_access_token() - assert sandboxed_verify.timeout_output_text(f"text {token}\n") == "text [REDACTED]\n" - assert sandboxed_verify.timeout_output_text(f"bytes {token}\n".encode()) == ( - "bytes [REDACTED]\n" - ) +def test_redact_shell_command_handles_parsed_and_malformed_input() -> None: + """Redact parsed commands and fall back to line redaction for bad quoting.""" + token = _provider_token() + parsed = redact_shell_command(f"tool --password {token} --name safe") + malformed = redact_shell_command(f"api_key={token}'") + + assert token not in parsed + assert "--password '[REDACTED]'" in parsed + assert token not in malformed + assert REDACTED in malformed + +def test_timeout_output_text_redacts_strings_bytes_and_none() -> None: + """Normalize every TimeoutExpired payload form without leaking credentials.""" + token = _provider_token() -def test_sandboxed_verify_redacts_completed_output( + assert sandboxed_verify.timeout_output_text(None) == "" + assert sandboxed_verify.timeout_output_text(f"token={token}") == f"token={REDACTED}" + assert sandboxed_verify.timeout_output_text(f"token={token}".encode()) == f"token={REDACTED}" + + +def test_sandboxed_verify_redacts_completed_output_command_and_note( monkeypatch, tmp_path: Path, capsys ) -> None: - """Ordinary verification stdout and stderr must not disclose captured secrets.""" - - repo_root = tmp_path / "repo" - repo_root.mkdir() - token = _fake_personal_access_token() + """Keep ordinary command completion evidence secret-free end to end.""" + token = _provider_token() + repository = tmp_path / "repository" + repository.mkdir() - monkeypatch.setattr( - sandboxed_verify, - "run_command", - lambda command, cwd, env, timeout: subprocess.CompletedProcess( + def fake_run_command(command, cwd, env, timeout): + return subprocess.CompletedProcess( command, 0, - stdout=f"verify-out {token}\n", - stderr=f"verify-err {token}\n", - ), - ) - - assert ( - sandboxed_verify.main( - ["--repo-root", str(repo_root), "--timeout", "5", "--", "true"] + stdout=f"token={token}\n", + stderr=f"Authorization: Bearer {token}\n", ) - == 0 + + monkeypatch.setattr(sandboxed_verify, "run_command", fake_run_command) + + exit_code = sandboxed_verify.main( + [ + "--repo-root", + str(repository), + "--evidence-note", + f"api_key={token}", + "--", + "tool", + "--api-key", + token, + ] ) captured = capsys.readouterr() + assert exit_code == 0 assert token not in captured.out assert token not in captured.err - assert "verify-out [REDACTED]" in captured.out - assert "verify-err [REDACTED]" in captured.err + assert REDACTED in captured.out + assert REDACTED in captured.err -def test_sandboxed_web_e2e_redacts_completed_output( - monkeypatch, tmp_path: Path, capsys -) -> None: - """Web E2E command output must be redacted before reaching Actions logs.""" +class _DoneProcess: + """Minimal completed-process double accepted by the service cleanup path.""" - repo_root = tmp_path / "repo" - repo_root.mkdir() - token = _fake_personal_access_token() + pid = 12345 - class FinishedProcess: - """Minimal completed-service process used by the orchestration test.""" + def poll(self) -> int: + """Report that the fake service has already exited.""" + return 0 - def poll(self) -> int: - """Report that the synthetic service has already exited cleanly.""" + def wait(self, timeout: int) -> int: + """Return immediately for interface compatibility.""" + del timeout + return 0 - return 0 - def fake_start_service(label, command, cwd, env, logs_dir): - """Return a bounded service record without starting a real subprocess.""" +def test_sandboxed_web_e2e_redacts_commands_output_and_service_logs( + monkeypatch, tmp_path: Path, capsys +) -> None: + """Keep web E2E output, JSON evidence, and service log tails secret-free.""" + token = _provider_token() + repository = tmp_path / "repository" + repository.mkdir() + def fake_start_service(label, command, cwd, env, logs_dir): + del cwd, env + log_path = logs_dir / f"{label}.log" + log_path.write_text(f"secret={token}\n", encoding="utf-8") return sandboxed_web_e2e.Service( label=label, command=command, - process=FinishedProcess(), - log_path=logs_dir / f"{label}.log", + process=cast(subprocess.Popen[str], _DoneProcess()), + log_path=log_path, ) - monkeypatch.setattr(sandboxed_web_e2e, "start_service", fake_start_service) - monkeypatch.setattr( - sandboxed_web_e2e, - "wait_for_url", - lambda url, timeout, service: True, - ) - monkeypatch.setattr( - sandboxed_web_e2e, - "run_shell", - lambda command, cwd, env, timeout: subprocess.CompletedProcess( + def fake_run_shell(command, cwd, env, timeout): + del cwd, env, timeout + return subprocess.CompletedProcess( command, 0, - stdout=f"e2e-out {token}\n", - stderr=f"e2e-err {token}\n", - ), - ) - monkeypatch.setattr(sandboxed_web_e2e, "stop_service", lambda service: None) - - assert ( - sandboxed_web_e2e.main( - [ - "--repo-root", - str(repo_root), - "--backend-cmd", - "backend", - "--frontend-cmd", - "frontend", - "--e2e-cmd", - "e2e", - "--startup-timeout", - "5", - "--e2e-timeout", - "5", - ] + stdout=f"token={token}\n", + stderr=f"Bearer {token}\n", ) - == 0 + + monkeypatch.setattr(sandboxed_web_e2e, "start_service", fake_start_service) + monkeypatch.setattr(sandboxed_web_e2e, "wait_for_url", lambda url, timeout, service: True) + monkeypatch.setattr(sandboxed_web_e2e, "run_shell", fake_run_shell) + + exit_code = sandboxed_web_e2e.main( + [ + "--repo-root", + str(repository), + "--backend-cmd", + f"backend --token {token}", + "--frontend-cmd", + f"frontend TOKEN={token}", + "--e2e-cmd", + f"e2e --api-key {token}", + "--evidence-note", + f"secret={token}", + ] ) captured = capsys.readouterr() + assert exit_code == 0 assert token not in captured.out assert token not in captured.err - assert "e2e-out [REDACTED]" in captured.out - assert "e2e-err [REDACTED]" in captured.err + assert captured.out.count(REDACTED) >= 7 + assert REDACTED in captured.err + + +def test_tail_text_handles_missing_and_bounded_existing_logs(tmp_path: Path) -> None: + """Return nothing for missing logs and redact only the requested final lines.""" + token = _provider_token() + missing = tmp_path / "missing.log" + log_path = tmp_path / "service.log" + log_path.write_text(f"first\nsecond token={token}\nthird\n", encoding="utf-8") + + assert sandboxed_web_e2e.tail_text(missing) == "" + tail = sandboxed_web_e2e.tail_text(log_path, max_lines=2) + assert tail == f"second token={REDACTED}\nthird" + assert token not in tail From d71f13372398e51ba9767e9bfd8daee302bbe43d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:08:27 +0900 Subject: [PATCH 06/24] fix(ci): redact every sandbox evidence publication sink --- scripts/ci/redact_sensitive_log.py | 41 ++++++++++++++++++++++++++++++ scripts/ci/sandboxed_verify.py | 21 ++++++++++----- scripts/ci/sandboxed_web_e2e.py | 21 ++++++++------- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/scripts/ci/redact_sensitive_log.py b/scripts/ci/redact_sensitive_log.py index cb89fe67b..0b72e6f07 100644 --- a/scripts/ci/redact_sensitive_log.py +++ b/scripts/ci/redact_sensitive_log.py @@ -5,7 +5,9 @@ import json import re +import shlex import sys +from collections.abc import Sequence from typing import Any REDACTED = "[REDACTED]" @@ -15,6 +17,11 @@ r"api[_-]?key|private[_-]?key|access[_-]?key|session[_-]?key)", re.IGNORECASE, ) +SENSITIVE_OPTION_RE = re.compile( + r"(?:token|secret|password|passwd|credential|authorization|jwt|" + r"api[-_]?key|private[-_]?key|access[-_]?key|session[-_]?key)", + re.IGNORECASE, +) JWT_RE = re.compile( r"(? str: return "".join(output) +def redact_command_arguments(arguments: Sequence[str]) -> list[str]: + """Return command arguments with sensitive option values redacted.""" + redacted: list[str] = [] + redact_next = False + for raw_argument in arguments: + argument = str(raw_argument) + if redact_next: + redacted.append(REDACTED) + redact_next = False + continue + + option = argument.lstrip("-") + if "=" in option: + key, _value = option.split("=", 1) + if SENSITIVE_OPTION_RE.fullmatch(key): + separator_index = argument.find("=") + redacted.append(f"{argument[: separator_index + 1]}{REDACTED}") + continue + + redacted.append(redact_text(argument)) + if SENSITIVE_OPTION_RE.fullmatch(option): + redact_next = True + return redacted + + +def redact_shell_command(command: str) -> str: + """Return a shell command safe for logs without executing or expanding it.""" + try: + arguments = shlex.split(command) + except ValueError: + return redact_text(command) + return shlex.join(redact_command_arguments(arguments)) + + def main() -> int: """Redact standard input to standard output.""" sys.stdout.write(redact_text(sys.stdin.read())) diff --git a/scripts/ci/sandboxed_verify.py b/scripts/ci/sandboxed_verify.py index 3650893c3..683c07256 100644 --- a/scripts/ci/sandboxed_verify.py +++ b/scripts/ci/sandboxed_verify.py @@ -6,6 +6,7 @@ import json import os import re +import shlex import shutil import subprocess import sys @@ -14,9 +15,14 @@ from collections.abc import Sequence from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from scripts.ci.redact_sensitive_log import ( + redact_command_arguments, + redact_text, +) -from scripts.ci.redact_sensitive_log import redact_text DEFAULT_IGNORE = ( ".git", @@ -168,7 +174,7 @@ def run_command(command: Sequence[str], cwd: Path, env: dict[str, str], timeout: def timeout_output_text(value: str | bytes | None) -> str: - """Return timeout output as text, regardless of subprocess internals.""" + """Return redacted timeout output as text, regardless of subprocess internals.""" if value is None: return "" if isinstance(value, bytes): @@ -188,13 +194,13 @@ def emit_result( network: str, evidence_note: str, ) -> None: - """Print a machine-readable execution evidence summary.""" + """Print a machine-readable execution evidence summary without secrets.""" payload = { "allowed_env": sorted(set(allowed_env)), - "command": list(command), + "command": redact_command_arguments(command), "cwd": 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)", @@ -214,7 +220,8 @@ def main(argv: Sequence[str] | None = None) -> int: 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)}") + safe_command = shlex.join(redact_command_arguments(args.command)) + print(f"sandboxed-verify: command={safe_command}") if args.allow_env: print(f"sandboxed-verify: allowed env names={','.join(sorted(set(args.allow_env)))}") if args.network != "default": diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index 39e2fed16..5dc0d61d6 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -22,7 +22,8 @@ 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 +from scripts.ci.redact_sensitive_log import redact_shell_command, redact_text + RESULT_MARKER = "SANDBOXED_WEB_E2E_RESULT" @@ -110,6 +111,7 @@ def start_service(label: str, command: str, cwd: Path, env: dict[str, str], logs stdout=log_file, stderr=subprocess.STDOUT, start_new_session=True, + shell=False, ) log_file.close() return Service(label=label, command=command, process=process, log_path=log_path) @@ -136,7 +138,7 @@ def wait_for_url(url: str, timeout: int, service: Service) -> bool: def run_shell(command: str, cwd: Path, env: dict[str, str], timeout: int) -> subprocess.CompletedProcess[str]: - """Run a shell command and capture its output.""" + """Run a shell-style command without invoking a shell and capture output.""" return subprocess.run( shlex.split(command), cwd=cwd, @@ -146,6 +148,7 @@ def run_shell(command: str, cwd: Path, env: dict[str, str], timeout: int) -> sub stderr=subprocess.PIPE, timeout=timeout, check=False, + shell=False, ) @@ -165,11 +168,11 @@ def stop_service(service: Service) -> None: def tail_text(path: Path, max_lines: int = 80) -> str: - """Return the final lines of a service log.""" + """Return redacted final lines of a service log.""" if not path.exists(): return "" lines = path.read_text(encoding="utf-8", errors="replace").splitlines() - return "\n".join(lines[-max_lines:]) + return redact_text("\n".join(lines[-max_lines:])) def emit_result( @@ -182,17 +185,17 @@ def emit_result( exit_code: int, elapsed_seconds: float, ) -> None: - """Print a machine-readable web E2E execution evidence summary.""" + """Print machine-readable web E2E evidence without credential values.""" payload = { - "backend_cmd": args.backend_cmd, + "backend_cmd": redact_shell_command(args.backend_cmd), "backend_ready": backend_ready, "allowed_env": sorted(set(args.allow_env)), "cwd": str(copied_repo), - "e2e_cmd": args.e2e_cmd, + "e2e_cmd": redact_shell_command(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_shell_command(args.frontend_cmd), "frontend_ready": frontend_ready, "network": args.network, "sandbox": str(sandbox_root) if args.keep_sandbox else "(removed)", From 27bb444c7ce2004f7b743b77404bc3528712e842 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:09:10 +0900 Subject: [PATCH 07/24] docs(ci): cover every sandbox evidence redaction sink --- .../sandboxed-command-log-redaction.md | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/docs/doctoring/sandboxed-command-log-redaction.md b/docs/doctoring/sandboxed-command-log-redaction.md index 1563f4d9c..652716e4f 100644 --- a/docs/doctoring/sandboxed-command-log-redaction.md +++ b/docs/doctoring/sandboxed-command-log-redaction.md @@ -1,37 +1,71 @@ -# Sandboxed command log redaction +# Sandboxed command and output redaction ## Decision -Captured `stdout` and `stderr` from repository verification and web end-to-end commands are untrusted evidence. Before those streams are written to GitHub Actions logs, they pass through the existing central `redact_text` boundary. The same rule applies to ordinary process completion and `TimeoutExpired` payloads represented as either text or bytes. +The central verification wrappers treat subprocess output, service log tails, command arguments, shell-command strings, and reviewer evidence notes as potentially sensitive before writing them to GitHub Actions logs or machine-readable review evidence. -This change preserves the command exit status, line boundaries, non-sensitive diagnostics, machine-readable result envelope, isolated workspace, scrubbed environment, bounded timeouts, and service cleanup. It does not synthesize success, discard stderr, widen an environment allowlist, or alter the review gate. +One trusted redaction module owns this publication boundary: + +- captured standard output and standard error are redacted before printing; +- `TimeoutExpired` byte and text payloads use the same redaction path; +- service log tails are redacted before publication; +- command arguments following sensitive options such as `--token`, `--password`, or `--api-key` are replaced; +- sensitive `KEY=value` command arguments are replaced while preserving the key; +- standalone provider-token shapes are removed; +- shell command strings are parsed without execution and reconstructed from redacted arguments; and +- JSON result markers redact commands and evidence notes before serialization. + +The original argument vectors and output are used only inside the isolated execution boundary. Redaction changes neither the command that runs nor its exit status. It is applied at every publication sink instead of depending on each child process to avoid printing credentials. ## Threat model -A tool, test, package manager, browser runner, or application process can print credentials obtained from an explicitly allowed environment variable, configuration file, exception, dependency-manager diagnostic, or echoed request. CI logs are durable review evidence and can have a broader readership than the secret itself. Redaction therefore occurs at the final central output sink rather than relying on every child process to behave correctly. +Repository verification commands and web end-to-end services can emit credentials through exception messages, dependency-manager diagnostics, HTTP-client traces, command-line options, environment-derived configuration, startup logs, and timeout payloads. An explicitly allowlisted environment variable can therefore remain correctly scoped to a child process and still be disclosed when that child echoes it. + +GitHub Actions logs and review envelopes are durable evidence with a potentially broader readership than the originating credential. MITRE classifies insertion of sensitive information into log files as CWE-532. OWASP's current logging guidance identifies access tokens, passwords, database connection strings, encryption keys, and other primary secrets as values that should normally be removed, masked, sanitized, hashed, or encrypted before logging. NIST SSDF requires protection of software and development artifacts from unauthorized access and disclosure. + +## Security boundaries + +- No provider-shaped credential literal is committed as a test fixture. Tests construct credential-shaped values from fragments at runtime so Secret Scan remains authoritative. +- Redaction is fail-closed for recognized sensitive option names, assignments, bearer/basic values, JWTs, and known provider token formats, but it is not a general data-loss-prevention engine. +- Sensitive option detection uses explicit credential terms. Ambiguous short flags such as `-p` are not guessed because they can mean port, path, project, or password depending on the child tool. +- Shell strings are tokenized with `shlex.split`; no shell is invoked for redaction. Malformed strings fall back to line-oriented redaction. +- `subprocess.run` and `subprocess.Popen` receive structured argument arrays with `shell=False`. Preventing shell interpretation and preventing log disclosure are independent controls. +- File paths, working directories, and sandbox paths remain visible operational evidence. Operators must not place credentials in path names. +- Redaction preserves line boundaries and ordinary non-sensitive diagnostics. It does not transform a failed command into a successful result or suppress a nonzero exit status. -MITRE classifies writing sensitive information to a log as CWE-532 and recommends not writing secrets to log files. OWASP likewise identifies access tokens, passwords, connection strings, encryption keys, and other primary secrets as data that should be removed, masked, sanitized, hashed, or encrypted before logging. The Python subprocess API does not implicitly select a system shell when `shell=False`; structured argument execution is retained independently of the log-redaction control. +No formal OWASP, NIST, or CWE conformity is claimed. ## Verification contract -Permanent regression tests must prove that: +The focused regression suite constructs a credential-shaped token at runtime and proves that it does not appear in: -1. text and byte timeout payloads redact a PAT-shaped fixture; -2. normal sandbox verification stdout and stderr redact the same fixture; -3. normal sandboxed web E2E stdout and stderr redact the same fixture; -4. the raw fixture never appears in captured output; -5. ordinary non-sensitive text and process exit codes remain visible; -6. the test fixture is assembled from separate fragments so repository secret scanning does not mistake it for a live credential. +1. completed verification stdout or stderr; +2. timeout output supplied as bytes or text; +3. human-readable command displays; +4. JSON result-marker command arrays; +5. backend, frontend, or E2E shell-command fields; +6. reviewer evidence notes; or +7. service log tails. -Exact-head repository tests, statement/branch coverage, docstring checks, Secret Scan, Semgrep, CodeQL, Security Scan, OpenCode, Noema, and branch protection remain authoritative. +The tests also cover separate sensitive options, `--option=value`, `KEY=value` assignments, standalone provider-token shapes, malformed shell quoting, missing logs, bounded final-line selection, and both wrappers' end-to-end publication paths. Ordinary text, line endings, result envelopes, cleanup, timeouts, and child-process exit codes remain observable. + +The exact pull-request head must additionally pass the complete central unit suite, 100% production statement and branch coverage for the changed surface, production docstring checks, Secret Scan, CodeQL, Semgrep, Python Security, Security Scan, OpenCode, Noema, CodeRabbit, independent current-head approval, and branch protection before merge. ## Modular boundary -Both wrappers remain independently executable scripts and reusable Python modules. They depend only on the central redaction utility and standard-library process primitives, so product repositories can consume the same behavior through the organization workflow without copying a repository-local implementation. +`sandboxed_verify.py`, `sandboxed_web_e2e.py`, and `redact_sensitive_log.py` remain independently executable scripts and reusable Python modules. Product repositories consume the behavior through the organization control plane without copying repository-local redaction code. The wrappers preserve their existing CLI and machine-readable result contracts. + +## Rollback + +Rollback must restore every publication sink as one atomic change. Removing only command redaction, service-tail redaction, or result-envelope redaction would recreate a bypass around the remaining controls. Before rollback, operators must prove that no allowlisted credential can reach child output or command metadata and must retain equivalent focused regression evidence. + +## APA 7 references + +MITRE Corporation. (2026). *CWE-117: Improper output neutralization for logs* (CWE Version 4.20). https://cwe.mitre.org/data/definitions/117.html -## References +MITRE Corporation. (2026). *CWE-532: Insertion of sensitive information into log file* (CWE Version 4.20). https://cwe.mitre.org/data/definitions/532.html -MITRE. (2026, April 30). *CWE-532: Insertion of sensitive information into log file*. Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/532.html +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 OWASP Foundation. (n.d.). *Logging cheat sheet*. OWASP Cheat Sheet Series. Retrieved August 5, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html From f5380536f93f9deb0e04b74452c114a05cd44151 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:10:33 +0900 Subject: [PATCH 08/24] test(ci): reject JSON-value credential disclosure --- tests/test_sandboxed_output_redaction.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_sandboxed_output_redaction.py b/tests/test_sandboxed_output_redaction.py index 38611c217..bab3956e7 100644 --- a/tests/test_sandboxed_output_redaction.py +++ b/tests/test_sandboxed_output_redaction.py @@ -11,6 +11,7 @@ REDACTED, redact_command_arguments, redact_shell_command, + redact_text, ) @@ -19,6 +20,26 @@ def _provider_token() -> str: return "gh" + "p_" + ("A" * 36) +def test_json_string_values_are_redacted_recursively() -> None: + """Valid JSON cannot bypass token redaction through non-sensitive value keys.""" + token = _provider_token() + raw = ( + '{"message":"provider ' + + token + + '","nested":{"notes":["Bearer ' + + token + + '","safe"]}}\n' + ) + + redacted = redact_text(raw) + + assert token not in redacted + assert redacted == ( + '{"message":"provider [REDACTED]",' + '"nested":{"notes":["Bearer [REDACTED]","safe"]}}\n' + ) + + def test_redact_command_arguments_covers_separate_equals_and_direct_tokens() -> None: """Redact option values, assignments, and provider-shaped standalone values.""" token = _provider_token() From 4d613048f20d75596066f4ee6a7c511acb111670 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:11:15 +0900 Subject: [PATCH 09/24] fix(ci): redact credential-shaped JSON values recursively --- scripts/ci/redact_sensitive_log.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/ci/redact_sensitive_log.py b/scripts/ci/redact_sensitive_log.py index 0b72e6f07..3e8da947c 100644 --- a/scripts/ci/redact_sensitive_log.py +++ b/scripts/ci/redact_sensitive_log.py @@ -40,7 +40,7 @@ def _redact_json(value: Any) -> Any: - """Recursively replace values whose JSON keys identify credentials.""" + """Recursively redact sensitive keys and credential-shaped JSON strings.""" if isinstance(value, dict): return { key: REDACTED if SENSITIVE_KEY_RE.search(str(key)) else _redact_json(item) @@ -48,6 +48,8 @@ def _redact_json(value: Any) -> Any: } if isinstance(value, list): return [_redact_json(item) for item in value] + if isinstance(value, str): + return _redact_unstructured(value) return value From 03c9c944ab3bb1a30ff8442614f676002e2d853b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:14:30 +0900 Subject: [PATCH 10/24] test(ci): reject credential-shaped JSON object keys --- tests/test_redact_json_key_boundary.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/test_redact_json_key_boundary.py diff --git a/tests/test_redact_json_key_boundary.py b/tests/test_redact_json_key_boundary.py new file mode 100644 index 000000000..6117502e4 --- /dev/null +++ b/tests/test_redact_json_key_boundary.py @@ -0,0 +1,21 @@ +"""Regression evidence for credential-shaped JSON object keys.""" + +from __future__ import annotations + +import re + +from scripts.ci import redact_sensitive_log as redactor + + +def test_json_object_keys_use_the_unstructured_redaction_boundary(monkeypatch) -> None: + """A JSON key matching a credential format must never bypass redaction.""" + + monkeypatch.setattr( + redactor, + "PROVIDER_TOKEN_RES", + (re.compile(r"\bcredential_key_marker\b"),), + ) + + assert redactor.redact_text('{"credential_key_marker":"safe"}\n') == ( + '{"[REDACTED]":"safe"}\n' + ) From 0fab42577bd403400c19a7d372aed643ff95d45a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:15:03 +0900 Subject: [PATCH 11/24] fix(ci): redact credential-shaped JSON object keys --- scripts/ci/redact_sensitive_log.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/ci/redact_sensitive_log.py b/scripts/ci/redact_sensitive_log.py index 3e8da947c..84b810d1f 100644 --- a/scripts/ci/redact_sensitive_log.py +++ b/scripts/ci/redact_sensitive_log.py @@ -43,7 +43,9 @@ def _redact_json(value: Any) -> Any: """Recursively redact sensitive keys and credential-shaped JSON strings.""" if isinstance(value, dict): return { - key: REDACTED if SENSITIVE_KEY_RE.search(str(key)) else _redact_json(item) + _redact_unstructured(str(key)): ( + REDACTED if SENSITIVE_KEY_RE.search(str(key)) else _redact_json(item) + ) for key, item in value.items() } if isinstance(value, list): From 87a674391f9208bd4081692a1cfa4b2fec59d6ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:15:51 +0900 Subject: [PATCH 12/24] test(ci): bound assignment redaction scanning work --- tests/test_redact_json_key_boundary.py | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/test_redact_json_key_boundary.py b/tests/test_redact_json_key_boundary.py index 6117502e4..e4c8621a4 100644 --- a/tests/test_redact_json_key_boundary.py +++ b/tests/test_redact_json_key_boundary.py @@ -1,4 +1,4 @@ -"""Regression evidence for credential-shaped JSON object keys.""" +"""Regression evidence for credential-shaped JSON keys and bounded scanning.""" from __future__ import annotations @@ -19,3 +19,28 @@ def test_json_object_keys_use_the_unstructured_redaction_boundary(monkeypatch) - assert redactor.redact_text('{"credential_key_marker":"safe"}\n') == ( '{"[REDACTED]":"safe"}\n' ) + + +def test_assignment_scan_does_not_rescan_one_long_ordinary_identifier( + monkeypatch, +) -> None: + """A long non-sensitive token must be inspected once rather than quadratically.""" + + class CountingSensitivePattern: + """Count the total candidate characters inspected by key classification.""" + + def __init__(self) -> None: + self.inspected_characters = 0 + + def search(self, value: str): + """Record one candidate and report that it is not a sensitive key.""" + + self.inspected_characters += len(value) + return None + + counting_pattern = CountingSensitivePattern() + monkeypatch.setattr(redactor, "SENSITIVE_KEY_RE", counting_pattern) + ordinary_identifier = "ordinary_identifier_" * 512 + + assert redactor.redact_text(ordinary_identifier) == ordinary_identifier + assert counting_pattern.inspected_characters <= len(ordinary_identifier) From 5cf890bb52827638df208960fd34537a2b8ab852 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:16:51 +0900 Subject: [PATCH 13/24] perf(ci): make assignment redaction a bounded forward scan --- scripts/ci/redact_sensitive_log.py | 34 ++++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/scripts/ci/redact_sensitive_log.py b/scripts/ci/redact_sensitive_log.py index 84b810d1f..129810220 100644 --- a/scripts/ci/redact_sensitive_log.py +++ b/scripts/ci/redact_sensitive_log.py @@ -55,8 +55,11 @@ def _redact_json(value: Any) -> Any: return value -def _consume_sensitive_assignment(text: str, start: int) -> tuple[str, int] | None: - """Return a redacted key/value assignment parsed in linear time.""" +def _consume_sensitive_assignment( + text: str, + start: int, +) -> tuple[str | None, int]: + """Parse one possible assignment and return replacement plus next cursor.""" cursor = start key_quote = "" if cursor < len(text) and text[cursor] in "\"'": @@ -64,25 +67,25 @@ def _consume_sensitive_assignment(text: str, start: int) -> tuple[str, int] | No cursor += 1 key_start = cursor if cursor >= len(text) or text[cursor] not in KEY_CHARS or text[cursor].isdigit(): - return None + return None, start + 1 while cursor < len(text) and text[cursor] in KEY_CHARS: cursor += 1 key = text[key_start:cursor] if key_quote: if cursor >= len(text) or text[cursor] != key_quote: - return None + return None, start + 1 cursor += 1 if not SENSITIVE_KEY_RE.search(key): - return None + return None, cursor while cursor < len(text) and text[cursor].isspace(): cursor += 1 if cursor >= len(text) or text[cursor] not in ":=": - return None + return None, cursor cursor += 1 while cursor < len(text) and text[cursor].isspace(): cursor += 1 if cursor >= len(text): - return None + return None, cursor value_start = cursor if text[cursor] in "\"'": @@ -102,22 +105,21 @@ def _consume_sensitive_assignment(text: str, start: int) -> tuple[str, int] | No while cursor < len(text) and not text[cursor].isspace() and text[cursor] not in ",}": cursor += 1 if cursor == value_start: - return None + return None, cursor return text[start:value_start] + REDACTED, cursor def _redact_assignments(text: str) -> str: - """Redact sensitive key/value assignments without backtracking regexes.""" + """Redact sensitive key/value assignments in one bounded forward scan.""" output: list[str] = [] cursor = 0 while cursor < len(text): - match = _consume_sensitive_assignment(text, cursor) - if match is None: - output.append(text[cursor]) - cursor += 1 - continue - replacement, cursor = match - output.append(replacement) + replacement, next_cursor = _consume_sensitive_assignment(text, cursor) + if replacement is None: + output.append(text[cursor:next_cursor]) + else: + output.append(replacement) + cursor = next_cursor return "".join(output) From 02d50bbf03a961d361c77d1f00b41eeb52c1b221 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:18:38 +0900 Subject: [PATCH 14/24] docs(ci): record recursive and bounded log redaction --- .../doctoring/sandboxed-command-log-redaction.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/doctoring/sandboxed-command-log-redaction.md b/docs/doctoring/sandboxed-command-log-redaction.md index 652716e4f..517ae519c 100644 --- a/docs/doctoring/sandboxed-command-log-redaction.md +++ b/docs/doctoring/sandboxed-command-log-redaction.md @@ -12,6 +12,7 @@ One trusted redaction module owns this publication boundary: - command arguments following sensitive options such as `--token`, `--password`, or `--api-key` are replaced; - sensitive `KEY=value` command arguments are replaced while preserving the key; - standalone provider-token shapes are removed; +- valid JSON is traversed recursively so credential-shaped object keys and string values cannot bypass line-oriented patterns; - shell command strings are parsed without execution and reconstructed from redacted arguments; and - JSON result markers redact commands and evidence notes before serialization. @@ -19,17 +20,18 @@ The original argument vectors and output are used only inside the isolated execu ## Threat model -Repository verification commands and web end-to-end services can emit credentials through exception messages, dependency-manager diagnostics, HTTP-client traces, command-line options, environment-derived configuration, startup logs, and timeout payloads. An explicitly allowlisted environment variable can therefore remain correctly scoped to a child process and still be disclosed when that child echoes it. +Repository verification commands and web end-to-end services can emit credentials through exception messages, dependency-manager diagnostics, HTTP-client traces, command-line options, environment-derived configuration, startup logs, structured JSON diagnostics, and timeout payloads. An explicitly allowlisted environment variable can therefore remain correctly scoped to a child process and still be disclosed when that child echoes it. GitHub Actions logs and review envelopes are durable evidence with a potentially broader readership than the originating credential. MITRE classifies insertion of sensitive information into log files as CWE-532. OWASP's current logging guidance identifies access tokens, passwords, database connection strings, encryption keys, and other primary secrets as values that should normally be removed, masked, sanitized, hashed, or encrypted before logging. NIST SSDF requires protection of software and development artifacts from unauthorized access and disclosure. -## Security boundaries +## Security and availability boundaries - No provider-shaped credential literal is committed as a test fixture. Tests construct credential-shaped values from fragments at runtime so Secret Scan remains authoritative. - Redaction is fail-closed for recognized sensitive option names, assignments, bearer/basic values, JWTs, and known provider token formats, but it is not a general data-loss-prevention engine. - Sensitive option detection uses explicit credential terms. Ambiguous short flags such as `-p` are not guessed because they can mean port, path, project, or password depending on the child tool. - Shell strings are tokenized with `shlex.split`; no shell is invoked for redaction. Malformed strings fall back to line-oriented redaction. - `subprocess.run` and `subprocess.Popen` receive structured argument arrays with `shell=False`. Preventing shell interpretation and preventing log disclosure are independent controls. +- The assignment scanner advances through each ordinary identifier once. A deterministic instrumentation test prevents a long non-sensitive token from reintroducing quadratic rescanning and log-processing denial of service. - File paths, working directories, and sandbox paths remain visible operational evidence. Operators must not place credentials in path names. - Redaction preserves line boundaries and ordinary non-sensitive diagnostics. It does not transform a failed command into a successful result or suppress a nonzero exit status. @@ -44,10 +46,12 @@ The focused regression suite constructs a credential-shaped token at runtime and 3. human-readable command displays; 4. JSON result-marker command arrays; 5. backend, frontend, or E2E shell-command fields; -6. reviewer evidence notes; or -7. service log tails. +6. reviewer evidence notes; +7. service log tails; +8. nested JSON string values; or +9. JSON object keys. -The tests also cover separate sensitive options, `--option=value`, `KEY=value` assignments, standalone provider-token shapes, malformed shell quoting, missing logs, bounded final-line selection, and both wrappers' end-to-end publication paths. Ordinary text, line endings, result envelopes, cleanup, timeouts, and child-process exit codes remain observable. +The tests also cover separate sensitive options, `--option=value`, `KEY=value` assignments, standalone provider-token shapes, malformed shell quoting, missing logs, bounded final-line selection, recursive JSON structures, bounded assignment scanning, and both wrappers' end-to-end publication paths. Ordinary text, line endings, result envelopes, cleanup, timeouts, and child-process exit codes remain observable. The exact pull-request head must additionally pass the complete central unit suite, 100% production statement and branch coverage for the changed surface, production docstring checks, Secret Scan, CodeQL, Semgrep, Python Security, Security Scan, OpenCode, Noema, CodeRabbit, independent current-head approval, and branch protection before merge. @@ -57,7 +61,7 @@ The exact pull-request head must additionally pass the complete central unit sui ## Rollback -Rollback must restore every publication sink as one atomic change. Removing only command redaction, service-tail redaction, or result-envelope redaction would recreate a bypass around the remaining controls. Before rollback, operators must prove that no allowlisted credential can reach child output or command metadata and must retain equivalent focused regression evidence. +Rollback must restore every publication sink as one atomic change. Removing only command redaction, JSON traversal, service-tail redaction, or result-envelope redaction would recreate a bypass around the remaining controls. Before rollback, operators must prove that no allowlisted credential can reach child output or command metadata and must retain equivalent focused regression evidence. ## APA 7 references From 9f92c3d19cd33a1ae8175f541347bba410da31d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:21:07 +0900 Subject: [PATCH 15/24] build(security): align Strix dependency snapshots --- requirements-strix-ci-hashes.txt | 341 ++++++++++++++++--------------- requirements-strix-ci.txt | 3 +- 2 files changed, 173 insertions(+), 171 deletions(-) diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index e2c8f00eb..c305e9c84 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -4,127 +4,128 @@ aiohappyeyeballs==2.7.1 \ --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.14.1 \ - --hash=sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5 \ - --hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \ - --hash=sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521 \ - --hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \ - --hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \ - --hash=sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a \ - --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ - --hash=sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a \ - --hash=sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f \ - --hash=sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee \ - --hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \ - --hash=sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb \ - --hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \ - --hash=sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05 \ - --hash=sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8 \ - --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ - --hash=sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2 \ - --hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \ - --hash=sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf \ - --hash=sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271 \ - --hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \ - --hash=sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847 \ - --hash=sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264 \ - --hash=sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf \ - --hash=sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6 \ - --hash=sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df \ - --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ - --hash=sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126 \ - --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ - --hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \ - --hash=sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4 \ - --hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \ - --hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \ - --hash=sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c \ - --hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \ - --hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \ - --hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \ - --hash=sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b \ - --hash=sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621 \ - --hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \ - --hash=sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da \ - --hash=sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491 \ - --hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \ - --hash=sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d \ - --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ - --hash=sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42 \ - --hash=sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c \ - --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ - --hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \ - --hash=sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8 \ - --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ - --hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \ - --hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \ - --hash=sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2 \ - --hash=sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966 \ - --hash=sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192 \ - --hash=sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95 \ - --hash=sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3 \ - --hash=sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b \ - --hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \ - --hash=sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6 \ - --hash=sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573 \ - --hash=sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af \ - --hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \ - --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ - --hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \ - --hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \ - --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ - --hash=sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817 \ - --hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \ - --hash=sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e \ - --hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \ - --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ - --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ - --hash=sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe \ - --hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \ - --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ - --hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \ - --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ - --hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \ - --hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \ - --hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \ - --hash=sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2 \ - --hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \ - --hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \ - --hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \ - --hash=sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3 \ - --hash=sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178 \ - --hash=sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3 \ - --hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \ - --hash=sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730 \ - --hash=sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842 \ - --hash=sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd \ - --hash=sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d \ - --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ - --hash=sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85 \ - --hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \ - --hash=sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199 \ - --hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \ - --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ - --hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \ - --hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \ - --hash=sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480 \ - --hash=sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04 \ - --hash=sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8 \ - --hash=sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce \ - --hash=sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087 \ - --hash=sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505 \ - --hash=sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780 \ - --hash=sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4 \ - --hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \ - --hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \ - --hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \ - --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ - --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ - --hash=sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a \ - --hash=sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7 \ - --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 \ - --hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 # via + # -r requirements-strix-ci.txt # gql # litellm aiosignal==1.4.0 \ @@ -401,53 +402,53 @@ click==8.4.1 \ # litellm # typer # uvicorn -cryptography==49.0.0 \ - --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ - --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ - --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ - --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ - --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ - --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ - --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ - --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ - --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ - --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ - --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ - --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ - --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ - --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ - --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ - --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ - --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ - --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ - --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ - --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ - --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ - --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ - --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ - --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ - --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ - --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ - --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ - --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ - --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ - --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ - --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ - --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ - --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ - --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ - --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ - --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ - --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ - --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ - --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ - --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ - --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ - --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ - --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ - --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ - --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ - --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 # via # -r requirements-strix-ci.txt # google-auth @@ -1680,9 +1681,9 @@ pyjwt==2.13.0 \ --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via mcp -pyopenssl==26.3.0 \ - --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ - --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 +pyopenssl==26.4.0 \ + --hash=sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7 \ + --hash=sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c # via google-auth python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index e32bd39a9..98e5c33e2 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -1,6 +1,7 @@ strix-agent==1.0.4 +aiohttp==3.14.3 google-cloud-aiplatform==1.133.0 protobuf<7.0.0 -cryptography==49.0.0 +cryptography==50.0.0 python-multipart==0.0.32 pyasn1==0.6.4 From 36f358db51e69661d379f655f58ba54cdb84b7fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:21:29 +0900 Subject: [PATCH 16/24] docs(ci): record complete sandbox evidence redaction --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..2480f65be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to the ContextualWisdomLab central GitHub control plane are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versioned releases follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Security + +- Upgrade the central Strix dependency snapshots to `aiohttp==3.14.3`, `cryptography==50.0.0`, and the compatible `pyOpenSSL==26.4.0` closure so the hard dependency gates contain no known affected releases. +- Redact credentials from every sandbox evidence publication sink, including completed and timed-out process output, service log tails, commands, reviewer notes, nested JSON values, and JSON object keys. + +### Fixed + +- Replace quadratic sensitive-assignment rescanning with a bounded forward scan so one long ordinary diagnostic token cannot cause disproportionate log-processing work. + +### Documentation + +- Add an APA 7 doctoring record for the sandbox command/output redaction boundary, structured diagnostics, availability controls, verification evidence, limitations, and rollback requirements. From eb07d6b1359f70c8bc4ef74d6986fa02d1efe448 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:23:01 +0900 Subject: [PATCH 17/24] test(ci): isolate provider-pattern JSON key coverage --- tests/test_redact_json_key_boundary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_redact_json_key_boundary.py b/tests/test_redact_json_key_boundary.py index e4c8621a4..27798bcbe 100644 --- a/tests/test_redact_json_key_boundary.py +++ b/tests/test_redact_json_key_boundary.py @@ -13,10 +13,10 @@ def test_json_object_keys_use_the_unstructured_redaction_boundary(monkeypatch) - monkeypatch.setattr( redactor, "PROVIDER_TOKEN_RES", - (re.compile(r"\bcredential_key_marker\b"),), + (re.compile(r"\bprovider_marker_value\b"),), ) - assert redactor.redact_text('{"credential_key_marker":"safe"}\n') == ( + assert redactor.redact_text('{"provider_marker_value":"safe"}\n') == ( '{"[REDACTED]":"safe"}\n' ) From ea38588c5455fcfed2f32e9132aeeac2854d7812 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:23:46 +0900 Subject: [PATCH 18/24] test(ci): complete central redaction behavior coverage --- tests/test_redact_sensitive_log_contract.py | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_redact_sensitive_log_contract.py diff --git a/tests/test_redact_sensitive_log_contract.py b/tests/test_redact_sensitive_log_contract.py new file mode 100644 index 000000000..dc8498fac --- /dev/null +++ b/tests/test_redact_sensitive_log_contract.py @@ -0,0 +1,74 @@ +"""Complete behavioral contracts for the central log-redaction primitive.""" + +from __future__ import annotations + +import io +import sys + +from scripts.ci import redact_sensitive_log as redactor + + +def _synthetic_provider_token() -> str: + """Construct a provider-shaped value without storing a scanner literal.""" + + return "gh" + "p_" + ("B" * 36) + + +def test_json_sensitive_keys_and_scalar_values_preserve_safe_evidence() -> None: + """Sensitive JSON values are replaced while ordinary scalars remain intact.""" + + assert redactor.redact_text( + '{"api_key":"value","count":3,"enabled":true,"empty":null}\n' + ) == ( + '{"api_key":"[REDACTED]","count":3,"enabled":true,"empty":null}\n' + ) + + +def test_assignment_parser_covers_quoted_keys_values_and_incomplete_forms() -> None: + """Quoted assignments redact values and malformed empty forms make progress.""" + + assert redactor.redact_text("'token' = 'quoted value'") == ( + "'token' = [REDACTED]" + ) + assert redactor.redact_text("token='escaped\\' value'") == ( + "token=[REDACTED]" + ) + assert redactor.redact_text("'token=value") == ( + "'token=[REDACTED]" + ) + assert redactor.redact_text("token") == "token" + assert redactor.redact_text("token=") == "token=" + assert redactor.redact_text("token=,") == "token=," + + +def test_unstructured_patterns_cover_basic_jwt_and_provider_values() -> None: + """Independent credential formats share one non-JSON redaction boundary.""" + + provider_value = _synthetic_provider_token() + assert redactor.redact_text("Authorization: Basic opaque-value") == ( + "Authorization: [REDACTED] [REDACTED]" + ) + assert redactor.redact_text("header.payload.signature") == redactor.REDACTED + assert redactor.redact_text(provider_value) == redactor.REDACTED + + +def test_command_argument_redaction_preserves_non_sensitive_options() -> None: + """Only explicit sensitive option values and credential shapes are replaced.""" + + assert redactor.redact_command_arguments( + ["tool", "--mode=safe", "--token"] + ) == ["tool", "--mode=safe", "--token"] + assert redactor.redact_shell_command("") == "" + + +def test_empty_text_and_cli_main_preserve_stream_contract(monkeypatch) -> None: + """Empty input is stable and the CLI writes only the redacted stream.""" + + assert redactor.redact_text("") == "" + input_stream = io.StringIO("password=value\nordinary\n") + output_stream = io.StringIO() + monkeypatch.setattr(sys, "stdin", input_stream) + monkeypatch.setattr(sys, "stdout", output_stream) + + assert redactor.main() == 0 + assert output_stream.getvalue() == "password=[REDACTED]\nordinary\n" From 45dc74f6ce19e0ff2bdea21df00948a43ee10baf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:24:40 +0900 Subject: [PATCH 19/24] test(ci): require complete Authorization header redaction --- tests/test_redact_sensitive_log_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_redact_sensitive_log_contract.py b/tests/test_redact_sensitive_log_contract.py index dc8498fac..350ce2044 100644 --- a/tests/test_redact_sensitive_log_contract.py +++ b/tests/test_redact_sensitive_log_contract.py @@ -46,7 +46,7 @@ def test_unstructured_patterns_cover_basic_jwt_and_provider_values() -> None: provider_value = _synthetic_provider_token() assert redactor.redact_text("Authorization: Basic opaque-value") == ( - "Authorization: [REDACTED] [REDACTED]" + "Authorization: [REDACTED]" ) assert redactor.redact_text("header.payload.signature") == redactor.REDACTED assert redactor.redact_text(provider_value) == redactor.REDACTED From c83271445822ffe951b9096f4cc70f78e3e41181 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:25:09 +0900 Subject: [PATCH 20/24] test(ci): preserve standalone auth scheme evidence safely --- tests/test_redact_sensitive_log_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_redact_sensitive_log_contract.py b/tests/test_redact_sensitive_log_contract.py index 350ce2044..b1352d892 100644 --- a/tests/test_redact_sensitive_log_contract.py +++ b/tests/test_redact_sensitive_log_contract.py @@ -48,6 +48,7 @@ def test_unstructured_patterns_cover_basic_jwt_and_provider_values() -> None: assert redactor.redact_text("Authorization: Basic opaque-value") == ( "Authorization: [REDACTED]" ) + assert redactor.redact_text("Basic opaque-value") == "Basic [REDACTED]" assert redactor.redact_text("header.payload.signature") == redactor.REDACTED assert redactor.redact_text(provider_value) == redactor.REDACTED From 13323b14e97396ce2b8d536a85917970a864378e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:25:47 +0900 Subject: [PATCH 21/24] fix(ci): consume complete Authorization header values --- scripts/ci/redact_sensitive_log.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/ci/redact_sensitive_log.py b/scripts/ci/redact_sensitive_log.py index 129810220..a4fc57fc1 100644 --- a/scripts/ci/redact_sensitive_log.py +++ b/scripts/ci/redact_sensitive_log.py @@ -88,6 +88,10 @@ def _consume_sensitive_assignment( return None, cursor value_start = cursor + if key.casefold() == "authorization": + while cursor < len(text) and text[cursor] not in ",}": + cursor += 1 + return text[start:value_start] + REDACTED, cursor if text[cursor] in "\"'": value_quote = text[cursor] cursor += 1 From cb533a8e72ce67e5295e18e7f0972859780908a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:26:45 +0900 Subject: [PATCH 22/24] test(ci): cover complete redactor branch and entry contracts --- tests/test_redact_sensitive_log_contract.py | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_redact_sensitive_log_contract.py b/tests/test_redact_sensitive_log_contract.py index b1352d892..11a1b34de 100644 --- a/tests/test_redact_sensitive_log_contract.py +++ b/tests/test_redact_sensitive_log_contract.py @@ -3,7 +3,11 @@ from __future__ import annotations import io +import runpy import sys +from pathlib import Path + +import pytest from scripts.ci import redact_sensitive_log as redactor @@ -33,12 +37,22 @@ def test_assignment_parser_covers_quoted_keys_values_and_incomplete_forms() -> N assert redactor.redact_text("token='escaped\\' value'") == ( "token=[REDACTED]" ) + assert redactor.redact_text("token='unterminated") == ( + "token=[REDACTED]" + ) assert redactor.redact_text("'token=value") == ( "'token=[REDACTED]" ) assert redactor.redact_text("token") == "token" + assert redactor.redact_text("token text") == "token text" assert redactor.redact_text("token=") == "token=" assert redactor.redact_text("token=,") == "token=," + assert redactor.redact_text("token=value ordinary") == ( + "token=[REDACTED] ordinary" + ) + assert redactor.redact_text("1token=value") == ( + "1token=[REDACTED]" + ) def test_unstructured_patterns_cover_basic_jwt_and_provider_values() -> None: @@ -73,3 +87,18 @@ def test_empty_text_and_cli_main_preserve_stream_contract(monkeypatch) -> None: assert redactor.main() == 0 assert output_stream.getvalue() == "password=[REDACTED]\nordinary\n" + + +def test_module_entry_point_exits_after_redacting_standard_input(monkeypatch) -> None: + """Direct script execution preserves the same redacted stream contract.""" + + input_stream = io.StringIO("secret=value\n") + output_stream = io.StringIO() + monkeypatch.setattr(sys, "stdin", input_stream) + monkeypatch.setattr(sys, "stdout", output_stream) + + with pytest.raises(SystemExit) as raised: + runpy.run_path(str(Path(redactor.__file__).resolve()), run_name="__main__") + + assert raised.value.code == 0 + assert output_stream.getvalue() == "secret=[REDACTED]\n" From 17d7d46d4669a8f4b6fd6dd95cb5f40e7a7880da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:32:23 +0900 Subject: [PATCH 23/24] test(redaction): cover echoed separate secret options --- tests/test_redact_sensitive_log_contract.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_redact_sensitive_log_contract.py b/tests/test_redact_sensitive_log_contract.py index 11a1b34de..c279987a3 100644 --- a/tests/test_redact_sensitive_log_contract.py +++ b/tests/test_redact_sensitive_log_contract.py @@ -67,6 +67,14 @@ def test_unstructured_patterns_cover_basic_jwt_and_provider_values() -> None: assert redactor.redact_text(provider_value) == redactor.REDACTED +def test_unstructured_output_redacts_separate_sensitive_option_values() -> None: + """A child that echoes a separate secret option must not disclose its value.""" + + assert redactor.redact_text( + "running tool --api-key ordinary-value --mode safe" + ) == "running tool --api-key [REDACTED] --mode safe" + + def test_command_argument_redaction_preserves_non_sensitive_options() -> None: """Only explicit sensitive option values and credential shapes are replaced.""" From d4065bf322279fae97a3de85518116270d65e3ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 10:42:55 +0900 Subject: [PATCH 24/24] fix(redaction): scrub echoed separate secret options --- scripts/ci/redact_sensitive_log.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/ci/redact_sensitive_log.py b/scripts/ci/redact_sensitive_log.py index a4fc57fc1..624273f34 100644 --- a/scripts/ci/redact_sensitive_log.py +++ b/scripts/ci/redact_sensitive_log.py @@ -17,9 +17,14 @@ r"api[_-]?key|private[_-]?key|access[_-]?key|session[_-]?key)", re.IGNORECASE, ) -SENSITIVE_OPTION_RE = re.compile( +SENSITIVE_OPTION_PATTERN = ( r"(?:token|secret|password|passwd|credential|authorization|jwt|" - r"api[-_]?key|private[-_]?key|access[-_]?key|session[-_]?key)", + r"api[-_]?key|private[-_]?key|access[-_]?key|session[-_]?key)" +) +SENSITIVE_OPTION_RE = re.compile(SENSITIVE_OPTION_PATTERN, re.IGNORECASE) +SEPARATE_SENSITIVE_OPTION_RE = re.compile( + rf"(?P(?(?!--)(?:\"[^\"]*\"|'[^']*'|[^\s,}]+))", re.IGNORECASE, ) JWT_RE = re.compile( @@ -130,6 +135,10 @@ def _redact_assignments(text: str) -> str: def _redact_unstructured(text: str) -> str: """Redact credential-shaped values from non-JSON diagnostic text.""" cleaned = _redact_assignments(text) + cleaned = SEPARATE_SENSITIVE_OPTION_RE.sub( + lambda match: f"{match.group('prefix')}{REDACTED}", + cleaned, + ) cleaned = BEARER_RE.sub(lambda match: f"{match.group('prefix')}{REDACTED}", cleaned) cleaned = JWT_RE.sub(REDACTED, cleaned) for pattern in PROVIDER_TOKEN_RES: