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

503 lines
18 KiB
Python

"""
Tests for DataSanitizer security module.
"""
from local_deep_research.security.data_sanitizer import (
DataSanitizer,
filter_research_metadata,
redact_data,
sanitize_data,
strip_settings_snapshot,
)
class TestDataSanitizerSanitize:
"""Tests for DataSanitizer.sanitize()."""
def test_sanitize_removes_api_key(self):
"""Removes api_key from data."""
data = {"username": "user", "api_key": "sk-secret-12345"}
result = DataSanitizer.sanitize(data)
assert "username" in result
assert "api_key" not in result
def test_sanitize_removes_password(self):
"""Removes password from data."""
data = {"user": "test", "password": "supersecret"}
result = DataSanitizer.sanitize(data)
assert "user" in result
assert "password" not in result
def test_sanitize_removes_access_token(self):
"""Removes access_token from data."""
data = {"id": 1, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}
result = DataSanitizer.sanitize(data)
assert "id" in result
assert "access_token" not in result
def test_sanitize_case_insensitive(self):
"""Sanitization is case-insensitive."""
data = {"API_KEY": "secret1", "ApiKey": "secret2", "apikey": "secret3"}
result = DataSanitizer.sanitize(data)
assert len(result) == 0
def test_sanitize_nested_dict(self):
"""Sanitizes nested dictionaries."""
data = {
"user": "test",
"settings": {
"theme": "dark",
"api_key": "nested_secret",
},
}
result = DataSanitizer.sanitize(data)
assert result["user"] == "test"
assert "theme" in result["settings"]
assert "api_key" not in result["settings"]
def test_sanitize_deeply_nested(self):
"""Sanitizes deeply nested structures."""
data = {
"level1": {
"level2": {
"level3": {
"password": "deep_secret",
"value": "keep_this",
}
}
}
}
result = DataSanitizer.sanitize(data)
assert result["level1"]["level2"]["level3"]["value"] == "keep_this"
assert "password" not in result["level1"]["level2"]["level3"]
def test_sanitize_list_of_dicts(self):
"""Sanitizes list of dictionaries."""
data = [
{"name": "item1", "secret": "value1"},
{"name": "item2", "secret": "value2"},
]
result = DataSanitizer.sanitize(data)
assert len(result) == 2
assert result[0]["name"] == "item1"
assert "secret" not in result[0]
assert result[1]["name"] == "item2"
assert "secret" not in result[1]
def test_sanitize_mixed_structure(self):
"""Sanitizes mixed nested structures."""
data = {
"users": [
{"username": "user1", "auth_token": "token1"},
{"username": "user2", "auth_token": "token2"},
],
"config": {
"public": True,
"private_key": "key123",
},
}
result = DataSanitizer.sanitize(data)
assert result["users"][0]["username"] == "user1"
assert "auth_token" not in result["users"][0]
assert result["config"]["public"] is True
assert "private_key" not in result["config"]
def test_sanitize_preserves_primitives(self):
"""Preserves primitive values unchanged."""
assert DataSanitizer.sanitize("string") == "string"
assert DataSanitizer.sanitize(123) == 123
assert DataSanitizer.sanitize(12.5) == 12.5
assert DataSanitizer.sanitize(True) is True
assert DataSanitizer.sanitize(None) is None
def test_sanitize_empty_dict(self):
"""Handles empty dictionary."""
assert DataSanitizer.sanitize({}) == {}
def test_sanitize_empty_list(self):
"""Handles empty list."""
assert DataSanitizer.sanitize([]) == []
def test_sanitize_custom_keys(self):
"""Uses custom sensitive keys."""
data = {
"custom_secret": "value",
"api_key": "standard_secret",
"name": "keep",
}
result = DataSanitizer.sanitize(data, sensitive_keys={"custom_secret"})
assert "custom_secret" not in result
assert "api_key" in result # Not in custom keys
assert "name" in result
def test_sanitize_all_default_keys(self):
"""Tests all default sensitive keys."""
data = {
"api_key": "1",
"apikey": "2",
"password": "3",
"secret": "4",
"access_token": "5",
"refresh_token": "6",
"private_key": "7",
"auth_token": "8",
"session_token": "9",
"csrf_token": "10",
"safe_key": "keep",
}
result = DataSanitizer.sanitize(data)
assert result == {"safe_key": "keep"}
class TestDataSanitizerRedact:
"""Tests for DataSanitizer.redact()."""
def test_redact_replaces_api_key(self):
"""Replaces api_key value with redaction text."""
data = {"username": "user", "api_key": "sk-secret-12345"}
result = DataSanitizer.redact(data)
assert result["username"] == "user"
assert result["api_key"] == "[REDACTED]"
def test_redact_preserves_structure(self):
"""Preserves data structure while redacting values."""
data = {"id": 1, "password": "secret123"}
result = DataSanitizer.redact(data)
assert "id" in result
assert "password" in result
assert result["password"] == "[REDACTED]"
def test_redact_custom_text(self):
"""Uses custom redaction text."""
data = {"password": "secret"}
result = DataSanitizer.redact(data, redaction_text="***")
assert result["password"] == "***"
def test_redact_case_insensitive(self):
"""Redaction is case-insensitive."""
data = {"API_KEY": "secret1", "ApiKey": "secret2", "apikey": "secret3"}
result = DataSanitizer.redact(data)
assert all(v == "[REDACTED]" for v in result.values())
def test_redact_nested_dict(self):
"""Redacts nested dictionaries."""
data = {
"user": "test",
"settings": {
"theme": "dark",
"api_key": "nested_secret",
},
}
result = DataSanitizer.redact(data)
assert result["user"] == "test"
assert result["settings"]["theme"] == "dark"
assert result["settings"]["api_key"] == "[REDACTED]"
def test_redact_list_of_dicts(self):
"""Redacts list of dictionaries."""
data = [
{"name": "item1", "secret": "value1"},
{"name": "item2", "secret": "value2"},
]
result = DataSanitizer.redact(data)
assert result[0]["name"] == "item1"
assert result[0]["secret"] == "[REDACTED]"
assert result[1]["name"] == "item2"
assert result[1]["secret"] == "[REDACTED]"
def test_redact_preserves_primitives(self):
"""Preserves primitive values unchanged."""
assert DataSanitizer.redact("string") == "string"
assert DataSanitizer.redact(123) == 123
assert DataSanitizer.redact(True) is True
def test_redact_custom_keys(self):
"""Uses custom sensitive keys."""
data = {"custom_secret": "value", "api_key": "keep", "name": "keep"}
result = DataSanitizer.redact(data, sensitive_keys={"custom_secret"})
assert result["custom_secret"] == "[REDACTED]"
assert result["api_key"] == "keep" # Not in custom keys
assert result["name"] == "keep"
class TestConvenienceFunctions:
"""Tests for convenience functions."""
def test_sanitize_data_function(self):
"""sanitize_data() works correctly."""
data = {"username": "user", "password": "secret"}
result = sanitize_data(data)
assert "username" in result
assert "password" not in result
def test_sanitize_data_with_custom_keys(self):
"""sanitize_data() with custom keys."""
data = {"custom": "secret", "keep": "value"}
result = sanitize_data(data, sensitive_keys={"custom"})
assert "custom" not in result
assert "keep" in result
def test_redact_data_function(self):
"""redact_data() works correctly."""
data = {"username": "user", "password": "secret"}
result = redact_data(data)
assert result["username"] == "user"
assert result["password"] == "[REDACTED]"
def test_redact_data_with_custom_text(self):
"""redact_data() with custom redaction text."""
data = {"password": "secret"}
result = redact_data(data, redaction_text="<hidden>")
assert result["password"] == "<hidden>"
class TestDefaultSensitiveKeys:
"""Tests for DEFAULT_SENSITIVE_KEYS constant."""
def test_default_keys_exist(self):
"""Default sensitive keys are defined."""
assert isinstance(DataSanitizer.DEFAULT_SENSITIVE_KEYS, set)
assert len(DataSanitizer.DEFAULT_SENSITIVE_KEYS) > 0
def test_default_keys_include_common_secrets(self):
"""Default keys include common secret patterns."""
expected = {
"api_key",
"password",
"secret",
"access_token",
"refresh_token",
"private_key",
}
assert expected.issubset(DataSanitizer.DEFAULT_SENSITIVE_KEYS)
class TestExpandedSecretLeafNames:
"""Secret leaf-names beyond the classic set must be caught by the
name-only predicate the bulk settings GET relies on (ui_element=None).
"""
NEW_NAMES = [
"client_secret",
"secret_key",
"bearer_token",
"api_secret",
"app_secret",
]
def test_present_in_default_set(self):
assert set(self.NEW_NAMES).issubset(
DataSanitizer.DEFAULT_SENSITIVE_KEYS
)
def test_recognized_by_leaf(self):
for name in self.NEW_NAMES:
key = f"integrations.oauth.{name}"
assert DataSanitizer.is_sensitive_setting(key)
# mirrors the bulk GET call: redact_value(key, None, value)
assert (
DataSanitizer.redact_value(key, None, "topsecret-value")
== DataSanitizer.REDACTION_TEXT
)
def test_no_over_redaction_of_similar_names(self):
# exact-leaf match must NOT redact these legit non-secret keys.
# The last two leaves CONTAIN a sensitive name as a proper substring
# ("password" in "passwordless", "secret" in "secretary") — they pin
# the predicate to exact-match, so a substring-broadening regression
# (e.g. `any(s in leaf ...)`) would be caught here.
for key in (
"llm.max_tokens",
"cache.key",
"search.public_key",
"app.token_count",
"oauth.client_id",
"llm.passwordless",
"integrations.secretary",
):
assert DataSanitizer.redact_value(key, None, "value") == "value"
class TestEdgeCases:
"""Edge case tests for DataSanitizer."""
def test_sanitize_with_none_sensitive_keys(self):
"""Uses default keys when sensitive_keys is None."""
data = {"api_key": "secret", "name": "keep"}
result = DataSanitizer.sanitize(data, sensitive_keys=None)
assert "api_key" not in result
assert "name" in result
def test_sanitize_with_empty_sensitive_keys(self):
"""Empty sensitive keys keeps all data."""
data = {"api_key": "secret", "password": "pass"}
result = DataSanitizer.sanitize(data, sensitive_keys=set())
assert "api_key" in result
assert "password" in result
def test_sanitize_list_with_primitives(self):
"""Handles list with primitive values."""
data = [1, 2, "string", None, True]
result = DataSanitizer.sanitize(data)
assert result == [1, 2, "string", None, True]
def test_sanitize_does_not_mutate_original(self):
"""Sanitization does not mutate original data."""
original = {"api_key": "secret", "name": "keep"}
_ = DataSanitizer.sanitize(original)
assert "api_key" in original
assert original["api_key"] == "secret"
def test_redact_does_not_mutate_original(self):
"""Redaction does not mutate original data."""
original = {"password": "secret", "name": "keep"}
_ = DataSanitizer.redact(original)
assert original["password"] == "secret"
class TestFilterResearchMetadata:
"""Tests for filter_research_metadata() helper."""
def test_none_input(self):
"""None returns default."""
assert filter_research_metadata(None) == {"is_news_search": False}
def test_empty_dict(self):
"""Empty dict returns default."""
assert filter_research_metadata({}) == {"is_news_search": False}
def test_strips_settings_snapshot(self):
"""Extracts only is_news_search, strips settings_snapshot."""
meta = {
"is_news_search": True,
"settings_snapshot": {"api_key": "sk-secret"},
}
result = filter_research_metadata(meta)
assert result == {"is_news_search": True}
assert "settings_snapshot" not in result
def test_explicit_false_preserved(self):
"""Explicit is_news_search=False is preserved."""
assert filter_research_metadata({"is_news_search": False}) == {
"is_news_search": False
}
def test_json_string_input(self):
"""JSON string is parsed correctly."""
result = filter_research_metadata('{"is_news_search": true}')
assert result == {"is_news_search": True}
def test_invalid_json_string(self):
"""Invalid JSON string returns default."""
assert filter_research_metadata("invalid json") == {
"is_news_search": False
}
def test_non_dict_json(self):
"""Non-dict JSON (e.g. array) returns default."""
assert filter_research_metadata("[]") == {"is_news_search": False}
def test_non_string_non_dict(self):
"""Non-string, non-dict input (e.g. int) returns default."""
assert filter_research_metadata(42) == {"is_news_search": False}
class TestStripSettingsSnapshot:
"""Tests for strip_settings_snapshot() helper."""
def test_none_input(self):
"""None returns empty dict."""
assert strip_settings_snapshot(None) == {}
def test_strips_settings_snapshot(self):
"""Removes settings_snapshot, keeps other keys."""
meta = {
"phase": "complete",
"settings_snapshot": {"api_key": "sk-secret"},
}
assert strip_settings_snapshot(meta) == {"phase": "complete"}
def test_no_op_when_absent(self):
"""Returns same dict when settings_snapshot absent."""
meta = {"phase": "error", "error_type": "timeout"}
assert strip_settings_snapshot(meta) == meta
def test_only_exact_key_stripped(self):
"""Only strips exact 'settings_snapshot' key, not similar names."""
meta = {"settings_version": 2, "settings_snapshot": {}}
assert strip_settings_snapshot(meta) == {"settings_version": 2}
def test_json_string_input(self):
"""JSON string is parsed and stripped."""
result = strip_settings_snapshot(
'{"phase": "done", "settings_snapshot": {}}'
)
assert result == {"phase": "done"}
def test_invalid_json_string(self):
"""Invalid JSON string returns empty dict."""
assert strip_settings_snapshot("invalid json") == {}
def test_does_not_mutate_original(self):
"""Original input dict is NOT modified."""
original = {
"phase": "complete",
"settings_snapshot": {"api_key": "sk-secret"},
}
_ = strip_settings_snapshot(original)
assert "settings_snapshot" in original
assert original["settings_snapshot"]["api_key"] == "sk-secret"
class TestRedactValueNestedDict:
"""redact_value must recurse into subtree dicts.
A namespace request (e.g. get_setting("llm")) returns a nested dict whose
outer key is not sensitive but whose inner keys are — without recursion
those nested secrets would ship in the clear via GET /settings/api/bulk.
"""
def test_nested_sensitive_leaf_redacted(self):
value = {"provider": "openai", "openai.api_key": "sk-secret"}
out = DataSanitizer.redact_value("llm", None, value)
assert out["openai.api_key"] == DataSanitizer.REDACTION_TEXT
assert out["provider"] == "openai"
def test_deeply_nested_secret_redacted(self):
value = {"openai": {"api_key": "sk-secret"}}
out = DataSanitizer.redact_value("llm", None, value)
assert out["openai"]["api_key"] == DataSanitizer.REDACTION_TEXT
def test_sensitive_outer_key_with_dict_redacted_wholesale(self):
# A sensitive-named key holding a dict is masked entirely (not recursed)
out = DataSanitizer.redact_value("x.api_key", None, {"nested": "v"})
assert out == DataSanitizer.REDACTION_TEXT
def test_primitive_value_unchanged(self):
assert (
DataSanitizer.redact_value("llm.provider", None, "openai")
== "openai"
)
def test_empty_dict_unchanged(self):
assert DataSanitizer.redact_value("llm", None, {}) == {}
def test_list_of_dicts_secret_redacted(self):
# a JSON list-of-dicts leaf (e.g. mcp.servers) must not ship secrets
value = [{"name": "prod", "api_key": "sk-secret"}]
out = DataSanitizer.redact_value("mcp.servers", None, value)
assert out[0]["api_key"] == DataSanitizer.REDACTION_TEXT
assert out[0]["name"] == "prod"
def test_plain_list_unchanged(self):
# a list of scalars under a non-sensitive key is not mangled
value = ["host1", "host2"]
assert DataSanitizer.redact_value("search.hosts", None, value) == [
"host1",
"host2",
]
def test_sensitive_key_with_list_redacted_wholesale(self):
out = DataSanitizer.redact_value("x.secret", None, ["a", "b"])
assert out == DataSanitizer.REDACTION_TEXT