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
191 lines
7.2 KiB
Python
191 lines
7.2 KiB
Python
"""Tests for DataSanitizer.redact_settings_snapshot.
|
|
|
|
The plain DataSanitizer.redact() does NOT redact secrets in nested-with-metadata
|
|
snapshots like {"llm.openai.api_key": {"value": "sk-...", "ui_element": "password"}}
|
|
— the outer compound key isn't in the sensitive-name set, and the inner key
|
|
"value" isn't sensitive either. redact_settings_snapshot fixes that for the
|
|
specific shape produced by SettingsManager.get_all_settings().
|
|
"""
|
|
|
|
from local_deep_research.security.data_sanitizer import DataSanitizer
|
|
|
|
|
|
def test_redacts_value_when_ui_element_is_password():
|
|
snap = {
|
|
"llm.openai.api_key": {
|
|
"value": "sk-secret123",
|
|
"ui_element": "password",
|
|
"type": "LLM",
|
|
}
|
|
}
|
|
out = DataSanitizer.redact_settings_snapshot(snap)
|
|
assert out["llm.openai.api_key"]["value"] == "[REDACTED]"
|
|
|
|
|
|
def test_preserves_metadata_alongside_redacted_value():
|
|
"""ui_element/type/etc. survive so YAML diffs still show the field exists."""
|
|
snap = {
|
|
"llm.openai.api_key": {
|
|
"value": "sk-secret",
|
|
"ui_element": "password",
|
|
"type": "LLM",
|
|
"description": "OpenAI key",
|
|
}
|
|
}
|
|
out = DataSanitizer.redact_settings_snapshot(snap)
|
|
assert out["llm.openai.api_key"]["ui_element"] == "password"
|
|
assert out["llm.openai.api_key"]["type"] == "LLM"
|
|
assert out["llm.openai.api_key"]["description"] == "OpenAI key"
|
|
|
|
|
|
def test_does_not_redact_non_secret_settings():
|
|
snap = {
|
|
"search.fetch.mode": {
|
|
"value": "summary_focus_query",
|
|
"ui_element": "select",
|
|
"type": "SEARCH",
|
|
}
|
|
}
|
|
out = DataSanitizer.redact_settings_snapshot(snap)
|
|
assert out["search.fetch.mode"]["value"] == "summary_focus_query"
|
|
|
|
|
|
def test_empty_secret_value_stays_empty():
|
|
"""An unset API key reads as "" — leaving it empty is more useful than
|
|
"[REDACTED]" when diffing two runs to spot which had the key set."""
|
|
snap = {"llm.lmstudio.api_key": {"value": "", "ui_element": "password"}}
|
|
out = DataSanitizer.redact_settings_snapshot(snap)
|
|
assert out["llm.lmstudio.api_key"]["value"] == ""
|
|
|
|
|
|
def test_none_secret_value_stays_none():
|
|
snap = {"llm.lmstudio.api_key": {"value": None, "ui_element": "password"}}
|
|
out = DataSanitizer.redact_settings_snapshot(snap)
|
|
assert out["llm.lmstudio.api_key"]["value"] is None
|
|
|
|
|
|
def test_defense_in_depth_via_key_suffix():
|
|
"""ui_element=text + key suffix in DEFAULT_SENSITIVE_KEYS still redacts.
|
|
|
|
Catches developer error: a future plugin author who registers
|
|
`plugin.x.api_key` with the wrong ui_element should still have the
|
|
secret redacted because `api_key` is in DEFAULT_SENSITIVE_KEYS.
|
|
"""
|
|
snap = {"plugin.x.api_key": {"value": "sk-leaked", "ui_element": "text"}}
|
|
out = DataSanitizer.redact_settings_snapshot(snap)
|
|
assert out["plugin.x.api_key"]["value"] == "[REDACTED]"
|
|
|
|
|
|
def test_input_is_not_mutated():
|
|
"""Pure-function contract — the in-memory snapshot used by the running
|
|
benchmark thread must remain unredacted; only the persisted copy is
|
|
redacted."""
|
|
snap = {
|
|
"llm.openai.api_key": {
|
|
"value": "sk-secret",
|
|
"ui_element": "password",
|
|
}
|
|
}
|
|
DataSanitizer.redact_settings_snapshot(snap)
|
|
assert snap["llm.openai.api_key"]["value"] == "sk-secret"
|
|
|
|
|
|
def test_passes_through_non_metadata_entries():
|
|
"""Tolerates flat-shape entries (bare key→value) so the helper is safe
|
|
on mixed snapshots without crashing."""
|
|
snap = {"flat_key": "raw_value", "no_value_key": {"ui_element": "text"}}
|
|
out = DataSanitizer.redact_settings_snapshot(snap)
|
|
assert out["flat_key"] == "raw_value"
|
|
assert out["no_value_key"] == {"ui_element": "text"}
|
|
|
|
|
|
def test_passes_through_non_dict_input():
|
|
"""Defensive — a None or list snapshot returns unchanged."""
|
|
assert DataSanitizer.redact_settings_snapshot(None) is None
|
|
assert DataSanitizer.redact_settings_snapshot([1, 2, 3]) == [1, 2, 3]
|
|
|
|
|
|
def test_custom_redaction_text():
|
|
snap = {"llm.openai.api_key": {"value": "sk-x", "ui_element": "password"}}
|
|
out = DataSanitizer.redact_settings_snapshot(snap, redaction_text="***")
|
|
assert out["llm.openai.api_key"]["value"] == "***"
|
|
|
|
|
|
def test_is_sensitive_setting_predicate():
|
|
"""The shared predicate the GET redactor and the write-back guards both
|
|
use: password ui_element OR a sensitive key suffix."""
|
|
# ui_element wins regardless of key
|
|
assert DataSanitizer.is_sensitive_setting("anything.here", "password")
|
|
# sensitive suffix wins regardless of ui_element
|
|
assert DataSanitizer.is_sensitive_setting("llm.openai.api_key", "text")
|
|
assert DataSanitizer.is_sensitive_setting("x.password", None)
|
|
assert DataSanitizer.is_sensitive_setting("x.secret", "textarea")
|
|
# neither -> not sensitive
|
|
assert not DataSanitizer.is_sensitive_setting("llm.model", "text")
|
|
assert not DataSanitizer.is_sensitive_setting("search.tool", None)
|
|
|
|
|
|
def test_redactor_and_predicate_agree():
|
|
"""Anything the predicate calls sensitive (and has a non-empty value)
|
|
must be redacted by redact_settings_snapshot — they share one source."""
|
|
snap = {
|
|
"llm.openai.api_key": {"value": "sk-x", "ui_element": "text"},
|
|
"foo.password": {"value": "pw", "ui_element": "text"},
|
|
"llm.model": {"value": "gpt", "ui_element": "text"},
|
|
}
|
|
out = DataSanitizer.redact_settings_snapshot(snap)
|
|
for key, entry in snap.items():
|
|
sensitive = DataSanitizer.is_sensitive_setting(key, entry["ui_element"])
|
|
if sensitive:
|
|
assert out[key]["value"] == DataSanitizer.REDACTION_TEXT
|
|
else:
|
|
assert out[key]["value"] == entry["value"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# redact_value — the single-value primitive that the singular GET, the bulk
|
|
# GET and redact_settings_snapshot all delegate to.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_redact_value_masks_password_ui_element():
|
|
"""A set secret on a password-typed setting returns the sentinel."""
|
|
assert (
|
|
DataSanitizer.redact_value("llm.openai.api_key", "password", "sk-real")
|
|
== DataSanitizer.REDACTION_TEXT
|
|
)
|
|
|
|
|
|
def test_redact_value_masks_by_suffix_when_no_ui_element():
|
|
"""The bulk GET has no ui_element, so redact_value falls back to the
|
|
suffix arm of the predicate (key ends in a sensitive name)."""
|
|
assert (
|
|
DataSanitizer.redact_value("llm.openai.api_key", None, "sk-real")
|
|
== DataSanitizer.REDACTION_TEXT
|
|
)
|
|
|
|
|
|
def test_redact_value_passes_through_non_secret():
|
|
"""A non-sensitive setting is returned unchanged."""
|
|
assert DataSanitizer.redact_value("llm.model", "text", "gpt") == "gpt"
|
|
|
|
|
|
def test_redact_value_leaves_empty_values_readable():
|
|
"""Empty values are never masked — the UI must tell 'unset' from 'set'
|
|
without the sentinel implying a secret exists."""
|
|
for empty in (None, "", [], {}):
|
|
assert (
|
|
DataSanitizer.redact_value("llm.openai.api_key", "password", empty)
|
|
== empty
|
|
)
|
|
|
|
|
|
def test_redact_value_respects_custom_redaction_text():
|
|
"""Callers can override the sentinel."""
|
|
assert (
|
|
DataSanitizer.redact_value(
|
|
"x.password", None, "pw", redaction_text="***"
|
|
)
|
|
== "***"
|
|
)
|