fix: sanitize Redmine exception output in CLI
Redmine/requests exceptions may embed the request URL containing the API key (?key=SECRET) or the X-Redmine-API-Key header value. The CLI printed the raw exception text to stderr, leaking the key into terminals and logs. Add _sanitize_error_text() which masks 'key=<value>' query params and the X-Redmine-API-Key header as 'key=***' / 'X-Redmine-API-Key: ***', and apply it to every exception-derived message printed to stderr (fetch errors in main() and SMTP errors in _save_and_maybe_send()). Full traceback with original exception details is now available under both --verbose and --debug (previously only --debug, only for RedmineAPIError). Exit codes and error-handling structure unchanged. Closes #55
This commit is contained in:
@@ -17,6 +17,26 @@ from .mailer import send_report
|
||||
from .report_builder import build_grouped_report, calculate_summary
|
||||
from .yaml_config import ensure_config_dir, resolve_output_path, save_period_to_config
|
||||
|
||||
# Маскируем API-ключ Redmine в тексте ошибок (#55):
|
||||
# - query-параметр key=<значение> в URL (?key=... / &key=...);
|
||||
# - заголовок X-Redmine-API-Key: <значение>.
|
||||
_SANITIZE_PATTERNS = [
|
||||
(re.compile(r"([?&]key=)[^\s&\"')]+", re.IGNORECASE), r"\1***"),
|
||||
(
|
||||
re.compile(
|
||||
r"(X-Redmine-API-Key[\"']?\s*[:=]\s*[\"']?)[^\s\"',}]+", re.IGNORECASE
|
||||
),
|
||||
r"\1***",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _sanitize_error_text(text: str) -> str:
|
||||
"""Маскирует API-ключ Redmine в тексте ошибки перед выводом в stderr (#55)."""
|
||||
for pattern, replacement in _SANITIZE_PATTERNS:
|
||||
text = pattern.sub(replacement, text)
|
||||
return text
|
||||
|
||||
|
||||
def parse_date_range(date_arg: str) -> tuple[str, str]:
|
||||
if "--" not in date_arg:
|
||||
@@ -226,7 +246,7 @@ def _save_and_maybe_send(
|
||||
)
|
||||
print(f"📧 Report sent to {', '.join(email_config.to)}")
|
||||
except RedmineAPIError as e:
|
||||
print(f"❌ {e.message}", file=sys.stderr)
|
||||
print(f"❌ {_sanitize_error_text(e.message)}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
@@ -392,12 +412,14 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
dedup_before=_compute_dedup_cutoff(),
|
||||
)
|
||||
except RedmineAPIError as e:
|
||||
print(f"❌ {e.message}", file=sys.stderr)
|
||||
if args.debug and e.original is not None:
|
||||
print(f"❌ {_sanitize_error_text(e.message)}", file=sys.stderr)
|
||||
if (args.verbose or args.debug) and e.original is not None:
|
||||
logging.exception("Original Redmine API error")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}", file=sys.stderr)
|
||||
print(f"❌ Unexpected error: {_sanitize_error_text(str(e))}", file=sys.stderr)
|
||||
if args.verbose or args.debug:
|
||||
logging.exception("Unexpected error")
|
||||
return 1
|
||||
|
||||
if issue_hours is None:
|
||||
|
||||
Reference in New Issue
Block a user