주기 회기 진단과 로그인 폭 보강

This commit is contained in:
Yun Chan 2026-08-12 16:33:59 +09:00
parent 946661926b
commit f97e7fadac
10 changed files with 216 additions and 17 deletions

View file

@ -103,6 +103,17 @@ CRITICAL_UNTRACKED_SUFFIXES = frozenset(
)
PROJECT_RE = re.compile(r"^vignette-periodic-[0-9a-f]{8}-[a-z0-9-]{6,32}$")
HEX_SHA_RE = re.compile(r"^[0-9a-f]{40}$")
SENSITIVE_ENV_KEY_RE = re.compile(
r"(?:PASSWORD|SECRET|TOKEN|API_KEY)$|^ENGINE_GATEWAY_SHARED_SECRET$"
)
DIAGNOSTIC_TAIL_CHARS = 12_000
EMAIL_RE = re.compile(r"(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b")
UUID_RE = re.compile(
r"(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b"
)
SENSITIVE_QUERY_RE = re.compile(
r"(?i)([?&](?:code|email|token|secret|session|state)=)[^&\s]+"
)
class GateError(RuntimeError):
@ -362,6 +373,27 @@ def _safe_error(exc: BaseException) -> str:
return str(exc).replace(str(REPO_ROOT), "<repo>")[-1600:]
def _sanitize_diagnostic_text(text: str, environ: Mapping[str, str]) -> str:
sanitized = text.replace(str(REPO_ROOT), "<repo>")
sensitive_values = sorted(
{
value
for key, value in environ.items()
if value and SENSITIVE_ENV_KEY_RE.search(key)
},
key=len,
reverse=True,
)
for value in sensitive_values:
sanitized = sanitized.replace(value, "<redacted>")
sanitized = EMAIL_RE.sub("<redacted-email>", sanitized)
sanitized = UUID_RE.sub("<redacted-uuid>", sanitized)
sanitized = SENSITIVE_QUERY_RE.sub(r"\1<redacted>", sanitized)
if any(value in sanitized for value in sensitive_values):
raise GateError("failure diagnostics retained a runtime secret")
return sanitized[-DIAGNOSTIC_TAIL_CHARS:]
def assert_safe_value(label: str, value: str) -> None:
normalized = value.strip().lower().replace("\\", "/")
if any(marker.lower().replace("\\", "/") in normalized for marker in FORBIDDEN_MARKERS):
@ -1056,6 +1088,46 @@ class PeriodicRunner:
proof["errors"].append(_safe_error(exc))
return proof
def _capture_failure_diagnostics(
self, stack_env: Mapping[str, str]
) -> dict[str, Any]:
if not self.state.stack_attempted:
return {"captured": False, "reason": "stack_not_attempted"}
diagnostics: dict[str, Any] = {
"captured": True,
"secret_value_emitted": False,
"commands": {},
}
commands = (
(
"compose_ps",
self._compose_argv("ps", "--format", "json"),
),
(
"compose_logs",
self._compose_argv(
"logs",
"--no-color",
"--timestamps",
"--tail",
"240",
"db",
"api",
),
),
)
for stage, argv in commands:
result = self._run(stage, argv, timeout=90, check=False)
combined = "\n".join(part for part in (result.stdout, result.stderr) if part)
sanitized = _sanitize_diagnostic_text(combined, stack_env)
diagnostics["commands"][stage] = {
"exit_code": result.returncode,
"duration_seconds": round(result.duration_seconds, 3),
"raw_sha256": hashlib.sha256(combined.encode("utf-8")).hexdigest(),
"sanitized_tail": sanitized,
}
return diagnostics
@staticmethod
def _cleanup_green(cleanup: Mapping[str, Any]) -> bool:
return (
@ -1116,6 +1188,7 @@ class PeriodicRunner:
status = "FAILED"
error = ""
cleanup: dict[str, Any] = {}
stack_env: dict[str, str] = {}
try:
self.state.source = self.source_identity(require_clean=True)
self.state.runtime = build_runtime_identity(self.state.source)
@ -1329,6 +1402,16 @@ class PeriodicRunner:
status = "GREEN"
except BaseException as exc:
error = _safe_error(exc)
if self.state.stack_attempted:
try:
self.state.proof["failure_diagnostics"] = (
self._capture_failure_diagnostics(stack_env)
)
except BaseException as diagnostic_exc:
self.state.proof["failure_diagnostics"] = {
"captured": False,
"reason": _safe_error(diagnostic_exc),
}
finally:
cleanup = self._cleanup()
if not self._cleanup_green(cleanup):