7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
215 lines
7.6 KiB
Python
215 lines
7.6 KiB
Python
"""Tests for the diagnose-gated ``SecureLogger`` wrapper.
|
|
|
|
``security.secure_logging.logger.exception()`` must log at ERROR level
|
|
while attaching the active exception to the record ONLY when both
|
|
``LDR_APP_DEBUG`` and ``LDR_LOGURU_DIAGNOSE`` are truthy. With the
|
|
exception stripped, no sink can render the traceback or the
|
|
``__cause__`` chain — the #4183 leak class (headers/URLs embedded in
|
|
wrapped exceptions) cannot reach any log output.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.security.secure_logging import (
|
|
SecureLogger,
|
|
env_truthy,
|
|
is_diagnose_mode,
|
|
logger,
|
|
)
|
|
|
|
_DEBUG_VAR = "LDR_APP_DEBUG"
|
|
_DIAGNOSE_VAR = "LDR_LOGURU_DIAGNOSE"
|
|
|
|
|
|
def _set_env(monkeypatch, debug, diagnose):
|
|
"""Set/unset both gating vars. ``None`` means unset."""
|
|
for var, value in ((_DEBUG_VAR, debug), (_DIAGNOSE_VAR, diagnose)):
|
|
if value is None:
|
|
monkeypatch.delenv(var, raising=False)
|
|
else:
|
|
monkeypatch.setenv(var, value)
|
|
|
|
|
|
def _log_wrapped_exception(message="request failed: scrubbed"):
|
|
"""Log *message* from inside a handler for a cause-chained exception.
|
|
|
|
The inner ``ValueError`` carries a fake credential — the same shape
|
|
as the #4183 leak, where the secret lived only in the ``__cause__``
|
|
chain, not in the logged message.
|
|
"""
|
|
try:
|
|
try:
|
|
raise ValueError("api_key=sk-LEAK-0123456789abcdef")
|
|
except ValueError as inner:
|
|
raise RuntimeError("wrapped") from inner
|
|
except RuntimeError:
|
|
logger.exception(message)
|
|
|
|
|
|
class TestEnvTruthy:
|
|
@pytest.mark.parametrize("value", ["1", "true", "TRUE", "yes", " YES "])
|
|
def test_truthy_values(self, monkeypatch, value):
|
|
monkeypatch.setenv(_DEBUG_VAR, value)
|
|
assert env_truthy(_DEBUG_VAR) is True
|
|
|
|
@pytest.mark.parametrize("value", ["", "0", "false", "no", "on", "2"])
|
|
def test_falsy_values(self, monkeypatch, value):
|
|
monkeypatch.setenv(_DEBUG_VAR, value)
|
|
assert env_truthy(_DEBUG_VAR) is False
|
|
|
|
def test_unset_is_falsy(self, monkeypatch):
|
|
monkeypatch.delenv(_DEBUG_VAR, raising=False)
|
|
assert env_truthy(_DEBUG_VAR) is False
|
|
|
|
|
|
class TestIsDiagnoseMode:
|
|
@pytest.mark.parametrize(
|
|
("debug", "diagnose", "expected"),
|
|
[
|
|
(None, None, False),
|
|
("1", None, False),
|
|
(None, "1", False),
|
|
("0", "1", False),
|
|
("1", "0", False),
|
|
("1", "1", True),
|
|
("true", "yes", True),
|
|
("YES ", "TRUE", True),
|
|
],
|
|
)
|
|
def test_truth_table(self, monkeypatch, debug, diagnose, expected):
|
|
_set_env(monkeypatch, debug, diagnose)
|
|
assert is_diagnose_mode() is expected
|
|
|
|
|
|
class TestExceptionDefaultMode:
|
|
"""Both flags unset — production behaviour."""
|
|
|
|
def test_logs_at_error_level_without_traceback(
|
|
self, monkeypatch, loguru_caplog_full
|
|
):
|
|
_set_env(monkeypatch, None, None)
|
|
with loguru_caplog_full.at_level("ERROR"):
|
|
_log_wrapped_exception()
|
|
|
|
records = [
|
|
r
|
|
for r in loguru_caplog_full.records
|
|
if "request failed: scrubbed" in r.getMessage()
|
|
]
|
|
assert records, "the event must still be logged"
|
|
assert all(r.levelname == "ERROR" for r in records)
|
|
assert all(r.exc_info is None for r in records)
|
|
assert "Traceback" not in loguru_caplog_full.text
|
|
|
|
def test_cause_chain_secret_never_reaches_output(
|
|
self, monkeypatch, loguru_caplog_full
|
|
):
|
|
"""#4183 regression: the fixture renders ``{exception}`` with
|
|
``backtrace=True``, so if the exception were attached the
|
|
``__cause__`` chain (holding the credential) WOULD appear here."""
|
|
_set_env(monkeypatch, None, None)
|
|
with loguru_caplog_full.at_level("ERROR"):
|
|
_log_wrapped_exception()
|
|
|
|
assert "request failed: scrubbed" in loguru_caplog_full.text
|
|
assert "sk-LEAK" not in loguru_caplog_full.text
|
|
assert "api_key" not in loguru_caplog_full.text
|
|
|
|
@pytest.mark.parametrize(
|
|
("debug", "diagnose"),
|
|
[("1", None), (None, "1"), ("1", "0"), ("0", "1")],
|
|
)
|
|
def test_single_flag_does_not_enable_traceback(
|
|
self, monkeypatch, loguru_caplog_full, debug, diagnose
|
|
):
|
|
_set_env(monkeypatch, debug, diagnose)
|
|
with loguru_caplog_full.at_level("ERROR"):
|
|
_log_wrapped_exception()
|
|
|
|
assert "request failed: scrubbed" in loguru_caplog_full.text
|
|
assert "Traceback" not in loguru_caplog_full.text
|
|
assert "sk-LEAK" not in loguru_caplog_full.text
|
|
|
|
|
|
class TestExceptionDiagnoseMode:
|
|
def test_both_flags_render_traceback(self, monkeypatch, loguru_caplog_full):
|
|
_set_env(monkeypatch, "1", "1")
|
|
with loguru_caplog_full.at_level("ERROR"):
|
|
_log_wrapped_exception()
|
|
|
|
records = [
|
|
r
|
|
for r in loguru_caplog_full.records
|
|
if "request failed: scrubbed" in r.getMessage()
|
|
]
|
|
assert records
|
|
assert all(r.levelname == "ERROR" for r in records)
|
|
assert "Traceback" in loguru_caplog_full.text
|
|
assert "RuntimeError" in loguru_caplog_full.text
|
|
|
|
|
|
class TestRecordAttribution:
|
|
def test_record_points_at_caller_not_wrapper(self, monkeypatch):
|
|
"""``depth=1`` must attribute the record to the calling module so
|
|
log locations stay useful and ``logger.disable()`` namespacing
|
|
keys off the caller, not ``secure_logging``."""
|
|
_set_env(monkeypatch, None, None)
|
|
captured = []
|
|
handler_id = logger.add(
|
|
lambda message: captured.append(message.record),
|
|
format="{message}",
|
|
level="ERROR",
|
|
)
|
|
try:
|
|
try:
|
|
raise RuntimeError("boom")
|
|
except RuntimeError:
|
|
logger.exception("attribution check")
|
|
finally:
|
|
logger.remove(handler_id)
|
|
|
|
records = [r for r in captured if r["message"] == "attribution check"]
|
|
assert records
|
|
record = records[-1]
|
|
assert record["module"] == "test_secure_logging"
|
|
assert record["function"] == "test_record_points_at_caller_not_wrapper"
|
|
assert record["exception"] is None
|
|
|
|
|
|
class TestWrapperChaining:
|
|
def test_bind_keeps_gated_exception(self, monkeypatch, loguru_caplog_full):
|
|
_set_env(monkeypatch, None, None)
|
|
bound = logger.bind(component="test")
|
|
assert isinstance(bound, SecureLogger)
|
|
with loguru_caplog_full.at_level("ERROR"):
|
|
try:
|
|
try:
|
|
raise ValueError("api_key=sk-LEAK-0123456789abcdef")
|
|
except ValueError as inner:
|
|
raise RuntimeError("wrapped") from inner
|
|
except RuntimeError:
|
|
bound.exception("bound scrubbed message")
|
|
|
|
assert "bound scrubbed message" in loguru_caplog_full.text
|
|
assert "Traceback" not in loguru_caplog_full.text
|
|
assert "sk-LEAK" not in loguru_caplog_full.text
|
|
|
|
def test_patch_returns_wrapper(self):
|
|
patched = logger.patch(lambda record: None)
|
|
assert isinstance(patched, SecureLogger)
|
|
|
|
def test_no_active_exception_does_not_raise(
|
|
self, monkeypatch, loguru_caplog_full
|
|
):
|
|
_set_env(monkeypatch, None, None)
|
|
with loguru_caplog_full.at_level("ERROR"):
|
|
logger.exception("called outside except block")
|
|
assert "called outside except block" in loguru_caplog_full.text
|
|
|
|
def test_delegates_other_methods_to_loguru(
|
|
self, monkeypatch, loguru_caplog_full
|
|
):
|
|
with loguru_caplog_full.at_level("INFO"):
|
|
logger.info("plain info via wrapper")
|
|
assert "plain info via wrapper" in loguru_caplog_full.text
|