Merge branch 'harness/B-implementation' into harness/A-integration

This commit is contained in:
DongHyeonka
2026-09-10 11:07:35 +09:00
61 changed files with 3870 additions and 4 deletions
+24 -4
View File
@@ -9,13 +9,33 @@ from pathlib import Path
ANSI_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
_REDACTION_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
(re.compile(r"(?i)^(\s*authorization\s*:\s*bearer\s+).*$"), r"\1[REDACTED]"),
(re.compile(r"(?i)^(\s*(?:cookie|set-cookie)\s*:\s*).*$"), r"\1[REDACTED]"),
# 줄 맨 앞에 앵커를 두면 `curl -H "Authorization: Bearer ..."` 를 놓친다.
# 터미널 증거에서 Bearer 가 가장 흔히 나오는 자리가 그 명령줄이다.
# 값은 따옴표와 줄바꿈 전까지 먹는다 — 헤더 한 줄이면 줄 끝까지, 인용부호 안이면 닫는
# 따옴표 앞까지다. 따옴표를 넘겨 먹으면 명령의 나머지가 통째로 가려진다
(re.compile(r"(?i)(\b(?:proxy-)?authorization\s*:\s*(?:bearer|basic)\s+)[^\"'\r\n]*"),
r"\1[REDACTED]"),
(re.compile(r"(?i)(\b(?:set-cookie|cookie)\s*:\s*)[^\"'\r\n]*"), r"\1[REDACTED]"),
# `curl -u user:pw` · `--user user:pw`. 사용자 이름은 남긴다
(re.compile(r"(?i)((?:^|\s)(?:-u|--user)[=\s]+)([^\s:\"']+):([^\s\"']+)"),
r"\1\2:[REDACTED]"),
# JWT 자체. `eyJ` 로 시작하는 점 두 개짜리 base64url 은 다른 것과 헷갈리지 않는다
(re.compile(r"\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]+"),
"[REDACTED]"),
# 접속 문자열의 자격증명 — postgresql://app:<암호>@db:5432/app.
# 사용자 이름은 남긴다. 어느 계정으로 붙었는지가 증거의 일부다
(
re.compile(r"(?i)\b([a-z][a-z0-9+.\-]*://)([^:/?#\s@]+):([^@\s/]+)@"),
r"\1\2:[REDACTED]@",
),
# 값의 끝을 **따옴표 앞에서** 막는다. `[^\s,;]+` 로 두면 닫는 따옴표까지 먹어
# `-H "X-Api-Key: [REDACTED] https://...` 가 되고, 증거에 실린 명령이 실제로 돌린
# 명령과 달라진다. 감싼 따옴표가 있으면 그대로 되돌려 놓는다
(
re.compile(
r"(?i)(\b(?:access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|passwd|pwd|client[_-]?secret|api[_-]?key|secret|aws_secret_access_key)\b\s*[=:]\s*)([^\s,;]+)"
r"(?i)(\b(?:access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|passwd|pwd|client[_-]?secret|api[_-]?key|secret|aws_secret_access_key)\b\s*[=:]\s*)([\"']?)([^\s,;\"'\r\n]+)([\"']?)"
),
r"\1[REDACTED]",
r"\1\2[REDACTED]\4",
),
(
re.compile(
@@ -43,6 +43,97 @@ class RenderTerminalTest(unittest.TestCase):
with self.subTest(raw=raw):
self.assertEqual(expected, redact_line(raw))
def test_credentials_inside_a_command_line_are_redacted(self):
"""줄 맨 앞이 아니라 명령 인자 안에 있는 자격증명.
터미널 증거에서 Bearer 가 가장 흔히 나오는 자리가 `curl -H` 의 인자다.
값은 닫는 따옴표 앞까지만 먹는다 — 넘겨 먹으면 명령의 나머지가 통째로 가려진다.
아래 값은 전부 합성이고 실제 비밀값이 아니다.
"""
cases = {
'curl -H "Authorization: Bearer TESTONLY-aaa.bbb.ccc" https://example.test/api':
'curl -H "Authorization: Bearer [REDACTED]" https://example.test/api',
"curl -H 'Authorization: Bearer TESTONLY-xyz' -sS https://example.test":
"curl -H 'Authorization: Bearer [REDACTED]' -sS https://example.test",
'curl -H "Cookie: SESSION=TESTONLY-sess" https://example.test/api':
'curl -H "Cookie: [REDACTED]" https://example.test/api',
"Set-Cookie: SESSION=TESTONLY-x; HttpOnly":
"Set-Cookie: [REDACTED]",
}
for raw, expected in cases.items():
with self.subTest(raw=raw):
self.assertEqual(expected, redact_line(raw))
def test_connection_string_password_is_redacted_and_user_is_kept(self):
"""scheme://user:pw@host 의 암호만 가린다.
어느 계정으로 붙었는지는 증거의 일부라 사용자 이름을 남긴다.
"""
cases = {
"psql postgresql://app:TESTONLY-pw@db:5432/app":
"psql postgresql://app:[REDACTED]@db:5432/app",
"DATABASE_URL=mysql://root:TESTONLY-pw@127.0.0.1:3306/app":
"DATABASE_URL=mysql://root:[REDACTED]@127.0.0.1:3306/app",
"redis://default:TESTONLY-pw@cache:6379/0":
"redis://default:[REDACTED]@cache:6379/0",
}
for raw, expected in cases.items():
with self.subTest(raw=raw):
self.assertEqual(expected, redact_line(raw))
def test_masking_does_not_eat_the_closing_quote(self):
"""가려졌다는 것과 명령이 그대로라는 것은 다르다.
값의 끝을 공백까지로 두면 닫는 따옴표까지 먹어 증거에 실린 명령이 실제로 돌린
명령과 달라진다. 가려진 것만 보고 지나치지 않도록 따옴표 수를 함께 센다.
아래 값은 전부 합성이고 실제 비밀값이 아니다.
"""
lines = (
'curl -H "X-Api-Key: TESTONLY-i-apikey-header" https://example.invalid/d',
'curl -H "X-Token: TESTONLY-x" -H "Accept: application/json" https://example.invalid/d',
'export TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJURVNUT05MWSJ9.TESTONLYsig"',
"curl -H 'X-Api-Key: TESTONLY-single' https://example.invalid/d",
)
for raw in lines:
with self.subTest(raw=raw):
out = redact_line(raw)
self.assertIn("[REDACTED]", out)
self.assertNotIn("TESTONLY", out)
self.assertEqual(raw.count('"'), out.count('"'), out)
self.assertEqual(raw.count("'"), out.count("'"), out)
def test_basic_auth_shapes_are_redacted(self):
"""Bearer 말고도 자격증명이 실리는 자리가 있다."""
cases = {
'curl -H "Authorization: Basic VEVTVE9OTFk6cHc=" https://example.invalid/d':
'curl -H "Authorization: Basic [REDACTED]" https://example.invalid/d',
'curl -H "Proxy-Authorization: Basic VEVTVE9OTFk6cHc=" https://example.invalid/d':
'curl -H "Proxy-Authorization: Basic [REDACTED]" https://example.invalid/d',
"curl -u admin:TESTONLY-basic-pw https://example.invalid/d":
"curl -u admin:[REDACTED] https://example.invalid/d",
"curl --user admin:TESTONLY-basic-pw https://example.invalid/d":
"curl --user admin:[REDACTED] https://example.invalid/d",
}
for raw, expected in cases.items():
with self.subTest(raw=raw):
self.assertEqual(expected, redact_line(raw))
def test_a_bare_jwt_is_redacted(self):
"""`eyJ` 로 시작하는 점 두 개짜리 base64url 은 다른 것과 헷갈리지 않는다."""
raw = "Set token eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJURVNUT05MWSJ9.TESTONLYsig now"
out = redact_line(raw)
self.assertEqual("Set token [REDACTED] now", out)
def test_ordinary_urls_are_not_touched(self):
"""자격증명이 없는 주소는 그대로 둔다. 과하게 가리면 증거를 못 읽는다."""
for line in (
"https://example.test/api?x=1",
"git clone https://github.com/org/repo.git",
"GET https://example.test/studio/documents/abc-123/edit -> 200",
):
with self.subTest(line=line):
self.assertEqual(line, redact_line(line))
def test_normal_output_is_not_changed_by_redaction(self):
line = "GET /api/me -> 200 in 14ms"
self.assertEqual(line, redact_line(line))