Files
wehub-resource-sync 7a0da7932b
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
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

247 lines
11 KiB
Python

"""Regression tests for URL-context redaction in engine log lines.
Comparison review of issue #4183 found that even after ``_scrub_error(e)``
scrubs the exception message, the URL interpolated into the *same* log line
(``url``, ``wayback_url``, ``self.instance_url``) was written to logs raw.
URLs can carry HTTP basic-auth userinfo (``https://user:pass@host``) or
credential-bearing query params (``?api_key=...``), both of which leaked.
These tests pin the fix: every URL interpolation in a log line goes through
``redact_url_for_log`` so only ``scheme://host:port`` reaches the log sink.
Uses ``loguru_caplog`` (not ``loguru_caplog_full``) because these engines
log exceptions through the diagnose-gated ``security.secure_logging``
wrapper: at ERROR level, but with no exception attached to the record
outside diagnose mode — so there is no rendered exception block to
capture, only the formatted message. (The wrapper's own gating is pinned
in ``test_secure_logging.py``.) The levelname assertions guard against a
re-downgrade to WARNING.
"""
from unittest.mock import Mock, patch
import requests
from local_deep_research.web_search_engines.engines.search_engine_paperless import (
PaperlessSearchEngine,
)
from local_deep_research.web_search_engines.engines.search_engine_searxng import (
SearXNGSearchEngine,
)
from local_deep_research.web_search_engines.engines.search_engine_wayback import (
WaybackSearchEngine,
)
WAYBACK_MODULE = (
"local_deep_research.web_search_engines.engines.search_engine_wayback"
)
SEARXNG_MODULE = (
"local_deep_research.web_search_engines.engines.search_engine_searxng"
)
PAPERLESS_MODULE = (
"local_deep_research.web_search_engines.engines.search_engine_paperless"
)
OLLAMA_MODULE = (
"local_deep_research.embeddings.providers.implementations.ollama"
)
def _mock_response(status=200, json_body=None, text=""):
resp = Mock()
resp.status_code = status
resp.json.return_value = json_body if json_body is not None else {}
resp.text = text
return resp
class TestWaybackUrlRedaction:
def test_snapshots_warning_redacts_userinfo(self, loguru_caplog):
"""``url`` with ``user:pass@`` must not leak the password."""
engine = WaybackSearchEngine()
cred_url = "https://user:hunter2@example.com/page"
with patch(f"{WAYBACK_MODULE}.safe_get", side_effect=OSError("boom")):
with loguru_caplog.at_level("ERROR"):
engine._get_wayback_snapshots(cred_url)
assert "hunter2" not in loguru_caplog.text
assert "user:hunter2@" not in loguru_caplog.text
# The error log fired AND carries the redacted form — tie the two
# together so the assertion can't false-pass on an unrelated log line
# that happens to mention the host.
assert any(
"Error getting Wayback snapshots for" in line
and "https://example.com" in line
for line in loguru_caplog.text.splitlines()
)
matching = [
r
for r in loguru_caplog.records
if "Error getting Wayback snapshots for" in r.getMessage()
]
assert matching
assert all(r.levelname == "ERROR" for r in matching)
def test_snapshots_warning_redacts_query_token(self, loguru_caplog):
"""Line 236: credential-bearing query params must not leak."""
engine = WaybackSearchEngine()
cred_url = "https://example.com/page?api_key=xyz12345abcde"
with patch(f"{WAYBACK_MODULE}.safe_get", side_effect=OSError("boom")):
with loguru_caplog.at_level("ERROR"):
engine._get_wayback_snapshots(cred_url)
assert "xyz12345abcde" not in loguru_caplog.text
assert "api_key=" not in loguru_caplog.text
def test_full_content_redacts_wayback_url(self, loguru_caplog):
"""``wayback_url`` carrying embedded creds must not leak.
Covers the info log on the happy path ("Retrieving content from …")
and the error log when ``_get_wayback_content`` then raises ("Error
processing …"). The wayback_url embeds the original target in its
path; if that target had basic-auth userinfo, the raw log line
would leak it.
"""
engine = WaybackSearchEngine()
cred_wayback_url = (
"https://web.archive.org/web/20240101/"
"https://user:hunter2@example.com/page"
)
items = [{"link": cred_wayback_url, "title": "X"}]
with patch(f"{WAYBACK_MODULE}.safe_get", side_effect=OSError("boom")):
with loguru_caplog.at_level("INFO"):
engine._get_full_content(items)
assert "hunter2" not in loguru_caplog.text
assert "user:hunter2@" not in loguru_caplog.text
matching = [
r
for r in loguru_caplog.records
if "Error retrieving content from" in r.getMessage()
]
assert matching
assert all(r.levelname == "ERROR" for r in matching)
class TestSearXNGUrlRedaction:
def test_init_redacts_basic_auth_instance_url(self, loguru_caplog):
"""All ``self.instance_url`` log sites must redact basic-auth creds.
Self-hosted SearXNG behind HTTP basic auth is a common deployment
(``https://user:pass@host``); the init path logs the instance URL at
info, and on connection failure the warning fires with it too.
"""
cred_url = "https://admin:s3cret-pw@searxng.example.com"
with patch(
f"{SEARXNG_MODULE}.safe_get",
side_effect=requests.ConnectionError("nope"),
):
with loguru_caplog.at_level("INFO"):
SearXNGSearchEngine(instance_url=cred_url)
assert "s3cret-pw" not in loguru_caplog.text
assert "admin:s3cret-pw@" not in loguru_caplog.text
# The connection-failure error log fired AND carries the redacted
# host — tie the two together so the assertion can't false-pass on
# the init info log alone.
assert any(
"Error while trying to access SearXNG instance at" in line
and "searxng.example.com" in line
for line in loguru_caplog.text.splitlines()
)
matching = [
r
for r in loguru_caplog.records
if "Error while trying to access SearXNG instance at"
in r.getMessage()
]
assert matching
assert all(r.levelname == "ERROR" for r in matching)
def test_debug_logs_redact_scraped_result_url(self, loguru_caplog):
"""Result-href debug logs (extracted + filtered) must redact too.
Scraped result hrefs come from the internet, not operator config, so
the leak vector is a malicious result with a credential-bearing URL —
lower severity than the instance_url vector but same leak class.
"""
cred_result_url = "https://user:hunter2@evil.example.com/path"
# Build a result element whose href carries basic-auth creds. The
# ``_is_valid_search_result`` check rejects URLs starting with the
# instance URL, so use an instance URL that doesn't prefix-match.
html = (
'<html><body><article class="result">'
f'<a class="result-title" href="{cred_result_url}">title</a>'
'<a class="result-url" href="{cred}">link</a>'
'<div class="result-content">snippet</div>'
"</article></body></html>"
).format(cred=cred_result_url)
resp = _mock_response(status=200, text=html)
with patch(f"{SEARXNG_MODULE}.safe_get", return_value=resp):
engine = SearXNGSearchEngine(
instance_url="http://searxng-invalid-host-99999.local:8080",
)
engine.is_available = True
engine.search_url = (
"http://searxng-invalid-host-99999.local:8080/search"
)
with loguru_caplog.at_level("DEBUG"):
engine._get_search_results("anything")
assert "hunter2" not in loguru_caplog.text
assert "user:hunter2@" not in loguru_caplog.text
class TestPaperlessUrlRedaction:
"""Paperless-ngx ``api_url`` carries HTTP basic-auth creds in many
self-hosted deployments (``https://user:pass@host``). Every log site
that interpolates it — init info, per-request debug, failed-request
debug — must go through ``redact_url_for_log``."""
def test_init_and_request_logs_redact_basic_auth(self, loguru_caplog):
cred_url = "https://admin:s3cret-pw@paperless.example.com"
with (
patch(f"{PAPERLESS_MODULE}.safe_get") as mock_get,
patch(f"{PAPERLESS_MODULE}.requests.get") as mock_requests_get,
):
mock_get.side_effect = requests.ConnectionError("nope")
mock_requests_get.side_effect = requests.ConnectionError("nope")
with loguru_caplog.at_level("DEBUG"):
engine = PaperlessSearchEngine(api_url=cred_url, api_key="x")
engine._make_request("/api/documents/")
assert "s3cret-pw" not in loguru_caplog.text
assert "admin:s3cret-pw@" not in loguru_caplog.text
# The init info log fired AND carries the redacted host — tie them
# together so the assertion can't false-pass on an unrelated log.
assert any(
"Initialized Paperless-ngx search engine for" in line
and "paperless.example.com" in line
for line in loguru_caplog.text.splitlines()
)
# The per-request debug log also fired with the redacted URL.
assert any(
"Making request to:" in line and "paperless.example.com" in line
for line in loguru_caplog.text.splitlines()
)
class TestOllamaBaseUrlRedaction:
"""Ollama ``base_url`` is operator-configured (settings/env) and may
sit behind a reverse proxy with HTTP basic auth. The init info log
that announces the resolved base_url must redact it."""
def test_create_embeddings_info_redacts_basic_auth(self, loguru_caplog):
cred_url = "https://user:hunter2@ollama.example.com"
with patch(f"{OLLAMA_MODULE}.OllamaEmbeddings") as mock_inner:
mock_inner.return_value = Mock()
from local_deep_research.embeddings.providers.implementations.ollama import (
OllamaEmbeddingsProvider,
)
with loguru_caplog.at_level("INFO"):
OllamaEmbeddingsProvider.create_embeddings(
model="nomic-embed-text", base_url=cred_url
)
assert "hunter2" not in loguru_caplog.text
assert "user:hunter2@" not in loguru_caplog.text
assert any(
"Creating OllamaEmbeddings with model=" in line
and "ollama.example.com" in line
for line in loguru_caplog.text.splitlines()
)